@aglyn/plugins-ai 1.0.0-beta.278 → 1.0.0-beta.283

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/package.json +17 -17
  2. package/src/lib/activity/ai-activity-actions.d.ts +2 -26
  3. package/src/lib/activity/ai-activity-actions.js +5 -42
  4. package/src/lib/activity/ai-activity-actions.js.map +1 -1
  5. package/src/lib/activity/ai-activity-words.d.ts +42 -0
  6. package/src/lib/activity/ai-activity-words.js +63 -0
  7. package/src/lib/activity/ai-activity-words.js.map +1 -0
  8. package/src/lib/components/ai-insight-card.component.d.ts +2 -6
  9. package/src/lib/components/ai-insight-card.component.js +2 -6
  10. package/src/lib/components/ai-insight-card.component.js.map +1 -1
  11. package/src/lib/components/ai-insight-dialog.component.js +22 -32
  12. package/src/lib/components/ai-insight-dialog.component.js.map +1 -1
  13. package/src/lib/components/ai-job-links.js +15 -0
  14. package/src/lib/components/ai-job-links.js.map +1 -1
  15. package/src/lib/components/assist-panel.component.js +3 -1
  16. package/src/lib/components/assist-panel.component.js.map +1 -1
  17. package/src/lib/jobs/ai-build-capabilities.js +23 -0
  18. package/src/lib/jobs/ai-build-capabilities.js.map +1 -1
  19. package/src/lib/jobs/ai-job-build-step.js +2 -1
  20. package/src/lib/jobs/ai-job-build-step.js.map +1 -1
  21. package/src/lib/jobs/ai-job-import-step.d.ts +1 -0
  22. package/src/lib/jobs/ai-job-import-step.js +17 -11
  23. package/src/lib/jobs/ai-job-import-step.js.map +1 -1
  24. package/src/lib/jobs/ai-job-plan-step.js +3 -2
  25. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  26. package/src/lib/jobs/ai-job-privacy-step.d.ts +184 -0
  27. package/src/lib/jobs/ai-job-privacy-step.js +471 -0
  28. package/src/lib/jobs/ai-job-privacy-step.js.map +1 -0
  29. package/src/lib/jobs/ai-site-inspiration.d.ts +10 -0
  30. package/src/lib/jobs/ai-site-inspiration.js +15 -0
  31. package/src/lib/jobs/ai-site-inspiration.js.map +1 -1
  32. package/src/lib/model/ai-insight-ask.d.ts +57 -0
  33. package/src/lib/model/ai-insight-ask.js +64 -0
  34. package/src/lib/model/ai-insight-ask.js.map +1 -0
  35. package/src/lib/model/ai-insight.d.ts +1 -6
  36. package/src/lib/model/ai-insight.js +2 -8
  37. package/src/lib/model/ai-insight.js.map +1 -1
  38. package/src/lib/model/ai-job-activity.js +1 -0
  39. package/src/lib/model/ai-job-activity.js.map +1 -1
  40. package/src/lib/model/ai-jobs.types.d.ts +5 -1
  41. package/src/lib/model/ai-jobs.types.js +1 -0
  42. package/src/lib/model/ai-jobs.types.js.map +1 -1
  43. package/src/lib/model/ai-permissions.js +6 -2
  44. package/src/lib/model/ai-permissions.js.map +1 -1
  45. package/src/lib/model/ai-site-import.d.ts +2 -0
  46. package/src/lib/model/ai-site-import.js +2 -0
  47. package/src/lib/model/ai-site-import.js.map +1 -1
  48. package/src/lib/model/ai-site-kinds.d.ts +10 -28
  49. package/src/lib/model/ai-site-kinds.js +12 -496
  50. package/src/lib/model/ai-site-kinds.js.map +1 -1
  51. package/src/lib/providers/anthropic.js +23 -14
  52. package/src/lib/providers/anthropic.js.map +1 -1
  53. package/src/lib/providers/contract.d.ts +39 -4
  54. package/src/lib/providers/contract.js +28 -0
  55. package/src/lib/providers/contract.js.map +1 -1
  56. package/src/lib/providers/routing.d.ts +3 -0
  57. package/src/lib/providers/routing.js +4 -3
  58. package/src/lib/providers/routing.js.map +1 -1
  59. package/src/lib/runtime/ai-dev-env.d.ts +3 -2
  60. package/src/lib/runtime/ai-dev-env.js +3 -2
  61. package/src/lib/runtime/ai-dev-env.js.map +1 -1
  62. package/src/lib/runtime/ai-page-links.d.ts +5 -2
  63. package/src/lib/runtime/ai-page-links.js +11 -3
  64. package/src/lib/runtime/ai-page-links.js.map +1 -1
  65. package/src/lib/runtime/ai-runtime.js +5 -2
  66. package/src/lib/runtime/ai-runtime.js.map +1 -1
  67. package/src/lib/server/assist-chat.js +58 -30
  68. package/src/lib/server/assist-chat.js.map +1 -1
  69. package/src/lib/server.js +2 -0
  70. package/src/lib/server.js.map +1 -1
  71. package/src/lib/site-import/fixtures/site-import-crawl-recordings.d.ts +3 -1
  72. package/src/lib/site-import/fixtures/site-import-crawl-recordings.js +19 -1
  73. package/src/lib/site-import/fixtures/site-import-crawl-recordings.js.map +1 -1
  74. package/src/lib/site-import/site-import-page.d.ts +2 -2
  75. package/src/lib/site-import/site-import-page.js +11 -1
  76. package/src/lib/site-import/site-import-page.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/providers/routing.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 { AI_SETTING_PLATFORM, AI_STEP_MODEL_SETTING } from '../plugin-config'\nimport {\n AI_MODEL_CATALOG,\n AI_STEP_TIERS,\n aiCatalogEntry,\n aiDefaultModelFor,\n type AiStepKind,\n} from './catalog'\nimport type { AiEvalKind } from '../runtime/ai-eval'\nimport type { AiEffort, AiProvider, AiThinking } from './contract'\nimport { aiProviderById, defaultAiProvider } from './registry'\n\n/**\n * The routing table (AGL-2937, AGL-2939): step kind → provider and model,\n * through the catalog and never a vendor literal.\n *\n * Precedence, dearest override first:\n *\n * 1. the org's `pluginSettings/ai` — a provider, and a model per step kind\n * that must be one the provider serves;\n * 2. the deployment's environment — `AI_PROVIDER` for the provider,\n * `AI_DEFAULT_MODEL` for every step, and `ASSIST_MODEL` for the\n * assistant alone (the incident-response override the chat door has\n * always honored);\n * 3. the catalog's tier for the step kind on the resolved provider.\n */\n\n/** The environment override for every step kind. */\nexport const AI_DEFAULT_MODEL_ENV = 'AI_DEFAULT_MODEL'\n/** The assistant's own override, honored before the general one. */\nexport const ASSIST_MODEL_ENV = 'ASSIST_MODEL'\n\n/** The org's resolved `pluginSettings/ai` values, as `getPluginConfig` merges them. */\nexport type AiPluginSettings = Record<string, unknown>\n\n/**\n * The model that serves a catalog id (AGL-3781): the id itself, or for a\n * retired row the successor it names, followed to a current row. An id the\n * catalog does not know is returned as it is, for the caller to refuse.\n */\nexport function aiServingModelId(modelId: string): string {\n let current = modelId\n for (let hops = 0; hops < AI_MODEL_CATALOG.length; hops += 1) {\n const successor = aiCatalogEntry(current)?.successor\n if (!successor) return current\n current = successor\n }\n return current\n}\n\nexport interface AiRoute {\n provider: AiProvider\n model: string\n}\n\n/** A settings value that names a real choice, or `undefined` for the platform default. */\nfunction chosen(settings: AiPluginSettings | undefined, key: string): string | undefined {\n const value = settings?.[key]\n return typeof value === 'string' && value && value !== AI_SETTING_PLATFORM\n ? value\n : undefined\n}\n\n/** The provider a workspace runs on: its own choice when registered, else the platform's. */\nexport function resolveAiProvider(settings?: AiPluginSettings): AiProvider | undefined {\n const wanted = chosen(settings, 'provider')\n if (wanted) {\n const named = aiProviderById(wanted)\n if (named) return named\n }\n return defaultAiProvider()\n}\n\n/**\n * The model a step kind runs on for a workspace, on the resolved provider.\n * A chosen model on another provider is not served — the provider decides\n * what it can run — and falls back to the provider's default for the tier.\n */\nexport function resolveAiRoute(\n kind: AiStepKind,\n settings?: AiPluginSettings,\n): AiRoute | undefined {\n const provider = resolveAiProvider(settings)\n if (!provider) return undefined\n // A setting or an override that names a retired model is served by its\n // successor, never by the retired model itself.\n const servedBy = (modelId: string | undefined): string | undefined => {\n const serving = modelId ? aiServingModelId(modelId) : undefined\n return serving && aiCatalogEntry(serving)?.provider === provider.id ? serving : undefined\n }\n const model =\n servedBy(chosen(settings, AI_STEP_MODEL_SETTING[kind])) ??\n (kind === 'assist.chat' ? servedBy(process.env[ASSIST_MODEL_ENV]?.trim()) : undefined) ??\n servedBy(process.env[AI_DEFAULT_MODEL_ENV]?.trim()) ??\n aiDefaultModelFor(provider.id, AI_STEP_TIERS[kind])\n return model ? { provider, model } : undefined\n}\n\n/**\n * What one step kind asks of its model, beside the tier it is served from\n * (`AI_STEP_TIERS` in the catalog): whether it thinks and how hard, its\n * answer ceiling, and the eval score that tier holds on the step's golden\n * briefs (AGL-2937).\n */\nexport interface AiRoutingRow {\n /**\n * `null` sends no setting: the model's own default, which on every current\n * model is adaptive thinking. A row whose ceiling holds the answer alone\n * says `off` (AGL-3781).\n */\n thinking: AiThinking | null\n effort: AiEffort | null\n /** The answer ceiling, thinking included. */\n maxTokens: number\n /** What the ceiling is sized from. */\n maxTokensBasis: string\n eval: AiRoutingEval\n /**\n * How long the step's prompt cache lives (AGL-3830): `1h` on every step a\n * site job runs, in production as in a live run, and the provider's five\n * minutes on a row that leaves it out. A guided start's routes each write\n * their prefix once and read it only inside the job, a beat or more apart,\n * so a five-minute entry is gone before the next job could read it; at an\n * hour the next site job reads what the last one wrote. The write costs 2x\n * the input price instead of 1.25x, so a step no job runs leaves it out.\n * Read by `runValidatedGeneration` into the request's `cacheTtl`; a door\n * that sends its own request on the same step (the copy assistant's blog\n * draft) keeps five minutes.\n */\n cacheTtl?: '1h'\n}\n\n/**\n * The cache lifetime every step a site job runs asks for (AGL-3830): the\n * plan, the look, the header and footer, every page and its listing, the\n * creations, the first posts and products, and the welcome email.\n */\nconst AI_SITE_JOB_CACHE_TTL = '1h' as const\n\nexport interface AiRoutingEval {\n /** The golden-brief kinds the step answers. */\n kinds: readonly AiEvalKind[]\n /** `answers` scores the step's answers; `plans` scores the plans those kinds' answers carry. */\n scores: 'answers' | 'plans'\n /** `authored` while the score is the hand-written references'; `recorded` once a live run records answers. */\n source: 'authored' | 'recorded'\n passRate: number\n /** The mean answer score; `null` for a plan score, which passes or fails. */\n meanScore: number | null\n}\n\n/**\n * THE ROUTING TABLE (AGL-2937): what each step kind asks of the model, in one\n * place, with the evidence beside it.\n *\n * A step runs on the smallest tier that holds its eval floor. A row moves to a\n * cheaper tier, or a lower effort, only when a recorded eval\n * (`npm run eval:ai-live`) holds the floor there; while a row's score is the\n * authored references', it says so, and the row stays where production has\n * run it. `runtime/ai-eval.spec.ts` recomputes every row's score from the\n * harness, so a row cannot claim a score its briefs do not hold.\n *\n * A ceiling is sized from the p95 output the staff signals page reports for\n * the kind once production has served it; until then `maxTokensBasis` names\n * what it is sized from. The same spec holds every reference answer under its\n * row's ceiling at three characters a token, a reading that errs high for a\n * tokenizer that splits finer than four.\n */\nexport const AI_ROUTING_TABLE: Readonly<Record<AiStepKind, AiRoutingRow>> = {\n 'assist.chat': {\n // A scoped, latency-sensitive chat turn: no thinking, the low effort rung.\n thinking: 'off',\n effort: 'low',\n maxTokens: 1024,\n maxTokensBasis:\n 'a docs-grounded answer of a few short paragraphs; the edit rung raises it for its tool call',\n eval: { kinds: ['chat'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 0.9688 },\n },\n 'copy.element': {\n // One element's copy on the fast tier, whose model thinks unless told not\n // to (AGL-3781): the ceiling holds the copy alone, as it always has, and\n // the effort is the model's own.\n thinking: 'off',\n effort: null,\n maxTokens: 1024,\n maxTokensBasis: 'one element of copy, about the length of the text it replaces',\n eval: { kinds: ['element'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 0.9688 },\n },\n 'copy.section': {\n thinking: null,\n effort: null,\n maxTokens: 3000,\n maxTokensBasis: 'a section of four to twelve nodes written as JSON',\n eval: { kinds: ['section'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 0.9688 },\n },\n 'copy.blog': {\n thinking: null,\n effort: null,\n maxTokens: 2048,\n maxTokensBasis: 'one blog post body in markdown-lite',\n eval: { kinds: ['blog'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 0.9688 },\n cacheTtl: AI_SITE_JOB_CACHE_TTL,\n },\n 'generate.section': {\n thinking: null,\n effort: null,\n maxTokens: 3000,\n maxTokensBasis: 'a section of four to twelve nodes written as JSON',\n eval: { kinds: ['section'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 0.9688 },\n },\n 'job.layout': {\n // The plan was reasoned out and confirmed before the step, so it writes without thinking.\n // A site's header and footer in the layout language think adaptively, at\n // low since AGL-3781 (`AI_LAYOUT_LANGUAGE_THINKING`): the live round of\n // 2026-10-14 built 13 of 13 frames at low, none cut off.\n thinking: 'off',\n effort: null,\n maxTokens: 8000,\n maxTokensBasis:\n \"tighter than the doctrine's default for a layout, so one step and its re-ask fit the job beat's budget; the tree is held to its rule 17 budget either way\",\n eval: { kinds: ['layout'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 0.9833 },\n cacheTtl: AI_SITE_JOB_CACHE_TTL,\n },\n 'job.template': {\n // Medium since the golden A/B of 2026-10-14 (AGL-3781): both template\n // briefs passed at medium as at the default, with about a third fewer\n // output tokens.\n thinking: 'off',\n effort: 'medium',\n maxTokens: 8000,\n maxTokensBasis:\n \"tighter than the doctrine's default for a template, so one step and its re-ask fit the job beat's budget; the tree is held to its rule 17 budget either way\",\n eval: { kinds: ['template'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 0.9833 },\n cacheTtl: AI_SITE_JOB_CACHE_TTL,\n },\n 'job.component': {\n // A component's tree and the properties it declares, in one answer, from a\n // plan reasoned out and confirmed before the step (AGL-2908).\n thinking: 'off',\n effort: null,\n maxTokens: 8000,\n maxTokensBasis:\n \"the ceiling the layout and template steps keep, so one step and its re-ask fit the job beat's budget; the tree is held to its rule 17 budget either way\",\n eval: { kinds: ['component'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 0.9833 },\n cacheTtl: AI_SITE_JOB_CACHE_TTL,\n },\n 'job.crm': {\n // CRM by AI (AGL-2917): short answers through a strict tool, written from\n // the facts the CRM reports about the record the member opened, with no\n // thinking. The ceiling fits an inline door, so a member is answered in\n // the request that asked, and has no room for thinking: the fast tier's\n // model thinks unless told not to, so the row says off, and its effort\n // is the low one a short answer read off given facts needs (AGL-3781).\n thinking: 'off',\n effort: 'low',\n maxTokens: 700,\n maxTokensBasis:\n \"the largest answer a CRM tool accepts: sixty columns matched to fields by number, or an email draft at its length limits, at three characters a token with room, as ai-job-crm-step.spec.ts measures it\",\n eval: { kinds: ['crm'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 0.9938 },\n },\n 'job.experiment': {\n // Variants of one piece of copy, or a result explained against a verdict\n // code reached before the request was made (AGL-2914). The judgment is\n // already made either way, so it writes without thinking.\n thinking: 'off',\n effort: null,\n maxTokens: 1500,\n maxTokensBasis:\n \"four variants at the tool's own lengths — a name, a headline, a body and a rationale each — written as JSON, about 3,000 characters at three characters a token with room; an explanation is a headline, four sentences and a next step, well under it, as ai-job-experiment-step.spec.ts measures both\",\n eval: { kinds: ['experiment'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 1 },\n },\n 'job.form': {\n // A form's two halves in one answer: the field declaration and the design\n // that renders it, as JSON, with no extended thinking (AGL-2913). The step\n // asks this ceiling, which is the doctrine's own for a form, and registers\n // the time it takes (AGL-3035).\n thinking: 'off',\n effort: null,\n maxTokens: 6000,\n maxTokensBasis:\n \"the largest form the doctrine's output budget admits, a form's fields, its consent and routing declaration and the design that renders them written as JSON at the wordiest golden's characters a stored byte, under it with room; ai-job-form-step.spec.ts measures it\",\n eval: { kinds: ['form'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 0.9833 },\n cacheTtl: AI_SITE_JOB_CACHE_TTL,\n },\n 'job.insight': {\n // Readers chosen, then their tables phrased (AGL-2915): short sentences\n // over figures code computed, each held to the rows it cites, so no\n // extended thinking. The read call before the answer asks for at most\n // `AI_INSIGHT_READ_MAX_TOKENS` of the same allowance.\n thinking: 'off',\n effort: null,\n maxTokens: 1500,\n maxTokensBasis:\n 'five insights at their 280-character bound, each citing three tables of ten rows, and the gap sentence, written as JSON: about 2,700 characters, under 1,000 tokens at three characters a token, with room',\n eval: { kinds: ['insight'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 0.9688 },\n },\n 'job.page': {\n // One section of a page, written against a plan a member already\n // confirmed, so it writes without thinking (AGL-2907). The ceiling here\n // is the MOST a pass may ask for; each pass sizes its own down from it to\n // what fits the time the job beat can give the step, which on the\n // balanced tier is about half of this. A page in the layout language\n // thinks adaptively, at low since AGL-3781 (`AI_LAYOUT_LANGUAGE_THINKING`):\n // the live round of 2026-10-14 built 39 of 39 pages at low, 64 of 65\n // answers held their rules on the first ask, none cut off.\n thinking: 'off',\n effort: null,\n maxTokens: 2000,\n maxTokensBasis:\n \"a section of a page written as JSON, about forty elements at the step's measured 45 tokens an element; a pass lowers it to the worst case that fits AI_JOB_PAGE_STEP_MINIMUM_MS on the model it runs\",\n eval: { kinds: ['page'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 0.9833 },\n cacheTtl: AI_SITE_JOB_CACHE_TTL,\n },\n 'job.products': {\n // A product's copy from its name, text and photo, or a store's first\n // products, categories and discounts from a brief (AGL-2916): answers held\n // to the storefront claim rules, written with no extended thinking.\n thinking: 'off',\n effort: null,\n maxTokens: 8000,\n maxTokensBasis:\n \"the largest catalog the tool accepts, twelve proposed products at every bound written as JSON, which the balanced tier asks as much of as fits a beat; one product's copy asks 1,500 and categories with discounts 2,000, as ai-job-products-step.spec.ts measures them\",\n eval: {\n kinds: ['product', 'catalog', 'categories'],\n scores: 'answers',\n source: 'authored',\n passRate: 1,\n meanScore: 0.9922,\n },\n cacheTtl: AI_SITE_JOB_CACHE_TTL,\n },\n 'job.email': {\n // One email design from the email palette, written against the brief with\n // no extended thinking (AGL-2912). A campaign job runs the same\n // generation, so it carries the same ceiling.\n thinking: 'off',\n effort: null,\n maxTokens: 6000,\n maxTokensBasis:\n \"an email's node map written as JSON, with three subject lines and three preheaders beside it\",\n eval: { kinds: ['email'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 0.9688 },\n cacheTtl: AI_SITE_JOB_CACHE_TTL,\n },\n 'job.campaign': {\n // The campaign job generates its email through the email step, so it is\n // routed identically; the draft campaign it also writes asks nothing of\n // the model (AGL-2912).\n thinking: 'off',\n effort: null,\n maxTokens: 6000,\n maxTokensBasis:\n \"an email's node map written as JSON, with three subject lines and three preheaders beside it\",\n eval: { kinds: ['email'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 0.9688 },\n },\n 'job.workflow': {\n // An automation drafted from a description, or one explained (AGL-2919):\n // which trigger and which steps a description means is the judgment the\n // step sells, so it thinks before it answers.\n thinking: 'adaptive',\n effort: null,\n maxTokens: 4000,\n maxTokensBasis:\n 'the largest automation the tool accepts, 6,000 characters written out as JSON, at three characters a token with as much again to think in, as ai-job-workflow-step.spec.ts measures it',\n eval: { kinds: ['workflow'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 0.9688 },\n },\n 'job.logic': {\n // A site function or variable proposed, or a function explained\n // (AGL-3603): which conditions and arithmetic a description means is the\n // judgment the step sells, so it thinks before it answers.\n thinking: 'adaptive',\n effort: null,\n maxTokens: 3000,\n maxTokensBasis:\n 'the largest function the tool accepts — twelve parameters, twenty-four operations — is far past any description; a function of eight operations is about 2,400 characters of JSON, at three characters a token with as much again to think in, as ai-job-logic-step.spec.ts measures it',\n eval: { kinds: ['logic'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 1 },\n },\n 'job.edit': {\n // A change to a page or a layout the site has (AGL-3616): which elements\n // a request means, on an outline with no selection, is the judgment the\n // step sells, so it thinks before it answers.\n thinking: 'adaptive',\n effort: null,\n maxTokens: 6144,\n maxTokensBasis:\n \"the Assist edit rung's own ceiling for the proposal's tool call (ASSIST_EDIT_MAX_OUTPUT_TOKENS, 4,096: forty new elements written as name/value pairs), with half as much again to think in, as ai-job-edit-step.spec.ts holds it; the whole of it twice, as a re-ask may, still fits one beat on the balanced tier\",\n eval: { kinds: ['edit'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 1 },\n },\n 'job.seo': {\n // A listing written from the page's own text to fixed lengths, sized for\n // the answer alone: no thinking, which the fast tier's model does unless\n // told not to, at the low effort a short bounded answer needs (AGL-3781).\n // The audit's site proposal and fix batches run on the same row.\n thinking: 'off',\n effort: 'low',\n maxTokens: 1024,\n maxTokensBasis:\n \"the largest listing the tool accepts, at three characters a token with a quarter again, as ai-job-seo-step.spec.ts measures it; an audit's site proposal and fix batches keep ceilings of their own, measured the same way\",\n eval: { kinds: ['seo'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 1 },\n cacheTtl: AI_SITE_JOB_CACHE_TTL,\n },\n 'job.text': {\n // Low since the golden A/B of 2026-10-14 (AGL-3781): the tagline brief\n // passed at low, its copy scored higher than at the default.\n thinking: 'adaptive',\n effort: 'low',\n maxTokens: 1024,\n maxTokensBasis: 'a few paragraphs of copy; a brief asking for more gets a draft to extend',\n eval: { kinds: ['text'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 1 },\n },\n 'job.theme': {\n // Medium since the golden A/B of 2026-10-14 (AGL-3781): the theme brief\n // passed at medium with the default's scores and half its output tokens.\n // A site start's look runs its own setting (`ai-job-site-look.ts`).\n thinking: 'adaptive',\n effort: 'medium',\n maxTokens: 8000,\n maxTokensBasis:\n 'the largest answer the theme tool accepts, about 2,500 tokens of JSON as ai-job-theme-step.spec.ts measures it, with as much again to think in',\n eval: { kinds: ['theme'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 0.9688 },\n cacheTtl: AI_SITE_JOB_CACHE_TTL,\n },\n 'job.plan': {\n // A site's plan is an outline written without thinking (AGL-3594), and a\n // paid site's asks medium (`AI_PAID_SITE_PLAN_EFFORT`, AGL-3781): the live\n // round of 2026-10-14 planned 11 of 11 paid site briefs at medium in 16\n // attempts to the default's 22, $0.129 to $0.164, with no refusal and no\n // rule broken. Every other plan, and a Free site's on the fast tier, asks\n // this row's effort — the model's default — until a round holds it lower.\n thinking: 'adaptive',\n effort: null,\n maxTokens: 8000,\n maxTokensBasis: 'a plan of up to twelve screens with sixteen sections each, with room to think',\n eval: {\n kinds: ['page', 'template', 'component', 'layout', 'form'],\n scores: 'plans',\n source: 'authored',\n passRate: 1,\n meanScore: null,\n },\n cacheTtl: AI_SITE_JOB_CACHE_TTL,\n },\n}\n\n/**\n * What a step kind's row asks of its model, as the fields a request carries:\n * only those the row names, so a row that names none sends none and the\n * model's own default stands.\n */\nexport function aiRouteSettings(kind: AiStepKind): { thinking?: AiThinking; effort?: AiEffort } {\n const row = AI_ROUTING_TABLE[kind]\n return {\n ...(row.thinking ? { thinking: row.thinking } : {}),\n ...(row.effort ? { effort: row.effort } : {}),\n }\n}\n\n/** The model id alone — what a door records on the meter and the job. */\nexport function aiModelForStep(kind: AiStepKind, settings?: AiPluginSettings): string {\n const route = resolveAiRoute(kind, settings)\n if (!route) {\n // Every door gates on the provider being configured before it asks\n // for a model, so this is a wiring fault rather than a customer path.\n throw new Error('no AI provider is registered')\n }\n return route.model\n}\n"],"names":["AI_SETTING_PLATFORM","AI_STEP_MODEL_SETTING","AI_MODEL_CATALOG","AI_STEP_TIERS","aiCatalogEntry","aiDefaultModelFor","aiProviderById","defaultAiProvider","AI_DEFAULT_MODEL_ENV","ASSIST_MODEL_ENV","aiServingModelId","modelId","current","hops","length","successor","chosen","settings","key","value","undefined","resolveAiProvider","wanted","named","resolveAiRoute","kind","servedBy","process","provider","serving","id","model","env","trim","AI_SITE_JOB_CACHE_TTL","AI_ROUTING_TABLE","thinking","effort","maxTokens","maxTokensBasis","eval","kinds","scores","source","passRate","meanScore","cacheTtl","aiRouteSettings","row","aiModelForStep","route","Error"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/providers/routing.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 { AI_SETTING_PLATFORM, AI_STEP_MODEL_SETTING } from '../plugin-config'\nimport {\n AI_MODEL_CATALOG,\n AI_STEP_TIERS,\n aiCatalogEntry,\n aiDefaultModelFor,\n type AiStepKind,\n} from './catalog'\nimport type { AiEvalKind } from '../runtime/ai-eval'\nimport type { AiEffort, AiProvider, AiThinking } from './contract'\nimport { aiProviderById, defaultAiProvider } from './registry'\n\n/**\n * The routing table (AGL-2937, AGL-2939): step kind → provider and model,\n * through the catalog and never a vendor literal.\n *\n * Precedence, dearest override first:\n *\n * 1. the org's `pluginSettings/ai` — a provider, and a model per step kind\n * that must be one the provider serves;\n * 2. the deployment's environment — `AI_PROVIDER` for the provider,\n * `AI_DEFAULT_MODEL` for every step, and `ASSIST_MODEL` for the\n * assistant alone (the incident-response override the chat door has\n * always honored);\n * 3. the catalog's tier for the step kind on the resolved provider.\n */\n\n/** The environment override for every step kind. */\nexport const AI_DEFAULT_MODEL_ENV = 'AI_DEFAULT_MODEL'\n/** The assistant's own override, honored before the general one. */\nexport const ASSIST_MODEL_ENV = 'ASSIST_MODEL'\n\n/** The org's resolved `pluginSettings/ai` values, as `getPluginConfig` merges them. */\nexport type AiPluginSettings = Record<string, unknown>\n\n/**\n * The model that serves a catalog id (AGL-3781): the id itself, or for a\n * retired row the successor it names, followed to a current row. An id the\n * catalog does not know is returned as it is, for the caller to refuse.\n */\nexport function aiServingModelId(modelId: string): string {\n let current = modelId\n for (let hops = 0; hops < AI_MODEL_CATALOG.length; hops += 1) {\n const successor = aiCatalogEntry(current)?.successor\n if (!successor) return current\n current = successor\n }\n return current\n}\n\nexport interface AiRoute {\n provider: AiProvider\n model: string\n}\n\n/** A settings value that names a real choice, or `undefined` for the platform default. */\nfunction chosen(settings: AiPluginSettings | undefined, key: string): string | undefined {\n const value = settings?.[key]\n return typeof value === 'string' && value && value !== AI_SETTING_PLATFORM\n ? value\n : undefined\n}\n\n/** The provider a workspace runs on: its own choice when registered, else the platform's. */\nexport function resolveAiProvider(settings?: AiPluginSettings): AiProvider | undefined {\n const wanted = chosen(settings, 'provider')\n if (wanted) {\n const named = aiProviderById(wanted)\n if (named) return named\n }\n return defaultAiProvider()\n}\n\n/**\n * The model a step kind runs on for a workspace, on the resolved provider.\n * A chosen model on another provider is not served — the provider decides\n * what it can run — and falls back to the provider's default for the tier.\n */\nexport function resolveAiRoute(\n kind: AiStepKind,\n settings?: AiPluginSettings,\n): AiRoute | undefined {\n const provider = resolveAiProvider(settings)\n if (!provider) return undefined\n // A setting or an override that names a retired model is served by its\n // successor, never by the retired model itself.\n const servedBy = (modelId: string | undefined): string | undefined => {\n const serving = modelId ? aiServingModelId(modelId) : undefined\n return serving && aiCatalogEntry(serving)?.provider === provider.id ? serving : undefined\n }\n const model =\n servedBy(chosen(settings, AI_STEP_MODEL_SETTING[kind])) ??\n (kind === 'assist.chat' ? servedBy(process.env[ASSIST_MODEL_ENV]?.trim()) : undefined) ??\n servedBy(process.env[AI_DEFAULT_MODEL_ENV]?.trim()) ??\n aiDefaultModelFor(provider.id, AI_STEP_TIERS[kind])\n return model ? { provider, model } : undefined\n}\n\n/**\n * What one step kind asks of its model, beside the tier it is served from\n * (`AI_STEP_TIERS` in the catalog): whether it thinks and how hard, its\n * answer ceiling, and the eval score that tier holds on the step's golden\n * briefs (AGL-2937).\n */\nexport interface AiRoutingRow {\n /**\n * `null` sends no setting: the model's own default, which on every current\n * model is adaptive thinking. A row whose ceiling holds the answer alone\n * says `off` (AGL-3781).\n */\n thinking: AiThinking | null\n effort: AiEffort | null\n /** The answer ceiling, thinking included. */\n maxTokens: number\n /** What the ceiling is sized from. */\n maxTokensBasis: string\n eval: AiRoutingEval\n /**\n * How long the step's prompt cache lives (AGL-3830): `1h` on every step a\n * site job runs, in production as in a live run, and the provider's five\n * minutes on a row that leaves it out. A guided start's routes each write\n * their prefix once and read it only inside the job, a beat or more apart,\n * so a five-minute entry is gone before the next job could read it; at an\n * hour the next site job reads what the last one wrote. The write costs 2x\n * the input price instead of 1.25x, so a step no job runs leaves it out.\n * The hour is the SHARED markers' alone (AGL-4050): a site's block and the\n * plan's own conversation are read by one job only, so they keep five\n * minutes on every row (`aiCacheMarkerTtls` in `providers/contract.ts`).\n * Read by `runValidatedGeneration` into the request's `cacheTtl`; a door\n * that sends its own request on the same step (the copy assistant's blog\n * draft) keeps five minutes.\n */\n cacheTtl?: '1h'\n}\n\n/**\n * The cache lifetime every step a site job runs asks for its shared prompt\n * (AGL-3830, AGL-4050): the plan, the look, the header and footer, every page\n * and its listing, the creations, the first posts and products, and the\n * welcome email.\n */\nconst AI_SITE_JOB_CACHE_TTL = '1h' as const\n\nexport interface AiRoutingEval {\n /** The golden-brief kinds the step answers. */\n kinds: readonly AiEvalKind[]\n /** `answers` scores the step's answers; `plans` scores the plans those kinds' answers carry. */\n scores: 'answers' | 'plans'\n /** `authored` while the score is the hand-written references'; `recorded` once a live run records answers. */\n source: 'authored' | 'recorded'\n passRate: number\n /** The mean answer score; `null` for a plan score, which passes or fails. */\n meanScore: number | null\n}\n\n/**\n * THE ROUTING TABLE (AGL-2937): what each step kind asks of the model, in one\n * place, with the evidence beside it.\n *\n * A step runs on the smallest tier that holds its eval floor. A row moves to a\n * cheaper tier, or a lower effort, only when a recorded eval\n * (`npm run eval:ai-live`) holds the floor there; while a row's score is the\n * authored references', it says so, and the row stays where production has\n * run it. `runtime/ai-eval.spec.ts` recomputes every row's score from the\n * harness, so a row cannot claim a score its briefs do not hold.\n *\n * A ceiling is sized from the p95 output the staff signals page reports for\n * the kind once production has served it; until then `maxTokensBasis` names\n * what it is sized from. The same spec holds every reference answer under its\n * row's ceiling at three characters a token, a reading that errs high for a\n * tokenizer that splits finer than four.\n */\nexport const AI_ROUTING_TABLE: Readonly<Record<AiStepKind, AiRoutingRow>> = {\n 'assist.chat': {\n // A scoped, latency-sensitive chat turn: no thinking, the low effort rung.\n thinking: 'off',\n effort: 'low',\n maxTokens: 1024,\n maxTokensBasis:\n 'a docs-grounded answer of a few short paragraphs; the edit rung raises it for its tool call',\n eval: { kinds: ['chat'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 0.9688 },\n },\n 'copy.element': {\n // One element's copy on the fast tier, whose model thinks unless told not\n // to (AGL-3781): the ceiling holds the copy alone, as it always has, and\n // the effort is the model's own.\n thinking: 'off',\n effort: null,\n maxTokens: 1024,\n maxTokensBasis: 'one element of copy, about the length of the text it replaces',\n eval: { kinds: ['element'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 0.9688 },\n },\n 'copy.section': {\n thinking: null,\n effort: null,\n maxTokens: 3000,\n maxTokensBasis: 'a section of four to twelve nodes written as JSON',\n eval: { kinds: ['section'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 0.9688 },\n },\n 'copy.blog': {\n thinking: null,\n effort: null,\n maxTokens: 2048,\n maxTokensBasis: 'one blog post body in markdown-lite',\n eval: { kinds: ['blog'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 0.9688 },\n cacheTtl: AI_SITE_JOB_CACHE_TTL,\n },\n 'generate.section': {\n thinking: null,\n effort: null,\n maxTokens: 3000,\n maxTokensBasis: 'a section of four to twelve nodes written as JSON',\n eval: { kinds: ['section'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 0.9688 },\n },\n 'job.layout': {\n // The plan was reasoned out and confirmed before the step, so it writes without thinking.\n // A site's header and footer in the layout language think adaptively, at\n // low since AGL-3781 (`AI_LAYOUT_LANGUAGE_THINKING`): the live round of\n // 2026-10-14 built 13 of 13 frames at low, none cut off.\n thinking: 'off',\n effort: null,\n maxTokens: 8000,\n maxTokensBasis:\n \"tighter than the doctrine's default for a layout, so one step and its re-ask fit the job beat's budget; the tree is held to its rule 17 budget either way\",\n eval: { kinds: ['layout'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 0.9833 },\n cacheTtl: AI_SITE_JOB_CACHE_TTL,\n },\n 'job.template': {\n // Medium since the golden A/B of 2026-10-14 (AGL-3781): both template\n // briefs passed at medium as at the default, with about a third fewer\n // output tokens.\n thinking: 'off',\n effort: 'medium',\n maxTokens: 8000,\n maxTokensBasis:\n \"tighter than the doctrine's default for a template, so one step and its re-ask fit the job beat's budget; the tree is held to its rule 17 budget either way\",\n eval: { kinds: ['template'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 0.9833 },\n cacheTtl: AI_SITE_JOB_CACHE_TTL,\n },\n 'job.component': {\n // A component's tree and the properties it declares, in one answer, from a\n // plan reasoned out and confirmed before the step (AGL-2908).\n thinking: 'off',\n effort: null,\n maxTokens: 8000,\n maxTokensBasis:\n \"the ceiling the layout and template steps keep, so one step and its re-ask fit the job beat's budget; the tree is held to its rule 17 budget either way\",\n eval: { kinds: ['component'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 0.9833 },\n cacheTtl: AI_SITE_JOB_CACHE_TTL,\n },\n 'job.crm': {\n // CRM by AI (AGL-2917): short answers through a strict tool, written from\n // the facts the CRM reports about the record the member opened, with no\n // thinking. The ceiling fits an inline door, so a member is answered in\n // the request that asked, and has no room for thinking: the fast tier's\n // model thinks unless told not to, so the row says off, and its effort\n // is the low one a short answer read off given facts needs (AGL-3781).\n thinking: 'off',\n effort: 'low',\n maxTokens: 700,\n maxTokensBasis:\n \"the largest answer a CRM tool accepts: sixty columns matched to fields by number, or an email draft at its length limits, at three characters a token with room, as ai-job-crm-step.spec.ts measures it\",\n eval: { kinds: ['crm'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 0.9938 },\n },\n 'job.experiment': {\n // Variants of one piece of copy, or a result explained against a verdict\n // code reached before the request was made (AGL-2914). The judgment is\n // already made either way, so it writes without thinking.\n thinking: 'off',\n effort: null,\n maxTokens: 1500,\n maxTokensBasis:\n \"four variants at the tool's own lengths — a name, a headline, a body and a rationale each — written as JSON, about 3,000 characters at three characters a token with room; an explanation is a headline, four sentences and a next step, well under it, as ai-job-experiment-step.spec.ts measures both\",\n eval: { kinds: ['experiment'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 1 },\n },\n 'job.form': {\n // A form's two halves in one answer: the field declaration and the design\n // that renders it, as JSON, with no extended thinking (AGL-2913). The step\n // asks this ceiling, which is the doctrine's own for a form, and registers\n // the time it takes (AGL-3035).\n thinking: 'off',\n effort: null,\n maxTokens: 6000,\n maxTokensBasis:\n \"the largest form the doctrine's output budget admits, a form's fields, its consent and routing declaration and the design that renders them written as JSON at the wordiest golden's characters a stored byte, under it with room; ai-job-form-step.spec.ts measures it\",\n eval: { kinds: ['form'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 0.9833 },\n cacheTtl: AI_SITE_JOB_CACHE_TTL,\n },\n 'job.insight': {\n // Readers chosen, then their tables phrased (AGL-2915): short sentences\n // over figures code computed, each held to the rows it cites, so no\n // extended thinking. The read call before the answer asks for at most\n // `AI_INSIGHT_READ_MAX_TOKENS` of the same allowance.\n thinking: 'off',\n effort: null,\n maxTokens: 1500,\n maxTokensBasis:\n 'five insights at their 280-character bound, each citing three tables of ten rows, and the gap sentence, written as JSON: about 2,700 characters, under 1,000 tokens at three characters a token, with room',\n eval: { kinds: ['insight'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 0.9688 },\n },\n 'job.page': {\n // One section of a page, written against a plan a member already\n // confirmed, so it writes without thinking (AGL-2907). The ceiling here\n // is the MOST a pass may ask for; each pass sizes its own down from it to\n // what fits the time the job beat can give the step, which on the\n // balanced tier is about half of this. A page in the layout language\n // thinks adaptively, at low since AGL-3781 (`AI_LAYOUT_LANGUAGE_THINKING`):\n // the live round of 2026-10-14 built 39 of 39 pages at low, 64 of 65\n // answers held their rules on the first ask, none cut off.\n thinking: 'off',\n effort: null,\n maxTokens: 2000,\n maxTokensBasis:\n \"a section of a page written as JSON, about forty elements at the step's measured 45 tokens an element; a pass lowers it to the worst case that fits AI_JOB_PAGE_STEP_MINIMUM_MS on the model it runs\",\n eval: { kinds: ['page'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 0.9833 },\n cacheTtl: AI_SITE_JOB_CACHE_TTL,\n },\n 'job.products': {\n // A product's copy from its name, text and photo, or a store's first\n // products, categories and discounts from a brief (AGL-2916): answers held\n // to the storefront claim rules, written with no extended thinking.\n thinking: 'off',\n effort: null,\n maxTokens: 8000,\n maxTokensBasis:\n \"the largest catalog the tool accepts, twelve proposed products at every bound written as JSON, which the balanced tier asks as much of as fits a beat; one product's copy asks 1,500 and categories with discounts 2,000, as ai-job-products-step.spec.ts measures them\",\n eval: {\n kinds: ['product', 'catalog', 'categories'],\n scores: 'answers',\n source: 'authored',\n passRate: 1,\n meanScore: 0.9922,\n },\n cacheTtl: AI_SITE_JOB_CACHE_TTL,\n },\n 'job.email': {\n // One email design from the email palette, written against the brief with\n // no extended thinking (AGL-2912). A campaign job runs the same\n // generation, so it carries the same ceiling.\n thinking: 'off',\n effort: null,\n maxTokens: 6000,\n maxTokensBasis:\n \"an email's node map written as JSON, with three subject lines and three preheaders beside it\",\n eval: { kinds: ['email'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 0.9688 },\n cacheTtl: AI_SITE_JOB_CACHE_TTL,\n },\n 'job.campaign': {\n // The campaign job generates its email through the email step, so it is\n // routed identically; the draft campaign it also writes asks nothing of\n // the model (AGL-2912).\n thinking: 'off',\n effort: null,\n maxTokens: 6000,\n maxTokensBasis:\n \"an email's node map written as JSON, with three subject lines and three preheaders beside it\",\n eval: { kinds: ['email'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 0.9688 },\n },\n 'job.workflow': {\n // An automation drafted from a description, or one explained (AGL-2919):\n // which trigger and which steps a description means is the judgment the\n // step sells, so it thinks before it answers.\n thinking: 'adaptive',\n effort: null,\n maxTokens: 4000,\n maxTokensBasis:\n 'the largest automation the tool accepts, 6,000 characters written out as JSON, at three characters a token with as much again to think in, as ai-job-workflow-step.spec.ts measures it',\n eval: { kinds: ['workflow'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 0.9688 },\n },\n 'job.logic': {\n // A site function or variable proposed, or a function explained\n // (AGL-3603): which conditions and arithmetic a description means is the\n // judgment the step sells, so it thinks before it answers.\n thinking: 'adaptive',\n effort: null,\n maxTokens: 3000,\n maxTokensBasis:\n 'the largest function the tool accepts — twelve parameters, twenty-four operations — is far past any description; a function of eight operations is about 2,400 characters of JSON, at three characters a token with as much again to think in, as ai-job-logic-step.spec.ts measures it',\n eval: { kinds: ['logic'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 1 },\n },\n 'job.edit': {\n // A change to a page or a layout the site has (AGL-3616): which elements\n // a request means, on an outline with no selection, is the judgment the\n // step sells, so it thinks before it answers.\n thinking: 'adaptive',\n effort: null,\n maxTokens: 6144,\n maxTokensBasis:\n \"the Assist edit rung's own ceiling for the proposal's tool call (ASSIST_EDIT_MAX_OUTPUT_TOKENS, 4,096: forty new elements written as name/value pairs), with half as much again to think in, as ai-job-edit-step.spec.ts holds it; the whole of it twice, as a re-ask may, still fits one beat on the balanced tier\",\n eval: { kinds: ['edit'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 1 },\n },\n 'job.seo': {\n // A listing written from the page's own text to fixed lengths, sized for\n // the answer alone: no thinking, which the fast tier's model does unless\n // told not to, at the low effort a short bounded answer needs (AGL-3781).\n // The audit's site proposal and fix batches run on the same row.\n thinking: 'off',\n effort: 'low',\n maxTokens: 1024,\n maxTokensBasis:\n \"the largest listing the tool accepts, at three characters a token with a quarter again, as ai-job-seo-step.spec.ts measures it; an audit's site proposal and fix batches keep ceilings of their own, measured the same way\",\n eval: { kinds: ['seo'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 1 },\n cacheTtl: AI_SITE_JOB_CACHE_TTL,\n },\n 'job.text': {\n // Low since the golden A/B of 2026-10-14 (AGL-3781): the tagline brief\n // passed at low, its copy scored higher than at the default.\n thinking: 'adaptive',\n effort: 'low',\n maxTokens: 1024,\n maxTokensBasis: 'a few paragraphs of copy; a brief asking for more gets a draft to extend',\n eval: { kinds: ['text'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 1 },\n },\n 'job.theme': {\n // Medium since the golden A/B of 2026-10-14 (AGL-3781): the theme brief\n // passed at medium with the default's scores and half its output tokens.\n // A site start's look runs its own setting (`ai-job-site-look.ts`).\n thinking: 'adaptive',\n effort: 'medium',\n maxTokens: 8000,\n maxTokensBasis:\n 'the largest answer the theme tool accepts, about 2,500 tokens of JSON as ai-job-theme-step.spec.ts measures it, with as much again to think in',\n eval: { kinds: ['theme'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 0.9688 },\n cacheTtl: AI_SITE_JOB_CACHE_TTL,\n },\n 'job.plan': {\n // A site's plan is an outline written without thinking (AGL-3594), and a\n // paid site's asks medium (`AI_PAID_SITE_PLAN_EFFORT`, AGL-3781): the live\n // round of 2026-10-14 planned 11 of 11 paid site briefs at medium in 16\n // attempts to the default's 22, $0.129 to $0.164, with no refusal and no\n // rule broken. Every other plan, and a Free site's on the fast tier, asks\n // this row's effort — the model's default — until a round holds it lower.\n thinking: 'adaptive',\n effort: null,\n maxTokens: 8000,\n maxTokensBasis: 'a plan of up to twelve screens with sixteen sections each, with room to think',\n eval: {\n kinds: ['page', 'template', 'component', 'layout', 'form'],\n scores: 'plans',\n source: 'authored',\n passRate: 1,\n meanScore: null,\n },\n cacheTtl: AI_SITE_JOB_CACHE_TTL,\n },\n}\n\n/**\n * What a step kind's row asks of its model, as the fields a request carries:\n * only those the row names, so a row that names none sends none and the\n * model's own default stands.\n */\nexport function aiRouteSettings(kind: AiStepKind): { thinking?: AiThinking; effort?: AiEffort } {\n const row = AI_ROUTING_TABLE[kind]\n return {\n ...(row.thinking ? { thinking: row.thinking } : {}),\n ...(row.effort ? { effort: row.effort } : {}),\n }\n}\n\n/** The model id alone — what a door records on the meter and the job. */\nexport function aiModelForStep(kind: AiStepKind, settings?: AiPluginSettings): string {\n const route = resolveAiRoute(kind, settings)\n if (!route) {\n // Every door gates on the provider being configured before it asks\n // for a model, so this is a wiring fault rather than a customer path.\n throw new Error('no AI provider is registered')\n }\n return route.model\n}\n"],"names":["AI_SETTING_PLATFORM","AI_STEP_MODEL_SETTING","AI_MODEL_CATALOG","AI_STEP_TIERS","aiCatalogEntry","aiDefaultModelFor","aiProviderById","defaultAiProvider","AI_DEFAULT_MODEL_ENV","ASSIST_MODEL_ENV","aiServingModelId","modelId","current","hops","length","successor","chosen","settings","key","value","undefined","resolveAiProvider","wanted","named","resolveAiRoute","kind","servedBy","process","provider","serving","id","model","env","trim","AI_SITE_JOB_CACHE_TTL","AI_ROUTING_TABLE","thinking","effort","maxTokens","maxTokensBasis","eval","kinds","scores","source","passRate","meanScore","cacheTtl","aiRouteSettings","row","aiModelForStep","route","Error"],"mappings":"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"}
@@ -52,8 +52,9 @@ export declare function aiDevReplayMode(env?: AiDevEnv): AiDevReplayMode;
52
52
  * The prompt-cache lifetime a development run asks for. An agent re-runs a
53
53
  * live eval every ten to forty minutes while it iterates, which is past the
54
54
  * default five-minute entry and inside an hour, so under the launcher the
55
- * breakpoints ask for the hour: the second run reads the prefix the first one
56
- * wrote instead of writing it again. A deployed server never reads this:
55
+ * shared breakpoints ask for the hour (a job's own keep five minutes,
56
+ * AGL-4050): the second run reads the prefix the first one wrote instead of
57
+ * writing it again. A deployed server never reads this:
57
58
  * `undefined` there, where the lifetime is the route's own
58
59
  * (`aiRequestCacheTtl`).
59
60
  */
@@ -53,8 +53,9 @@
53
53
  * The prompt-cache lifetime a development run asks for. An agent re-runs a
54
54
  * live eval every ten to forty minutes while it iterates, which is past the
55
55
  * default five-minute entry and inside an hour, so under the launcher the
56
- * breakpoints ask for the hour: the second run reads the prefix the first one
57
- * wrote instead of writing it again. A deployed server never reads this:
56
+ * shared breakpoints ask for the hour (a job's own keep five minutes,
57
+ * AGL-4050): the second run reads the prefix the first one wrote instead of
58
+ * writing it again. A deployed server never reads this:
58
59
  * `undefined` there, where the lifetime is the route's own
59
60
  * (`aiRequestCacheTtl`).
60
61
  */ export function aiDevCacheTtl(env = process.env) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/runtime/ai-dev-env.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 * Which development layers this process runs (AGL-3660): the replay cache,\n * the hour-long prompt cache and batch mode, each read from the environment.\n * Kept apart from `ai-dev-replay.ts`, which reads and writes files, so the\n * runtime asks these questions on every request without loading a file\n * system module into a server that will never use one. Every answer is `off`\n * in a deployed server.\n */\n\nimport type { AiEffort } from '../providers/contract'\n\nexport type AiDevReplayMode = 'off' | 'replay' | 'refresh'\n\n/** The variable that turns the replay cache on, off, or to refresh. */\nexport const AI_REPLAY_ENV = 'AGLYN_AI_REPLAY'\n/** Where the answers are kept, when not under the repository's `.cache/ai-replay`. */\nexport const AI_REPLAY_DIR_ENV = 'AGLYN_AI_REPLAY_DIR'\n/** The prompt-cache lifetime a development run asks for: `1h` or `5m`. */\nexport const AI_DEV_CACHE_TTL_ENV = 'AGLYN_AI_CACHE_TTL'\n/** The mark the live-eval launcher leaves, which `jest.setup.js` also reads. */\nexport const AI_LIVE_LAUNCHER_ENV = 'AI_EVAL_LIVE_LAUNCHER'\n\nexport type AiDevEnv = Readonly<Record<string, string | undefined>>\n\n/** Whether this process is a deployed server, where no development layer may run. */\nexport function aiIsDeployedRuntime(env: AiDevEnv = process.env): boolean {\n return (\n env['NODE_ENV'] === 'production' ||\n env['VERCEL_ENV'] === 'production' ||\n env['VERCEL_ENV'] === 'preview' ||\n Boolean(env['K_SERVICE'])\n )\n}\n\n/** The variable every live spec is started with. */\nexport const AI_LIVE_RUN_ENV = 'AGLYN_LIVE_AI'\n\n/**\n * Whether this process is a development run that asked for the live\n * provider: through the launcher, or a live spec started with\n * `AGLYN_LIVE_AI=1` (AGL-3781). Every development layer is on by default in\n * such a run — a live spec started without the launcher's mark used to send\n * every request again, at the five-minute cache and full price — and each can\n * still be turned off by its own variable.\n */\nexport function aiUnderLiveRun(env: AiDevEnv = process.env): boolean {\n if (aiIsDeployedRuntime(env)) return false\n return Boolean(env[AI_LIVE_LAUNCHER_ENV]) || String(env[AI_LIVE_RUN_ENV] ?? '').trim() === '1'\n}\n\n/** The replay mode this process runs under; `off` in any deployed server. */\nexport function aiDevReplayMode(env: AiDevEnv = process.env): AiDevReplayMode {\n if (aiIsDeployedRuntime(env)) return 'off'\n const raw = String(env[AI_REPLAY_ENV] ?? '').trim().toLowerCase()\n if (raw === 'refresh') return 'refresh'\n if (raw === '1' || raw === 'on' || raw === 'true' || raw === 'replay') return 'replay'\n if (raw === '0' || raw === 'off' || raw === 'false') return 'off'\n return aiUnderLiveRun(env) ? 'replay' : 'off'\n}\n\n/**\n * The prompt-cache lifetime a development run asks for. An agent re-runs a\n * live eval every ten to forty minutes while it iterates, which is past the\n * default five-minute entry and inside an hour, so under the launcher the\n * breakpoints ask for the hour: the second run reads the prefix the first one\n * wrote instead of writing it again. A deployed server never reads this:\n * `undefined` there, where the lifetime is the route's own\n * (`aiRequestCacheTtl`).\n */\nexport function aiDevCacheTtl(env: AiDevEnv = process.env): '1h' | undefined {\n if (aiIsDeployedRuntime(env)) return undefined\n const raw = String(env[AI_DEV_CACHE_TTL_ENV] ?? '').trim().toLowerCase()\n if (raw === '1h') return '1h'\n if (raw === '5m') return undefined\n return aiUnderLiveRun(env) ? '1h' : undefined\n}\n\n/**\n * The lifetime a request goes out with: the one its door asked for, which a\n * site job's routes set to the hour through their routing rows (AGL-3830),\n * else this process's own. A deployed server sends what the door asked for\n * and nothing else. In development `AGLYN_AI_CACHE_TTL=5m` turns every\n * request back to five minutes, the route's hour included, for a one-off\n * sweep that will not run again within the hour (docs/AI_CACHE_AUDIT_2026-10.md,\n * lever 5).\n */\nexport function aiRequestCacheTtl(requested: '1h' | undefined, env: AiDevEnv = process.env): '1h' | undefined {\n if (aiIsDeployedRuntime(env)) return requested\n if (String(env[AI_DEV_CACHE_TTL_ENV] ?? '').trim().toLowerCase() === '5m') return undefined\n return requested ?? aiDevCacheTtl(env)\n}\n\n/** A deployed server that was handed the variable says so once, and runs without it. */\nlet warnedDeployed = false\nexport function aiWarnIfReplayAskedInDeployment(env: AiDevEnv = process.env): void {\n if (warnedDeployed || !aiIsDeployedRuntime(env)) return\n if (!env[AI_REPLAY_ENV] && !env[AI_DEV_CACHE_TTL_ENV]) return\n warnedDeployed = true\n console.error(\n `[ai] ${AI_REPLAY_ENV} / ${AI_DEV_CACHE_TTL_ENV} are development-only and are ignored in a deployed server`,\n )\n}\n\n/** The variable that turns batch mode on (`runtime/ai-live-batch.ts`). */\nexport const AI_LIVE_BATCH_ENV = 'AGLYN_LIVE_AI_BATCH'\n\n/**\n * Whether this process batches its live requests: what `AGLYN_LIVE_AI_BATCH`\n * says, and in a live run that says nothing, yes (AGL-3781). Nobody waits on\n * an eval's answers, and a batch is half price on every token, cache reads\n * and writes included; `AGLYN_LIVE_AI_BATCH=0` is for the quick single run an\n * iteration wants back in seconds.\n */\nexport function aiLiveBatchWanted(env: AiDevEnv = process.env): boolean {\n if (aiIsDeployedRuntime(env)) return false\n const raw = String(env[AI_LIVE_BATCH_ENV] ?? '').trim()\n if (/^(?:1|true|yes|on)$/i.test(raw)) return true\n if (/^(?:0|false|no|off)$/i.test(raw)) return false\n return aiUnderLiveRun(env)\n}\n\n/** The variable that bounds how long a live run waits on one batch (AGL-3781). */\nexport const AI_LIVE_BATCH_WAIT_ENV = 'AGLYN_LIVE_AI_BATCH_WAIT_MS'\n\n/** How long a batch is waited on when the run names no wait. */\nexport const AI_LIVE_BATCH_DEFAULT_WAIT_MS = 30 * 60_000\n\n/**\n * How long a live run waits on one batch (AGL-3781): what\n * `AGLYN_LIVE_AI_BATCH_WAIT_MS` says, or half an hour. Most batches end in\n * minutes. One that has not by then is canceled, which keeps every answer it\n * already has, and the requests it left unanswered are asked on their own at\n * full price — so a slow batch costs a round part of its discount, never the\n * round.\n */\nexport function aiLiveBatchWaitMs(env: AiDevEnv = process.env): number {\n const raw = String(env[AI_LIVE_BATCH_WAIT_ENV] ?? '').trim()\n const parsed = raw ? Number(raw) : Number.NaN\n return Number.isFinite(parsed) && parsed >= 0 ? parsed : AI_LIVE_BATCH_DEFAULT_WAIT_MS\n}\n\n/**\n * The most a batched live run may take: a run's requests go as a few batches\n * one after another — the answers, their re-asks, the grades — and each is\n * waited on for `aiLiveBatchWaitMs`, then given time to end its cancel and to\n * have what it left asked on its own. A spec that timed out would keep none\n * of the answers it already paid for, so the bound is the worst case.\n */\nexport const AI_LIVE_BATCH_TIMEOUT_MS = 5 * 60 * 60_000\n\n/** A live spec's timeout: `baseMs`, or the batched run's when this run batches. */\nexport function aiLiveTimeoutMs(baseMs: number, env: AiDevEnv = process.env): number {\n if (!aiLiveBatchWanted(env)) return baseMs\n // Four batches in a row, each waited on, canceled and finished by hand.\n const longWait = 4 * (aiLiveBatchWaitMs(env) + 20 * 60_000)\n return Math.max(baseMs, AI_LIVE_BATCH_TIMEOUT_MS, longWait)\n}\n\n/**\n * The variable an effort A/B names its arm with (AGL-3781): a step kind's\n * effort for this run, as `job.form:medium,job.text:low`, or `*:medium` for\n * every kind. A development run only, so the defaults stay where production\n * runs them until a recorded round shows a lower effort holds.\n */\nexport const AI_EFFORT_ARM_ENV = 'AGLYN_AI_EFFORT'\n\nconst AI_EFFORT_LEVELS: readonly AiEffort[] = ['low', 'medium', 'high']\n\n/**\n * Every step kind this development run's A/B arm names, with its effort, in\n * kind order — `*` for every kind it does not name. A pair whose effort is no\n * level is ignored; a kind named twice keeps its first effort, and `*` its\n * last. Empty when the run names no arm, and in any deployed server.\n */\nexport function aiDevEffortArm(env: AiDevEnv = process.env): Record<string, AiEffort> {\n if (aiIsDeployedRuntime(env)) return {}\n const raw = String(env[AI_EFFORT_ARM_ENV] ?? '').trim()\n const arm: Record<string, AiEffort> = {}\n for (const pair of raw ? raw.split(',') : []) {\n const [kind, level] = pair.split(':').map((part) => part.trim())\n if (!kind || !AI_EFFORT_LEVELS.includes(level as AiEffort)) continue\n if (kind === '*' || !(kind in arm)) arm[kind] = level as AiEffort\n }\n return Object.fromEntries(Object.entries(arm).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)))\n}\n\n/** The effort this development run's A/B arm asks `step` at, or `undefined` for the step's own. */\nexport function aiDevEffortFor(step: string | undefined, env: AiDevEnv = process.env): AiEffort | undefined {\n const arm = aiDevEffortArm(env)\n return (step !== undefined && step !== '*' ? arm[step] : undefined) ?? arm['*']\n}\n"],"names":["AI_REPLAY_ENV","AI_REPLAY_DIR_ENV","AI_DEV_CACHE_TTL_ENV","AI_LIVE_LAUNCHER_ENV","aiIsDeployedRuntime","env","process","Boolean","AI_LIVE_RUN_ENV","aiUnderLiveRun","String","trim","aiDevReplayMode","raw","toLowerCase","aiDevCacheTtl","undefined","aiRequestCacheTtl","requested","warnedDeployed","aiWarnIfReplayAskedInDeployment","console","error","AI_LIVE_BATCH_ENV","aiLiveBatchWanted","test","AI_LIVE_BATCH_WAIT_ENV","AI_LIVE_BATCH_DEFAULT_WAIT_MS","aiLiveBatchWaitMs","parsed","Number","NaN","isFinite","AI_LIVE_BATCH_TIMEOUT_MS","aiLiveTimeoutMs","baseMs","longWait","Math","max","AI_EFFORT_ARM_ENV","AI_EFFORT_LEVELS","aiDevEffortArm","arm","pair","split","kind","level","map","part","includes","Object","fromEntries","entries","sort","a","b","aiDevEffortFor","step"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/runtime/ai-dev-env.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 * Which development layers this process runs (AGL-3660): the replay cache,\n * the hour-long prompt cache and batch mode, each read from the environment.\n * Kept apart from `ai-dev-replay.ts`, which reads and writes files, so the\n * runtime asks these questions on every request without loading a file\n * system module into a server that will never use one. Every answer is `off`\n * in a deployed server.\n */\n\nimport type { AiEffort } from '../providers/contract'\n\nexport type AiDevReplayMode = 'off' | 'replay' | 'refresh'\n\n/** The variable that turns the replay cache on, off, or to refresh. */\nexport const AI_REPLAY_ENV = 'AGLYN_AI_REPLAY'\n/** Where the answers are kept, when not under the repository's `.cache/ai-replay`. */\nexport const AI_REPLAY_DIR_ENV = 'AGLYN_AI_REPLAY_DIR'\n/** The prompt-cache lifetime a development run asks for: `1h` or `5m`. */\nexport const AI_DEV_CACHE_TTL_ENV = 'AGLYN_AI_CACHE_TTL'\n/** The mark the live-eval launcher leaves, which `jest.setup.js` also reads. */\nexport const AI_LIVE_LAUNCHER_ENV = 'AI_EVAL_LIVE_LAUNCHER'\n\nexport type AiDevEnv = Readonly<Record<string, string | undefined>>\n\n/** Whether this process is a deployed server, where no development layer may run. */\nexport function aiIsDeployedRuntime(env: AiDevEnv = process.env): boolean {\n return (\n env['NODE_ENV'] === 'production' ||\n env['VERCEL_ENV'] === 'production' ||\n env['VERCEL_ENV'] === 'preview' ||\n Boolean(env['K_SERVICE'])\n )\n}\n\n/** The variable every live spec is started with. */\nexport const AI_LIVE_RUN_ENV = 'AGLYN_LIVE_AI'\n\n/**\n * Whether this process is a development run that asked for the live\n * provider: through the launcher, or a live spec started with\n * `AGLYN_LIVE_AI=1` (AGL-3781). Every development layer is on by default in\n * such a run — a live spec started without the launcher's mark used to send\n * every request again, at the five-minute cache and full price — and each can\n * still be turned off by its own variable.\n */\nexport function aiUnderLiveRun(env: AiDevEnv = process.env): boolean {\n if (aiIsDeployedRuntime(env)) return false\n return Boolean(env[AI_LIVE_LAUNCHER_ENV]) || String(env[AI_LIVE_RUN_ENV] ?? '').trim() === '1'\n}\n\n/** The replay mode this process runs under; `off` in any deployed server. */\nexport function aiDevReplayMode(env: AiDevEnv = process.env): AiDevReplayMode {\n if (aiIsDeployedRuntime(env)) return 'off'\n const raw = String(env[AI_REPLAY_ENV] ?? '').trim().toLowerCase()\n if (raw === 'refresh') return 'refresh'\n if (raw === '1' || raw === 'on' || raw === 'true' || raw === 'replay') return 'replay'\n if (raw === '0' || raw === 'off' || raw === 'false') return 'off'\n return aiUnderLiveRun(env) ? 'replay' : 'off'\n}\n\n/**\n * The prompt-cache lifetime a development run asks for. An agent re-runs a\n * live eval every ten to forty minutes while it iterates, which is past the\n * default five-minute entry and inside an hour, so under the launcher the\n * shared breakpoints ask for the hour (a job's own keep five minutes,\n * AGL-4050): the second run reads the prefix the first one wrote instead of\n * writing it again. A deployed server never reads this:\n * `undefined` there, where the lifetime is the route's own\n * (`aiRequestCacheTtl`).\n */\nexport function aiDevCacheTtl(env: AiDevEnv = process.env): '1h' | undefined {\n if (aiIsDeployedRuntime(env)) return undefined\n const raw = String(env[AI_DEV_CACHE_TTL_ENV] ?? '').trim().toLowerCase()\n if (raw === '1h') return '1h'\n if (raw === '5m') return undefined\n return aiUnderLiveRun(env) ? '1h' : undefined\n}\n\n/**\n * The lifetime a request goes out with: the one its door asked for, which a\n * site job's routes set to the hour through their routing rows (AGL-3830),\n * else this process's own. A deployed server sends what the door asked for\n * and nothing else. In development `AGLYN_AI_CACHE_TTL=5m` turns every\n * request back to five minutes, the route's hour included, for a one-off\n * sweep that will not run again within the hour (docs/AI_CACHE_AUDIT_2026-10.md,\n * lever 5).\n */\nexport function aiRequestCacheTtl(requested: '1h' | undefined, env: AiDevEnv = process.env): '1h' | undefined {\n if (aiIsDeployedRuntime(env)) return requested\n if (String(env[AI_DEV_CACHE_TTL_ENV] ?? '').trim().toLowerCase() === '5m') return undefined\n return requested ?? aiDevCacheTtl(env)\n}\n\n/** A deployed server that was handed the variable says so once, and runs without it. */\nlet warnedDeployed = false\nexport function aiWarnIfReplayAskedInDeployment(env: AiDevEnv = process.env): void {\n if (warnedDeployed || !aiIsDeployedRuntime(env)) return\n if (!env[AI_REPLAY_ENV] && !env[AI_DEV_CACHE_TTL_ENV]) return\n warnedDeployed = true\n console.error(\n `[ai] ${AI_REPLAY_ENV} / ${AI_DEV_CACHE_TTL_ENV} are development-only and are ignored in a deployed server`,\n )\n}\n\n/** The variable that turns batch mode on (`runtime/ai-live-batch.ts`). */\nexport const AI_LIVE_BATCH_ENV = 'AGLYN_LIVE_AI_BATCH'\n\n/**\n * Whether this process batches its live requests: what `AGLYN_LIVE_AI_BATCH`\n * says, and in a live run that says nothing, yes (AGL-3781). Nobody waits on\n * an eval's answers, and a batch is half price on every token, cache reads\n * and writes included; `AGLYN_LIVE_AI_BATCH=0` is for the quick single run an\n * iteration wants back in seconds.\n */\nexport function aiLiveBatchWanted(env: AiDevEnv = process.env): boolean {\n if (aiIsDeployedRuntime(env)) return false\n const raw = String(env[AI_LIVE_BATCH_ENV] ?? '').trim()\n if (/^(?:1|true|yes|on)$/i.test(raw)) return true\n if (/^(?:0|false|no|off)$/i.test(raw)) return false\n return aiUnderLiveRun(env)\n}\n\n/** The variable that bounds how long a live run waits on one batch (AGL-3781). */\nexport const AI_LIVE_BATCH_WAIT_ENV = 'AGLYN_LIVE_AI_BATCH_WAIT_MS'\n\n/** How long a batch is waited on when the run names no wait. */\nexport const AI_LIVE_BATCH_DEFAULT_WAIT_MS = 30 * 60_000\n\n/**\n * How long a live run waits on one batch (AGL-3781): what\n * `AGLYN_LIVE_AI_BATCH_WAIT_MS` says, or half an hour. Most batches end in\n * minutes. One that has not by then is canceled, which keeps every answer it\n * already has, and the requests it left unanswered are asked on their own at\n * full price — so a slow batch costs a round part of its discount, never the\n * round.\n */\nexport function aiLiveBatchWaitMs(env: AiDevEnv = process.env): number {\n const raw = String(env[AI_LIVE_BATCH_WAIT_ENV] ?? '').trim()\n const parsed = raw ? Number(raw) : Number.NaN\n return Number.isFinite(parsed) && parsed >= 0 ? parsed : AI_LIVE_BATCH_DEFAULT_WAIT_MS\n}\n\n/**\n * The most a batched live run may take: a run's requests go as a few batches\n * one after another — the answers, their re-asks, the grades — and each is\n * waited on for `aiLiveBatchWaitMs`, then given time to end its cancel and to\n * have what it left asked on its own. A spec that timed out would keep none\n * of the answers it already paid for, so the bound is the worst case.\n */\nexport const AI_LIVE_BATCH_TIMEOUT_MS = 5 * 60 * 60_000\n\n/** A live spec's timeout: `baseMs`, or the batched run's when this run batches. */\nexport function aiLiveTimeoutMs(baseMs: number, env: AiDevEnv = process.env): number {\n if (!aiLiveBatchWanted(env)) return baseMs\n // Four batches in a row, each waited on, canceled and finished by hand.\n const longWait = 4 * (aiLiveBatchWaitMs(env) + 20 * 60_000)\n return Math.max(baseMs, AI_LIVE_BATCH_TIMEOUT_MS, longWait)\n}\n\n/**\n * The variable an effort A/B names its arm with (AGL-3781): a step kind's\n * effort for this run, as `job.form:medium,job.text:low`, or `*:medium` for\n * every kind. A development run only, so the defaults stay where production\n * runs them until a recorded round shows a lower effort holds.\n */\nexport const AI_EFFORT_ARM_ENV = 'AGLYN_AI_EFFORT'\n\nconst AI_EFFORT_LEVELS: readonly AiEffort[] = ['low', 'medium', 'high']\n\n/**\n * Every step kind this development run's A/B arm names, with its effort, in\n * kind order — `*` for every kind it does not name. A pair whose effort is no\n * level is ignored; a kind named twice keeps its first effort, and `*` its\n * last. Empty when the run names no arm, and in any deployed server.\n */\nexport function aiDevEffortArm(env: AiDevEnv = process.env): Record<string, AiEffort> {\n if (aiIsDeployedRuntime(env)) return {}\n const raw = String(env[AI_EFFORT_ARM_ENV] ?? '').trim()\n const arm: Record<string, AiEffort> = {}\n for (const pair of raw ? raw.split(',') : []) {\n const [kind, level] = pair.split(':').map((part) => part.trim())\n if (!kind || !AI_EFFORT_LEVELS.includes(level as AiEffort)) continue\n if (kind === '*' || !(kind in arm)) arm[kind] = level as AiEffort\n }\n return Object.fromEntries(Object.entries(arm).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)))\n}\n\n/** The effort this development run's A/B arm asks `step` at, or `undefined` for the step's own. */\nexport function aiDevEffortFor(step: string | undefined, env: AiDevEnv = process.env): AiEffort | undefined {\n const arm = aiDevEffortArm(env)\n return (step !== undefined && step !== '*' ? arm[step] : undefined) ?? arm['*']\n}\n"],"names":["AI_REPLAY_ENV","AI_REPLAY_DIR_ENV","AI_DEV_CACHE_TTL_ENV","AI_LIVE_LAUNCHER_ENV","aiIsDeployedRuntime","env","process","Boolean","AI_LIVE_RUN_ENV","aiUnderLiveRun","String","trim","aiDevReplayMode","raw","toLowerCase","aiDevCacheTtl","undefined","aiRequestCacheTtl","requested","warnedDeployed","aiWarnIfReplayAskedInDeployment","console","error","AI_LIVE_BATCH_ENV","aiLiveBatchWanted","test","AI_LIVE_BATCH_WAIT_ENV","AI_LIVE_BATCH_DEFAULT_WAIT_MS","aiLiveBatchWaitMs","parsed","Number","NaN","isFinite","AI_LIVE_BATCH_TIMEOUT_MS","aiLiveTimeoutMs","baseMs","longWait","Math","max","AI_EFFORT_ARM_ENV","AI_EFFORT_LEVELS","aiDevEffortArm","arm","pair","split","kind","level","map","part","includes","Object","fromEntries","entries","sort","a","b","aiDevEffortFor","step"],"mappings":"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"}
@@ -41,8 +41,11 @@ export declare const AI_PAGE_SCROLL_INTERACTION_ID = "ai-scroll-to-section";
41
41
  * when it names none, or more than one. A name matches a section whose name
42
42
  * has the same words in any case and spacing, as "consultation-request-form"
43
43
  * matches "consultation request form"; failing that, the one section whose
44
- * name holds every word of it, as "#consultation-form" names that section. An
45
- * `href` is read as a fragment, `#` and all.
44
+ * name holds every word of it, as "#consultation-form" names that section.
45
+ * When several hold them, the one whose title (the words before its first
46
+ * `:`) holds them: a plan's "Hero: … a button to the invitation builder"
47
+ * names the section "#builder" means, "Invitation builder: …", without being
48
+ * it (AGL-4032). An `href` is read as a fragment, `#` and all.
46
49
  */
47
50
  export declare function aiPageLinkSection(text: string, sections: readonly string[]): number | null;
48
51
  /** An `href` that is only a fragment: `#consultation-form`, and never `#` alone. */
@@ -40,8 +40,11 @@
40
40
  * when it names none, or more than one. A name matches a section whose name
41
41
  * has the same words in any case and spacing, as "consultation-request-form"
42
42
  * matches "consultation request form"; failing that, the one section whose
43
- * name holds every word of it, as "#consultation-form" names that section. An
44
- * `href` is read as a fragment, `#` and all.
43
+ * name holds every word of it, as "#consultation-form" names that section.
44
+ * When several hold them, the one whose title (the words before its first
45
+ * `:`) holds them: a plan's "Hero: … a button to the invitation builder"
46
+ * names the section "#builder" means, "Invitation builder: …", without being
47
+ * it (AGL-4032). An `href` is read as a fragment, `#` and all.
45
48
  */ export function aiPageLinkSection(text, sections) {
46
49
  const words = wordsOf(text.trim().replace(/^#/, ''));
47
50
  if (!words.length) return null;
@@ -53,7 +56,12 @@
53
56
  const holding = named.flatMap((section, index)=>words.every((word)=>section.includes(word)) ? [
54
57
  index
55
58
  ] : []);
56
- return holding.length === 1 ? holding[0] : null;
59
+ if (holding.length === 1) return holding[0];
60
+ const titled = holding.filter((index)=>{
61
+ const title = wordsOf(sections[index].split(':')[0]);
62
+ return words.every((word)=>title.includes(word));
63
+ });
64
+ return titled.length === 1 ? titled[0] : null;
57
65
  }
58
66
  /** An `href` that is only a fragment: `#consultation-form`, and never `#` alone. */ export function isAiFragmentHref(value) {
59
67
  return typeof value === 'string' && /^#\S+$/.test(value.trim());
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/runtime/ai-page-links.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n nodeIdFromInteractionSelector,\n nodeInteractionSelector,\n type NodeInteraction,\n} from '@aglyn/aglyn/app-utils/node-interactions'\n\n/**\n * A link to a section of the same page (AGL-3097).\n *\n * A call to action often belongs on the page it is on: a hero's \"Request a\n * Consultation\" takes the visitor to the consultation form further down. The\n * platform does that with the Scroll to element interaction (AGL-2867), never\n * with an anchor: a placed element carries no id an `href` fragment could\n * name, and the palette validator drops a bare fragment. A generated node may\n * not write an interaction either, and when a page's first section is written\n * the section it points at does not exist yet.\n *\n * So a page's link names a section of the page by its name in the plan, with\n * `scrollTo`, or with an `href` fragment made of that name's words, and the\n * page step writes the interaction for it. Every section's root id is minted\n * from the job and the section's place in the plan before any is built, so\n * the interaction can name its target on the pass that writes the link.\n */\n\n/** The prop a link names a section of its own page with, by the section's name in the plan. */\nexport const AI_PAGE_SCROLL_TO_PROP = 'scrollTo'\n\n/** The id of the interaction a link scrolling to a section of its page carries. */\nexport const AI_PAGE_SCROLL_INTERACTION_ID = 'ai-scroll-to-section'\n\n/** The words of a name or an anchor, as they are compared: lowercase, letters and digits. */\nfunction wordsOf(text: string): string[] {\n return text\n .toLowerCase()\n .split(/[^\\p{L}\\p{N}]+/u)\n .filter(Boolean)\n}\n\n/**\n * The section of the page a link names, by its place in the plan, or `null`\n * when it names none, or more than one. A name matches a section whose name\n * has the same words in any case and spacing, as \"consultation-request-form\"\n * matches \"consultation request form\"; failing that, the one section whose\n * name holds every word of it, as \"#consultation-form\" names that section. An\n * `href` is read as a fragment, `#` and all.\n */\nexport function aiPageLinkSection(text: string, sections: readonly string[]): number | null {\n const words = wordsOf(text.trim().replace(/^#/, ''))\n if (!words.length) return null\n const named = sections.map(wordsOf)\n const exact = named.flatMap((section, index) => (section.join(' ') === words.join(' ') ? [index] : []))\n if (exact.length) return exact.length === 1 ? exact[0] : null\n const holding = named.flatMap((section, index) => (words.every((word) => section.includes(word)) ? [index] : []))\n return holding.length === 1 ? holding[0] : null\n}\n\n/** An `href` that is only a fragment: `#consultation-form`, and never `#` alone. */\nexport function isAiFragmentHref(value: unknown): value is string {\n return typeof value === 'string' && /^#\\S+$/.test(value.trim())\n}\n\n/** Where a written link says it goes on its own page: a section, or a name no section has. */\nexport type AiPageLinkTarget =\n | { kind: 'section'; section: number }\n | { kind: 'unknown'; value: string; via: 'scrollTo' | 'href' }\n\n/**\n * Where a link node, as the model wrote it, goes on its own page: the section\n * its `scrollTo` names, or else the one its `href` fragment names, by its\n * place in the plan; the name it wrote when no section has it; `null` when it\n * names no section of the page at all.\n */\nexport function aiPageLinkTarget(written: unknown, sections: readonly string[]): AiPageLinkTarget | null {\n const props = isRecord(written) && isRecord(written['props']) ? written['props'] : {}\n const scrollTo = props[AI_PAGE_SCROLL_TO_PROP]\n const [value, via] =\n typeof scrollTo === 'string' && scrollTo.trim()\n ? [scrollTo.trim(), 'scrollTo' as const]\n : isAiFragmentHref(props['href'])\n ? [props['href'].trim(), 'href' as const]\n : [null, null]\n if (value === null || via === null) return null\n const section = aiPageLinkSection(value, sections)\n return section === null ? { kind: 'unknown', value, via } : { kind: 'section', section }\n}\n\n/**\n * The Scroll to element interaction a link carries to a section of its page,\n * in the shape the interactions editor stores on a node: a click, every time,\n * scrolling smoothly to the section's root with no offset, which are the\n * editor's own defaults. The selector is the one the renderer stamps.\n */\nexport function aiPageScrollInteraction(sectionRootId: string, sectionName: string): NodeInteraction {\n return {\n id: AI_PAGE_SCROLL_INTERACTION_ID,\n name: `Scroll to the ${sectionName} section`,\n enabled: true,\n trigger: { event: 'elementClick', everyTime: true },\n steps: [{ type: 'scrollTo', selector: nodeInteractionSelector(sectionRootId) }],\n }\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\n/**\n * The nodes a stored node scrolls to when it is clicked: every enabled click\n * interaction's Scroll to element steps, by the ids their selectors name.\n * Read only from a page as the page step stores it, where the palette\n * validator has already dropped every interaction a model wrote.\n */\nexport function aiStoredScrollTargets(node: unknown): string[] {\n const interactions = isRecord(node) && Array.isArray(node['interactions']) ? node['interactions'] : []\n return interactions.flatMap((interaction: unknown) => {\n if (!isRecord(interaction) || interaction['enabled'] === false) return []\n const trigger = isRecord(interaction['trigger']) ? interaction['trigger'] : {}\n if (trigger['event'] !== 'elementClick' || !Array.isArray(interaction['steps'])) return []\n return interaction['steps'].flatMap((step: unknown) => {\n const target = isRecord(step) && step['type'] === 'scrollTo' ? nodeIdFromInteractionSelector(step['selector']) : undefined\n return target ? [target] : []\n })\n })\n}\n"],"names":["nodeIdFromInteractionSelector","nodeInteractionSelector","AI_PAGE_SCROLL_TO_PROP","AI_PAGE_SCROLL_INTERACTION_ID","wordsOf","text","toLowerCase","split","filter","Boolean","aiPageLinkSection","sections","words","trim","replace","length","named","map","exact","flatMap","section","index","join","holding","every","word","includes","isAiFragmentHref","value","test","aiPageLinkTarget","written","props","isRecord","scrollTo","via","kind","aiPageScrollInteraction","sectionRootId","sectionName","id","name","enabled","trigger","event","everyTime","steps","type","selector","Array","isArray","aiStoredScrollTargets","node","interactions","interaction","step","target","undefined"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/runtime/ai-page-links.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n nodeIdFromInteractionSelector,\n nodeInteractionSelector,\n type NodeInteraction,\n} from '@aglyn/aglyn/app-utils/node-interactions'\n\n/**\n * A link to a section of the same page (AGL-3097).\n *\n * A call to action often belongs on the page it is on: a hero's \"Request a\n * Consultation\" takes the visitor to the consultation form further down. The\n * platform does that with the Scroll to element interaction (AGL-2867), never\n * with an anchor: a placed element carries no id an `href` fragment could\n * name, and the palette validator drops a bare fragment. A generated node may\n * not write an interaction either, and when a page's first section is written\n * the section it points at does not exist yet.\n *\n * So a page's link names a section of the page by its name in the plan, with\n * `scrollTo`, or with an `href` fragment made of that name's words, and the\n * page step writes the interaction for it. Every section's root id is minted\n * from the job and the section's place in the plan before any is built, so\n * the interaction can name its target on the pass that writes the link.\n */\n\n/** The prop a link names a section of its own page with, by the section's name in the plan. */\nexport const AI_PAGE_SCROLL_TO_PROP = 'scrollTo'\n\n/** The id of the interaction a link scrolling to a section of its page carries. */\nexport const AI_PAGE_SCROLL_INTERACTION_ID = 'ai-scroll-to-section'\n\n/** The words of a name or an anchor, as they are compared: lowercase, letters and digits. */\nfunction wordsOf(text: string): string[] {\n return text\n .toLowerCase()\n .split(/[^\\p{L}\\p{N}]+/u)\n .filter(Boolean)\n}\n\n/**\n * The section of the page a link names, by its place in the plan, or `null`\n * when it names none, or more than one. A name matches a section whose name\n * has the same words in any case and spacing, as \"consultation-request-form\"\n * matches \"consultation request form\"; failing that, the one section whose\n * name holds every word of it, as \"#consultation-form\" names that section.\n * When several hold them, the one whose title (the words before its first\n * `:`) holds them: a plan's \"Hero: … a button to the invitation builder\"\n * names the section \"#builder\" means, \"Invitation builder: …\", without being\n * it (AGL-4032). An `href` is read as a fragment, `#` and all.\n */\nexport function aiPageLinkSection(text: string, sections: readonly string[]): number | null {\n const words = wordsOf(text.trim().replace(/^#/, ''))\n if (!words.length) return null\n const named = sections.map(wordsOf)\n const exact = named.flatMap((section, index) => (section.join(' ') === words.join(' ') ? [index] : []))\n if (exact.length) return exact.length === 1 ? exact[0] : null\n const holding = named.flatMap((section, index) => (words.every((word) => section.includes(word)) ? [index] : []))\n if (holding.length === 1) return holding[0]\n const titled = holding.filter((index) => {\n const title = wordsOf(sections[index].split(':')[0])\n return words.every((word) => title.includes(word))\n })\n return titled.length === 1 ? titled[0] : null\n}\n\n/** An `href` that is only a fragment: `#consultation-form`, and never `#` alone. */\nexport function isAiFragmentHref(value: unknown): value is string {\n return typeof value === 'string' && /^#\\S+$/.test(value.trim())\n}\n\n/** Where a written link says it goes on its own page: a section, or a name no section has. */\nexport type AiPageLinkTarget =\n | { kind: 'section'; section: number }\n | { kind: 'unknown'; value: string; via: 'scrollTo' | 'href' }\n\n/**\n * Where a link node, as the model wrote it, goes on its own page: the section\n * its `scrollTo` names, or else the one its `href` fragment names, by its\n * place in the plan; the name it wrote when no section has it; `null` when it\n * names no section of the page at all.\n */\nexport function aiPageLinkTarget(written: unknown, sections: readonly string[]): AiPageLinkTarget | null {\n const props = isRecord(written) && isRecord(written['props']) ? written['props'] : {}\n const scrollTo = props[AI_PAGE_SCROLL_TO_PROP]\n const [value, via] =\n typeof scrollTo === 'string' && scrollTo.trim()\n ? [scrollTo.trim(), 'scrollTo' as const]\n : isAiFragmentHref(props['href'])\n ? [props['href'].trim(), 'href' as const]\n : [null, null]\n if (value === null || via === null) return null\n const section = aiPageLinkSection(value, sections)\n return section === null ? { kind: 'unknown', value, via } : { kind: 'section', section }\n}\n\n/**\n * The Scroll to element interaction a link carries to a section of its page,\n * in the shape the interactions editor stores on a node: a click, every time,\n * scrolling smoothly to the section's root with no offset, which are the\n * editor's own defaults. The selector is the one the renderer stamps.\n */\nexport function aiPageScrollInteraction(sectionRootId: string, sectionName: string): NodeInteraction {\n return {\n id: AI_PAGE_SCROLL_INTERACTION_ID,\n name: `Scroll to the ${sectionName} section`,\n enabled: true,\n trigger: { event: 'elementClick', everyTime: true },\n steps: [{ type: 'scrollTo', selector: nodeInteractionSelector(sectionRootId) }],\n }\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\n/**\n * The nodes a stored node scrolls to when it is clicked: every enabled click\n * interaction's Scroll to element steps, by the ids their selectors name.\n * Read only from a page as the page step stores it, where the palette\n * validator has already dropped every interaction a model wrote.\n */\nexport function aiStoredScrollTargets(node: unknown): string[] {\n const interactions = isRecord(node) && Array.isArray(node['interactions']) ? node['interactions'] : []\n return interactions.flatMap((interaction: unknown) => {\n if (!isRecord(interaction) || interaction['enabled'] === false) return []\n const trigger = isRecord(interaction['trigger']) ? interaction['trigger'] : {}\n if (trigger['event'] !== 'elementClick' || !Array.isArray(interaction['steps'])) return []\n return interaction['steps'].flatMap((step: unknown) => {\n const target = isRecord(step) && step['type'] === 'scrollTo' ? nodeIdFromInteractionSelector(step['selector']) : undefined\n return target ? [target] : []\n })\n })\n}\n"],"names":["nodeIdFromInteractionSelector","nodeInteractionSelector","AI_PAGE_SCROLL_TO_PROP","AI_PAGE_SCROLL_INTERACTION_ID","wordsOf","text","toLowerCase","split","filter","Boolean","aiPageLinkSection","sections","words","trim","replace","length","named","map","exact","flatMap","section","index","join","holding","every","word","includes","titled","title","isAiFragmentHref","value","test","aiPageLinkTarget","written","props","isRecord","scrollTo","via","kind","aiPageScrollInteraction","sectionRootId","sectionName","id","name","enabled","trigger","event","everyTime","steps","type","selector","Array","isArray","aiStoredScrollTargets","node","interactions","interaction","step","target","undefined"],"mappings":"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"}
@@ -16,7 +16,7 @@ import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_wi
16
16
  * See the License for the specific language governing permissions and
17
17
  * limitations under the License.
18
18
  */ import { aiCatalogEntry, aiModelCacheMinTokens } from "../providers/catalog.js";
19
- import { AI_IMAGE_MAX_BYTES, AI_IMAGE_MEDIA_TYPES, AI_REQUEST_MAX_IMAGES, AiRequestShapeError, AiUpstreamError, aiBase64Bytes, aiMessageImages } from "../providers/contract.js";
19
+ import { AI_IMAGE_MAX_BYTES, AI_IMAGE_MEDIA_TYPES, AI_REQUEST_MAX_IMAGES, AiRequestShapeError, AiUpstreamError, aiBase64Bytes, aiCacheMarkerTtls, aiMessageImages } from "../providers/contract.js";
20
20
  import { resolveAiProvider } from "../providers/routing.js";
21
21
  import { aiRequestCacheTtl, aiDevReplayMode, aiIsDeployedRuntime, aiLiveBatchWanted, aiUnderLiveRun, aiWarnIfReplayAskedInDeployment } from "./ai-dev-env.js";
22
22
  import { aiAnsweredOutsideBatch, aiCompleteInBatch } from "./ai-live-batch.js";
@@ -292,7 +292,10 @@ export async function runAiRequest(input) {
292
292
  return stream ? provider.stream(request) : provider.complete(request);
293
293
  }
294
294
  const dev = await import("./ai-dev-replay.js");
295
- const ttlMs = request.cacheTtl === '1h' ? 60 * 60000 : 5 * 60000;
295
+ // The prefix runs through the last system marker, so it stays warm as long
296
+ // as that marker's entry lives: five minutes behind a site's block (AGL-4050).
297
+ const lastMarker = aiCacheMarkerTtls(request).system.filter((ttl)=>ttl !== null).pop();
298
+ const ttlMs = lastMarker === '1h' ? 60 * 60000 : 5 * 60000;
296
299
  const prefix = dev.aiCachedPrefixKey(request);
297
300
  if (stream) {
298
301
  const live = ()=>dev.aiDevWarmPrefixFirst(prefix, ttlMs, ()=>provider.stream(request));
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/runtime/ai-runtime.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 { aiCatalogEntry, aiModelCacheMinTokens } from '../providers/catalog'\nimport {\n AI_IMAGE_MAX_BYTES,\n AI_IMAGE_MEDIA_TYPES,\n AI_REQUEST_MAX_IMAGES,\n AiRequestShapeError,\n AiUpstreamError,\n aiBase64Bytes,\n aiMessageImages,\n type AiMessage,\n type AiProvider,\n type AiProviderRequest,\n type AiRequestBase,\n type AiResult,\n type AiStreamEvent,\n type AiSystemBlock,\n type AiTool,\n} from '../providers/contract'\nimport { resolveAiProvider, type AiPluginSettings } from '../providers/routing'\nimport {\n aiRequestCacheTtl,\n aiDevReplayMode,\n aiIsDeployedRuntime,\n aiLiveBatchWanted,\n aiUnderLiveRun,\n aiWarnIfReplayAskedInDeployment,\n} from './ai-dev-env'\nimport { aiAnsweredOutsideBatch, aiCompleteInBatch } from './ai-live-batch'\n\nexport {\n AI_UPSTREAM_FAILURE_COPY,\n AiRequestShapeError,\n AiUpstreamError,\n type AiCompletion,\n type AiEffort,\n type AiImagePart,\n type AiMessage,\n type AiMessagePart,\n type AiRefusal,\n type AiRequestBase,\n type AiResult,\n type AiStreamEvent,\n type AiSystemBlock,\n type AiThinking,\n type AiTool,\n type AiToolUse,\n type AiUsage,\n} from '../providers/contract'\n\n/**\n * The one runtime every AI door calls (AGL-2903), provider-generic since\n * AGL-2939.\n *\n * `/api/assist/chat` and `/api/ai/assist` each carried their own `fetch` to\n * one vendor, their own SSE parser and their own usage extraction, and a\n * third door would have been a third copy. This module owns the request\n * shape, the cache-prefix rule, the provider resolution and the error\n * boundary; an adapter owns one vendor's wire protocol; a door owns its\n * prompt, its gate ladder and its wire format.\n *\n * The cache-prefix rule (AGL-2352) is enforced HERE, before any adapter:\n * a block marked `volatile` may not sit at or before the last\n * `cacheBreakpoint`, and more breakpoints than any provider caches are\n * refused. A provider without a cache ignores the markers and keeps the\n * order, so a request that is right for the caching adapter is right for\n * every adapter.\n *\n * The picture rule (AGL-2916) is enforced here too, and for the same reason:\n * a picture on an assistant turn, a picture for a model whose provider does\n * not say it reads one, or a picture past the bounds is refused before any\n * adapter is reached, so no door can send one a provider would reject.\n */\n\n/** The most `cacheBreakpoint` markers a request may carry. */\nexport const AI_MAX_CACHE_BREAKPOINTS = 4\n\n/**\n * The acceptable-use rules every generation prompt carries (AGL-2925).\n *\n * ONE constant, appended to each door's STATIC system text — inside the\n * cached prefix on the console chat, ahead of the mode prompt on the copy\n * assistant — so the rules the model is held to are the same at every\n * door and carry no per-org byte. The Free taste is what makes them worth\n * stating: a generated page that nobody paid for is the cheapest phishing\n * kit there is, and the model declining it is the first line, ahead of the\n * abuse-report route and the lockdown key.\n *\n * The categories are the platform's own Acceptable Use Policy, restated in\n * the second person for the model. A decline from the model's safety layer\n * arrives as a refusal, which the meter counts against the account's daily\n * pause and never against its credits.\n */\nexport const AI_ACCEPTABLE_USE_BLOCK = `Acceptable use — decline these rather than write them, and say briefly why:\n- Pages, emails or copy that harvest credentials, payment details or personal data by deception (phishing, fake sign-in or checkout pages, fake \"verify your account\" notices).\n- Impersonating a real business, brand, public body or person you were not told the user represents — their name, logo, sign-in page or voice — or copy that claims an endorsement, certification or partnership that was not given to you as a fact.\n- Content that promotes or sells prohibited goods and services: weapons, illegal drugs, counterfeit goods, malware or deceptive downloads, unlicensed gambling, or adult content involving minors.\n- Scams and fraud: fake investment or crypto schemes, advance-fee offers, tech-support scams, fake charities, deceptive \"you have won\" pages, or copy built to defraud.\n- Harassment, threats, or hateful content targeting a person or a group.\n- Spam farms: bulk near-duplicate pages or copy whose only purpose is to game search engines or send unsolicited mail.\nWhen a brief is ordinary business content, write it well and do not lecture.`\n\n/** A door's request, with the org's settings when the door knows the org. */\ninterface AiRuntimeOptions {\n /** The org's resolved `pluginSettings/ai`, for its provider and model choices. */\n settings?: AiPluginSettings\n /** A provider chosen by the caller, ahead of every setting; specs use it. */\n provider?: AiProvider\n}\n\nexport interface AiRequest extends AiRequestBase, AiRuntimeOptions {\n stream: false\n}\n\nexport interface AiStreamRequest extends AiRequestBase, AiRuntimeOptions {\n stream: true\n}\n\n/**\n * The cache guard, applied to the system blocks before any adapter sees\n * them. Exported so a spec and a door can assert a prompt's shape without\n * a provider.\n */\nexport function validateAiSystemBlocks(system: readonly AiSystemBlock[]): void {\n const breakpoints = system.filter((block) => block.cacheBreakpoint)\n if (breakpoints.length > AI_MAX_CACHE_BREAKPOINTS) {\n throw new AiRequestShapeError(\n `${breakpoints.length} cache breakpoints; at most ${AI_MAX_CACHE_BREAKPOINTS} are cached`,\n )\n }\n // Caching is a prefix match, so the LAST breakpoint decides what is\n // cached, and every block at or before it is inside the cached prefix. A\n // volatile block anywhere in that span — including one that is itself\n // the breakpoint — puts a per-request byte into the prefix (AGL-2352).\n const lastBreakpoint = system.reduce(\n (last, block, index) => (block.cacheBreakpoint ? index : last),\n -1,\n )\n system.forEach((block, index) => {\n if (block.volatile && index <= lastBreakpoint) {\n throw new AiRequestShapeError(\n `system block ${index} is volatile but sits inside the cached prefix (last breakpoint at block ${lastBreakpoint})`,\n )\n }\n })\n // A site's block is cached per site (AGL-3661), so it closes the cached\n // span: a platform-wide breakpoint after it would be keyed on the site too,\n // and every workspace would warm its own copy of a shared prompt.\n const firstSite = system.findIndex((block) => block.site && block.cacheBreakpoint)\n if (firstSite >= 0 && firstSite !== lastBreakpoint) {\n throw new AiRequestShapeError(\n `system block ${firstSite} is a site's cached block but breakpoint ${lastBreakpoint} follows it`,\n )\n }\n}\n\n/**\n * Whether a request may also cache its conversation (AGL-3809): the\n * conversation's breakpoint is one more marker, so it fits only where the\n * system blocks leave one of the provider's four free.\n */\nexport function aiConversationCacheFits(system: readonly AiSystemBlock[]): boolean {\n return system.filter((block) => block.cacheBreakpoint).length < AI_MAX_CACHE_BREAKPOINTS\n}\n\n/**\n * Whether a provider's model reads a picture in a user turn (AGL-2916): what\n * the provider's own descriptor of the model says. A model the provider does\n * not describe reads none, so an adopter's provider is never sent a picture\n * it did not say it can take.\n */\nexport function aiModelReadsImages(provider: AiProvider, model: string): boolean {\n return provider\n .models()\n .some((descriptor) => descriptor.id === model && descriptor.capabilities.vision === true)\n}\n\n/**\n * Whether a model a door resolved reads pictures on the provider that would\n * serve the request: what a door asks before it attaches one, so a model that\n * does not is sent the text alone rather than refused.\n */\nexport function aiRouteReadsImages(model: string, settings?: AiPluginSettings): boolean {\n const provider = resolveAiProvider(settings)\n return provider ? aiModelReadsImages(provider, model) : false\n}\n\n/** A character standard base64 does not use, outside its padding. */\nconst NOT_BASE64 = /[^A-Za-z0-9+/]/\n\n/**\n * Whether `data` is standard base64 and nothing else: its alphabet, then at\n * most two `=` of padding; no `data:` prefix, no line breaks.\n *\n * Scanned for a character outside the alphabet rather than matched whole: a\n * picture is megabytes of base64, and a whole-string pattern with a greedy\n * loop can exhaust the regular expression engine's backtrack stack on input\n * that large, which throws a RangeError instead of answering.\n */\nfunction isBareBase64(data: string): boolean {\n const body = data.endsWith('==') ? data.slice(0, -2) : data.endsWith('=') ? data.slice(0, -1) : data\n return body.length > 0 && !NOT_BASE64.test(body)\n}\n\n/**\n * The picture guard (AGL-2916), applied to the messages before any adapter\n * sees them. A picture rides only a user turn, only to a model that reads\n * pictures, only in a format every such model takes, as bare base64 within\n * the size a provider accepts, and no more of them than a request may carry.\n * `readsImages` is asked only when a turn carries a picture. Exported so a\n * spec and a door can assert a request's shape without a provider.\n */\nexport function validateAiMessages(messages: readonly AiMessage[], readsImages: () => boolean): void {\n messages.forEach((message, index) => {\n if (typeof message.content === 'string') return\n if (!Array.isArray(message.content) || message.content.length === 0) {\n throw new AiRequestShapeError(`message ${index} has neither text nor parts`)\n }\n for (const part of message.content) {\n if (part.type === 'text') {\n if (typeof part.text !== 'string') {\n throw new AiRequestShapeError(`message ${index} has a text part with no text`)\n }\n continue\n }\n if (part.type !== 'image') {\n throw new AiRequestShapeError(`message ${index} has a part of an unknown type`)\n }\n if (message.role !== 'user') {\n throw new AiRequestShapeError(`message ${index} is an assistant turn and carries a picture`)\n }\n if (!(AI_IMAGE_MEDIA_TYPES as readonly string[]).includes(part.mediaType)) {\n throw new AiRequestShapeError(`message ${index} carries a picture of an unsupported type`)\n }\n if (typeof part.data !== 'string') {\n throw new AiRequestShapeError(`message ${index} carries a picture that is not bare base64`)\n }\n // The size first: it needs only the length, so an oversized picture is\n // refused without reading its bytes.\n if (aiBase64Bytes(part.data) > AI_IMAGE_MAX_BYTES) {\n throw new AiRequestShapeError(\n `message ${index} carries a picture over ${AI_IMAGE_MAX_BYTES} bytes`,\n )\n }\n if (!isBareBase64(part.data)) {\n throw new AiRequestShapeError(`message ${index} carries a picture that is not bare base64`)\n }\n }\n })\n const images = aiMessageImages(messages).length\n if (images > AI_REQUEST_MAX_IMAGES) {\n throw new AiRequestShapeError(`${images} pictures; a request carries at most ${AI_REQUEST_MAX_IMAGES}`)\n }\n if (images > 0 && !readsImages()) {\n throw new AiRequestShapeError('the request carries a picture and its model does not read pictures')\n }\n}\n\n/**\n * Characters to a token when a prompt is measured rather than tokenized\n * (AGL-2937). Four is the high end of English prose, so reading a prefix\n * through it errs LOW on the token count — and a prefix counts as cached\n * only when it clears its model's minimum even at that reading. Erring the\n * other way would let a prompt claim a cache it does not get.\n */\nexport const AI_CACHE_PREFIX_TOKEN_CHARS = 4\n\n/**\n * The characters inside the cached span of a request: the tools, which a\n * provider renders ahead of the system blocks and which therefore sit in\n * every prefix, plus every system block up to and including the last\n * breakpoint. Zero when the request marks no breakpoint at all.\n */\nexport function aiCachedPrefixChars(\n system: readonly AiSystemBlock[],\n tools: readonly AiTool[] = [],\n): number {\n const lastBreakpoint = system.reduce(\n (last, block, index) => (block.cacheBreakpoint ? index : last),\n -1,\n )\n if (lastBreakpoint < 0) return 0\n const toolChars = tools.reduce((total, tool) => total + JSON.stringify(tool).length, 0)\n return system\n .slice(0, lastBreakpoint + 1)\n .reduce((total, block) => total + block.text.length, toolChars)\n}\n\n/**\n * Whether this request's cached span is long enough for the model to cache\n * it at all (AGL-2937). A prefix under the model's `cacheMinTokens` is\n * served with the breakpoints honored and nothing cached — no error, and a\n * usage report that reads as a permanent miss — so a door designed around a\n * cache has to answer this question before it believes its own markers, and\n * `runtime/ai-prompt-cache.spec.ts` answers it for every door at once.\n */\nexport function aiCachedPrefixCaches(\n model: string,\n system: readonly AiSystemBlock[],\n tools: readonly AiTool[] = [],\n): boolean {\n const chars = aiCachedPrefixChars(system, tools)\n return chars / AI_CACHE_PREFIX_TOKEN_CHARS >= aiModelCacheMinTokens(model)\n}\n\n/**\n * Whether a provider is configured to answer: registered, and its key\n * present. Every door asks this before it spends a reservation, and\n * answers 501 (or the docs fallback) when it is false.\n */\nexport function aiProviderReady(settings?: AiPluginSettings): boolean {\n return Boolean(resolveAiProvider(settings)?.readApiKey())\n}\n\n/** The provider that would answer, for a door that names it in its logs or its inventory. */\nexport function aiActiveProvider(settings?: AiPluginSettings): AiProvider | undefined {\n return resolveAiProvider(settings)\n}\n\nfunction providerFor(input: AiRuntimeOptions): { provider: AiProvider; apiKey: string } {\n const provider = input.provider ?? resolveAiProvider(input.settings)\n if (!provider) throw new Error('no AI provider is registered')\n const apiKey = provider.readApiKey()\n if (!apiKey) {\n // Every door gates on `aiProviderReady` before it gets here, so this is\n // a wiring fault rather than a customer path.\n throw new Error(`${provider.apiKeyEnv} is not set`)\n }\n return { provider, apiKey }\n}\n\n/**\n * Send one request through the resolved provider.\n *\n * Streaming: resolves once the provider has ACCEPTED the request — after\n * the headers, before the first token — with an iterator of simplified\n * events the caller wraps in its own wire format. A non-2xx answer rejects\n * with `AiUpstreamError` before the iterator exists, so a door can still\n * send a plain error status and hand back its reservation.\n *\n * Non-streaming: resolves with the parsed result, a decline typed as\n * `AiRefusal` rather than thrown.\n */\nexport async function runAiRequest(\n input: AiStreamRequest,\n): Promise<AsyncIterable<AiStreamEvent>>\nexport async function runAiRequest(input: AiRequest): Promise<AiResult>\nexport async function runAiRequest(\n input: AiRequest | AiStreamRequest,\n): Promise<AsyncIterable<AiStreamEvent> | AiResult> {\n validateAiSystemBlocks(input.system)\n if (input.cacheConversation && !aiConversationCacheFits(input.system)) {\n throw new AiRequestShapeError(\n `the conversation's breakpoint would be the ${AI_MAX_CACHE_BREAKPOINTS + 1}th; at most ${AI_MAX_CACHE_BREAKPOINTS} are cached`,\n )\n }\n refuseRetiredModelInLiveRun(input.model)\n const { provider, apiKey } = providerFor(input)\n validateAiMessages(input.messages, () => aiModelReadsImages(provider, input.model))\n const { stream, settings: _settings, provider: _provider, ...request } = input\n aiWarnIfReplayAskedInDeployment()\n const cacheTtl = aiRequestCacheTtl(request.cacheTtl)\n const providerRequest: AiProviderRequest = { ...request, ...(cacheTtl ? { cacheTtl } : {}), apiKey }\n try {\n if (aiIsDeployedRuntime()) {\n return await (stream ? provider.stream(providerRequest) : provider.complete(providerRequest))\n }\n return await runInDevelopment(provider, providerRequest, stream)\n } catch (error) {\n await reportAccountProblem(provider, error)\n throw error\n }\n}\n\n/**\n * A live development run that names a retired model is refused before it\n * spends (AGL-3781). Routing serves a retired id's successor wherever a\n * setting or an override names it, so a retired id reaching here was asked\n * for by name — an override left in a run, or a comparison a spec kept — and\n * would bill a model production no longer serves.\n */\nfunction refuseRetiredModelInLiveRun(model: string): void {\n if (!aiUnderLiveRun()) return\n const successor = aiCatalogEntry(model)?.successor\n if (successor) {\n throw new AiRequestShapeError(`${model} is retired; ${successor} serves in its place`)\n }\n}\n\n/**\n * A request in a development process (AGL-3660): through the replay cache,\n * batch mode and the first-request-warms-the-prefix rule when this process\n * asked for them (`ai-dev-env.ts`), straight to the provider when it asked\n * for none. The file-backed half is loaded only when a layer is on, so a dev\n * server that never asked loads nothing it did not before.\n */\nasync function runInDevelopment(\n provider: AiProvider,\n request: AiProviderRequest,\n stream: boolean,\n): Promise<AsyncIterable<AiStreamEvent> | AiResult> {\n const mode = aiDevReplayMode()\n const batch = !stream && aiLiveBatchWanted() && Boolean(provider.completeBatch)\n if (mode === 'off' && !batch) {\n return stream ? provider.stream(request) : provider.complete(request)\n }\n const dev = await import('./ai-dev-replay')\n const ttlMs = request.cacheTtl === '1h' ? 60 * 60_000 : 5 * 60_000\n const prefix = dev.aiCachedPrefixKey(request)\n if (stream) {\n const live = () => dev.aiDevWarmPrefixFirst(prefix, ttlMs, () => provider.stream(request))\n return mode === 'off' ? live() : dev.aiDevReplayStream(provider, request, mode, live)\n }\n const live = async (): Promise<AiResult> => {\n const result = batch\n ? await aiCompleteInBatch(provider, request)\n : await dev.aiDevWarmPrefixFirst(prefix, ttlMs, () => provider.complete(request))\n // A request its batch left unanswered was asked on its own, at full price (AGL-3781).\n const discounted = batch && !aiAnsweredOutsideBatch(result)\n dev.aiDevCountLive(result.usage, request.model, discounted ? AI_BATCH_PRICE_FACTOR : 1)\n return result\n }\n return mode === 'off' ? live() : dev.aiDevReplayComplete(provider, request, mode, live)\n}\n\n/** What a batch's discount leaves of a request's price, for the development count. */\nconst AI_BATCH_PRICE_FACTOR = 0.5\n\n/**\n * Tells the operator when the failure is the platform's provider account —\n * a refused key, no credit — rather than the request (AGL-3377). Once per\n * provider and problem per window; loaded lazily so the runtime pulls no\n * Admin SDK until an account is actually broken. Never throws.\n */\nasync function reportAccountProblem(provider: AiProvider, error: unknown): Promise<void> {\n if (!(error instanceof AiUpstreamError) || !error.accountProblem) return\n try {\n const [{ raiseOperatorAlert }, { AI_PROVIDER_UNAVAILABLE }] = await Promise.all([\n import('./operator-alerts-seam'),\n import('../operator-alerts'),\n ])\n await raiseOperatorAlert(AI_PROVIDER_UNAVAILABLE, {\n dedupeKey: `${provider.id}:${error.accountProblem}`,\n context: {\n provider: provider.label,\n keyEnv: provider.apiKeyEnv,\n reason:\n error.accountProblem === 'credentials'\n ? `its API key was refused (HTTP ${error.status ?? 'unknown'})`\n : 'the account is out of credit',\n },\n })\n } catch (alertError) {\n console.error('[ai] the provider account alert could not be raised', alertError)\n }\n}\n"],"names":["aiCatalogEntry","aiModelCacheMinTokens","AI_IMAGE_MAX_BYTES","AI_IMAGE_MEDIA_TYPES","AI_REQUEST_MAX_IMAGES","AiRequestShapeError","AiUpstreamError","aiBase64Bytes","aiMessageImages","resolveAiProvider","aiRequestCacheTtl","aiDevReplayMode","aiIsDeployedRuntime","aiLiveBatchWanted","aiUnderLiveRun","aiWarnIfReplayAskedInDeployment","aiAnsweredOutsideBatch","aiCompleteInBatch","AI_UPSTREAM_FAILURE_COPY","AI_MAX_CACHE_BREAKPOINTS","AI_ACCEPTABLE_USE_BLOCK","validateAiSystemBlocks","system","breakpoints","filter","block","cacheBreakpoint","length","lastBreakpoint","reduce","last","index","forEach","volatile","firstSite","findIndex","site","aiConversationCacheFits","aiModelReadsImages","provider","model","models","some","descriptor","id","capabilities","vision","aiRouteReadsImages","settings","NOT_BASE64","isBareBase64","data","body","endsWith","slice","test","validateAiMessages","messages","readsImages","message","content","Array","isArray","part","type","text","role","includes","mediaType","images","AI_CACHE_PREFIX_TOKEN_CHARS","aiCachedPrefixChars","tools","toolChars","total","tool","JSON","stringify","aiCachedPrefixCaches","chars","aiProviderReady","Boolean","readApiKey","aiActiveProvider","providerFor","input","Error","apiKey","apiKeyEnv","runAiRequest","cacheConversation","refuseRetiredModelInLiveRun","stream","_settings","_provider","request","cacheTtl","providerRequest","complete","runInDevelopment","error","reportAccountProblem","successor","mode","batch","completeBatch","dev","ttlMs","prefix","aiCachedPrefixKey","live","aiDevWarmPrefixFirst","aiDevReplayStream","result","discounted","aiDevCountLive","usage","AI_BATCH_PRICE_FACTOR","aiDevReplayComplete","accountProblem","raiseOperatorAlert","AI_PROVIDER_UNAVAILABLE","Promise","all","dedupeKey","context","label","keyEnv","reason","status","alertError","console"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/runtime/ai-runtime.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 { aiCatalogEntry, aiModelCacheMinTokens } from '../providers/catalog'\nimport {\n AI_IMAGE_MAX_BYTES,\n AI_IMAGE_MEDIA_TYPES,\n AI_REQUEST_MAX_IMAGES,\n AiRequestShapeError,\n AiUpstreamError,\n aiBase64Bytes,\n aiCacheMarkerTtls,\n aiMessageImages,\n type AiMessage,\n type AiProvider,\n type AiProviderRequest,\n type AiRequestBase,\n type AiResult,\n type AiStreamEvent,\n type AiSystemBlock,\n type AiTool,\n} from '../providers/contract'\nimport { resolveAiProvider, type AiPluginSettings } from '../providers/routing'\nimport {\n aiRequestCacheTtl,\n aiDevReplayMode,\n aiIsDeployedRuntime,\n aiLiveBatchWanted,\n aiUnderLiveRun,\n aiWarnIfReplayAskedInDeployment,\n} from './ai-dev-env'\nimport { aiAnsweredOutsideBatch, aiCompleteInBatch } from './ai-live-batch'\n\nexport {\n AI_UPSTREAM_FAILURE_COPY,\n AiRequestShapeError,\n AiUpstreamError,\n type AiCompletion,\n type AiEffort,\n type AiImagePart,\n type AiMessage,\n type AiMessagePart,\n type AiRefusal,\n type AiRequestBase,\n type AiResult,\n type AiStreamEvent,\n type AiSystemBlock,\n type AiThinking,\n type AiTool,\n type AiToolUse,\n type AiUsage,\n} from '../providers/contract'\n\n/**\n * The one runtime every AI door calls (AGL-2903), provider-generic since\n * AGL-2939.\n *\n * `/api/assist/chat` and `/api/ai/assist` each carried their own `fetch` to\n * one vendor, their own SSE parser and their own usage extraction, and a\n * third door would have been a third copy. This module owns the request\n * shape, the cache-prefix rule, the provider resolution and the error\n * boundary; an adapter owns one vendor's wire protocol; a door owns its\n * prompt, its gate ladder and its wire format.\n *\n * The cache-prefix rule (AGL-2352) is enforced HERE, before any adapter:\n * a block marked `volatile` may not sit at or before the last\n * `cacheBreakpoint`, and more breakpoints than any provider caches are\n * refused. A provider without a cache ignores the markers and keeps the\n * order, so a request that is right for the caching adapter is right for\n * every adapter.\n *\n * The picture rule (AGL-2916) is enforced here too, and for the same reason:\n * a picture on an assistant turn, a picture for a model whose provider does\n * not say it reads one, or a picture past the bounds is refused before any\n * adapter is reached, so no door can send one a provider would reject.\n */\n\n/** The most `cacheBreakpoint` markers a request may carry. */\nexport const AI_MAX_CACHE_BREAKPOINTS = 4\n\n/**\n * The acceptable-use rules every generation prompt carries (AGL-2925).\n *\n * ONE constant, appended to each door's STATIC system text — inside the\n * cached prefix on the console chat, ahead of the mode prompt on the copy\n * assistant — so the rules the model is held to are the same at every\n * door and carry no per-org byte. The Free taste is what makes them worth\n * stating: a generated page that nobody paid for is the cheapest phishing\n * kit there is, and the model declining it is the first line, ahead of the\n * abuse-report route and the lockdown key.\n *\n * The categories are the platform's own Acceptable Use Policy, restated in\n * the second person for the model. A decline from the model's safety layer\n * arrives as a refusal, which the meter counts against the account's daily\n * pause and never against its credits.\n */\nexport const AI_ACCEPTABLE_USE_BLOCK = `Acceptable use — decline these rather than write them, and say briefly why:\n- Pages, emails or copy that harvest credentials, payment details or personal data by deception (phishing, fake sign-in or checkout pages, fake \"verify your account\" notices).\n- Impersonating a real business, brand, public body or person you were not told the user represents — their name, logo, sign-in page or voice — or copy that claims an endorsement, certification or partnership that was not given to you as a fact.\n- Content that promotes or sells prohibited goods and services: weapons, illegal drugs, counterfeit goods, malware or deceptive downloads, unlicensed gambling, or adult content involving minors.\n- Scams and fraud: fake investment or crypto schemes, advance-fee offers, tech-support scams, fake charities, deceptive \"you have won\" pages, or copy built to defraud.\n- Harassment, threats, or hateful content targeting a person or a group.\n- Spam farms: bulk near-duplicate pages or copy whose only purpose is to game search engines or send unsolicited mail.\nWhen a brief is ordinary business content, write it well and do not lecture.`\n\n/** A door's request, with the org's settings when the door knows the org. */\ninterface AiRuntimeOptions {\n /** The org's resolved `pluginSettings/ai`, for its provider and model choices. */\n settings?: AiPluginSettings\n /** A provider chosen by the caller, ahead of every setting; specs use it. */\n provider?: AiProvider\n}\n\nexport interface AiRequest extends AiRequestBase, AiRuntimeOptions {\n stream: false\n}\n\nexport interface AiStreamRequest extends AiRequestBase, AiRuntimeOptions {\n stream: true\n}\n\n/**\n * The cache guard, applied to the system blocks before any adapter sees\n * them. Exported so a spec and a door can assert a prompt's shape without\n * a provider.\n */\nexport function validateAiSystemBlocks(system: readonly AiSystemBlock[]): void {\n const breakpoints = system.filter((block) => block.cacheBreakpoint)\n if (breakpoints.length > AI_MAX_CACHE_BREAKPOINTS) {\n throw new AiRequestShapeError(\n `${breakpoints.length} cache breakpoints; at most ${AI_MAX_CACHE_BREAKPOINTS} are cached`,\n )\n }\n // Caching is a prefix match, so the LAST breakpoint decides what is\n // cached, and every block at or before it is inside the cached prefix. A\n // volatile block anywhere in that span — including one that is itself\n // the breakpoint — puts a per-request byte into the prefix (AGL-2352).\n const lastBreakpoint = system.reduce(\n (last, block, index) => (block.cacheBreakpoint ? index : last),\n -1,\n )\n system.forEach((block, index) => {\n if (block.volatile && index <= lastBreakpoint) {\n throw new AiRequestShapeError(\n `system block ${index} is volatile but sits inside the cached prefix (last breakpoint at block ${lastBreakpoint})`,\n )\n }\n })\n // A site's block is cached per site (AGL-3661), so it closes the cached\n // span: a platform-wide breakpoint after it would be keyed on the site too,\n // and every workspace would warm its own copy of a shared prompt.\n const firstSite = system.findIndex((block) => block.site && block.cacheBreakpoint)\n if (firstSite >= 0 && firstSite !== lastBreakpoint) {\n throw new AiRequestShapeError(\n `system block ${firstSite} is a site's cached block but breakpoint ${lastBreakpoint} follows it`,\n )\n }\n}\n\n/**\n * Whether a request may also cache its conversation (AGL-3809): the\n * conversation's breakpoint is one more marker, so it fits only where the\n * system blocks leave one of the provider's four free.\n */\nexport function aiConversationCacheFits(system: readonly AiSystemBlock[]): boolean {\n return system.filter((block) => block.cacheBreakpoint).length < AI_MAX_CACHE_BREAKPOINTS\n}\n\n/**\n * Whether a provider's model reads a picture in a user turn (AGL-2916): what\n * the provider's own descriptor of the model says. A model the provider does\n * not describe reads none, so an adopter's provider is never sent a picture\n * it did not say it can take.\n */\nexport function aiModelReadsImages(provider: AiProvider, model: string): boolean {\n return provider\n .models()\n .some((descriptor) => descriptor.id === model && descriptor.capabilities.vision === true)\n}\n\n/**\n * Whether a model a door resolved reads pictures on the provider that would\n * serve the request: what a door asks before it attaches one, so a model that\n * does not is sent the text alone rather than refused.\n */\nexport function aiRouteReadsImages(model: string, settings?: AiPluginSettings): boolean {\n const provider = resolveAiProvider(settings)\n return provider ? aiModelReadsImages(provider, model) : false\n}\n\n/** A character standard base64 does not use, outside its padding. */\nconst NOT_BASE64 = /[^A-Za-z0-9+/]/\n\n/**\n * Whether `data` is standard base64 and nothing else: its alphabet, then at\n * most two `=` of padding; no `data:` prefix, no line breaks.\n *\n * Scanned for a character outside the alphabet rather than matched whole: a\n * picture is megabytes of base64, and a whole-string pattern with a greedy\n * loop can exhaust the regular expression engine's backtrack stack on input\n * that large, which throws a RangeError instead of answering.\n */\nfunction isBareBase64(data: string): boolean {\n const body = data.endsWith('==') ? data.slice(0, -2) : data.endsWith('=') ? data.slice(0, -1) : data\n return body.length > 0 && !NOT_BASE64.test(body)\n}\n\n/**\n * The picture guard (AGL-2916), applied to the messages before any adapter\n * sees them. A picture rides only a user turn, only to a model that reads\n * pictures, only in a format every such model takes, as bare base64 within\n * the size a provider accepts, and no more of them than a request may carry.\n * `readsImages` is asked only when a turn carries a picture. Exported so a\n * spec and a door can assert a request's shape without a provider.\n */\nexport function validateAiMessages(messages: readonly AiMessage[], readsImages: () => boolean): void {\n messages.forEach((message, index) => {\n if (typeof message.content === 'string') return\n if (!Array.isArray(message.content) || message.content.length === 0) {\n throw new AiRequestShapeError(`message ${index} has neither text nor parts`)\n }\n for (const part of message.content) {\n if (part.type === 'text') {\n if (typeof part.text !== 'string') {\n throw new AiRequestShapeError(`message ${index} has a text part with no text`)\n }\n continue\n }\n if (part.type !== 'image') {\n throw new AiRequestShapeError(`message ${index} has a part of an unknown type`)\n }\n if (message.role !== 'user') {\n throw new AiRequestShapeError(`message ${index} is an assistant turn and carries a picture`)\n }\n if (!(AI_IMAGE_MEDIA_TYPES as readonly string[]).includes(part.mediaType)) {\n throw new AiRequestShapeError(`message ${index} carries a picture of an unsupported type`)\n }\n if (typeof part.data !== 'string') {\n throw new AiRequestShapeError(`message ${index} carries a picture that is not bare base64`)\n }\n // The size first: it needs only the length, so an oversized picture is\n // refused without reading its bytes.\n if (aiBase64Bytes(part.data) > AI_IMAGE_MAX_BYTES) {\n throw new AiRequestShapeError(\n `message ${index} carries a picture over ${AI_IMAGE_MAX_BYTES} bytes`,\n )\n }\n if (!isBareBase64(part.data)) {\n throw new AiRequestShapeError(`message ${index} carries a picture that is not bare base64`)\n }\n }\n })\n const images = aiMessageImages(messages).length\n if (images > AI_REQUEST_MAX_IMAGES) {\n throw new AiRequestShapeError(`${images} pictures; a request carries at most ${AI_REQUEST_MAX_IMAGES}`)\n }\n if (images > 0 && !readsImages()) {\n throw new AiRequestShapeError('the request carries a picture and its model does not read pictures')\n }\n}\n\n/**\n * Characters to a token when a prompt is measured rather than tokenized\n * (AGL-2937). Four is the high end of English prose, so reading a prefix\n * through it errs LOW on the token count — and a prefix counts as cached\n * only when it clears its model's minimum even at that reading. Erring the\n * other way would let a prompt claim a cache it does not get.\n */\nexport const AI_CACHE_PREFIX_TOKEN_CHARS = 4\n\n/**\n * The characters inside the cached span of a request: the tools, which a\n * provider renders ahead of the system blocks and which therefore sit in\n * every prefix, plus every system block up to and including the last\n * breakpoint. Zero when the request marks no breakpoint at all.\n */\nexport function aiCachedPrefixChars(\n system: readonly AiSystemBlock[],\n tools: readonly AiTool[] = [],\n): number {\n const lastBreakpoint = system.reduce(\n (last, block, index) => (block.cacheBreakpoint ? index : last),\n -1,\n )\n if (lastBreakpoint < 0) return 0\n const toolChars = tools.reduce((total, tool) => total + JSON.stringify(tool).length, 0)\n return system\n .slice(0, lastBreakpoint + 1)\n .reduce((total, block) => total + block.text.length, toolChars)\n}\n\n/**\n * Whether this request's cached span is long enough for the model to cache\n * it at all (AGL-2937). A prefix under the model's `cacheMinTokens` is\n * served with the breakpoints honored and nothing cached — no error, and a\n * usage report that reads as a permanent miss — so a door designed around a\n * cache has to answer this question before it believes its own markers, and\n * `runtime/ai-prompt-cache.spec.ts` answers it for every door at once.\n */\nexport function aiCachedPrefixCaches(\n model: string,\n system: readonly AiSystemBlock[],\n tools: readonly AiTool[] = [],\n): boolean {\n const chars = aiCachedPrefixChars(system, tools)\n return chars / AI_CACHE_PREFIX_TOKEN_CHARS >= aiModelCacheMinTokens(model)\n}\n\n/**\n * Whether a provider is configured to answer: registered, and its key\n * present. Every door asks this before it spends a reservation, and\n * answers 501 (or the docs fallback) when it is false.\n */\nexport function aiProviderReady(settings?: AiPluginSettings): boolean {\n return Boolean(resolveAiProvider(settings)?.readApiKey())\n}\n\n/** The provider that would answer, for a door that names it in its logs or its inventory. */\nexport function aiActiveProvider(settings?: AiPluginSettings): AiProvider | undefined {\n return resolveAiProvider(settings)\n}\n\nfunction providerFor(input: AiRuntimeOptions): { provider: AiProvider; apiKey: string } {\n const provider = input.provider ?? resolveAiProvider(input.settings)\n if (!provider) throw new Error('no AI provider is registered')\n const apiKey = provider.readApiKey()\n if (!apiKey) {\n // Every door gates on `aiProviderReady` before it gets here, so this is\n // a wiring fault rather than a customer path.\n throw new Error(`${provider.apiKeyEnv} is not set`)\n }\n return { provider, apiKey }\n}\n\n/**\n * Send one request through the resolved provider.\n *\n * Streaming: resolves once the provider has ACCEPTED the request — after\n * the headers, before the first token — with an iterator of simplified\n * events the caller wraps in its own wire format. A non-2xx answer rejects\n * with `AiUpstreamError` before the iterator exists, so a door can still\n * send a plain error status and hand back its reservation.\n *\n * Non-streaming: resolves with the parsed result, a decline typed as\n * `AiRefusal` rather than thrown.\n */\nexport async function runAiRequest(\n input: AiStreamRequest,\n): Promise<AsyncIterable<AiStreamEvent>>\nexport async function runAiRequest(input: AiRequest): Promise<AiResult>\nexport async function runAiRequest(\n input: AiRequest | AiStreamRequest,\n): Promise<AsyncIterable<AiStreamEvent> | AiResult> {\n validateAiSystemBlocks(input.system)\n if (input.cacheConversation && !aiConversationCacheFits(input.system)) {\n throw new AiRequestShapeError(\n `the conversation's breakpoint would be the ${AI_MAX_CACHE_BREAKPOINTS + 1}th; at most ${AI_MAX_CACHE_BREAKPOINTS} are cached`,\n )\n }\n refuseRetiredModelInLiveRun(input.model)\n const { provider, apiKey } = providerFor(input)\n validateAiMessages(input.messages, () => aiModelReadsImages(provider, input.model))\n const { stream, settings: _settings, provider: _provider, ...request } = input\n aiWarnIfReplayAskedInDeployment()\n const cacheTtl = aiRequestCacheTtl(request.cacheTtl)\n const providerRequest: AiProviderRequest = { ...request, ...(cacheTtl ? { cacheTtl } : {}), apiKey }\n try {\n if (aiIsDeployedRuntime()) {\n return await (stream ? provider.stream(providerRequest) : provider.complete(providerRequest))\n }\n return await runInDevelopment(provider, providerRequest, stream)\n } catch (error) {\n await reportAccountProblem(provider, error)\n throw error\n }\n}\n\n/**\n * A live development run that names a retired model is refused before it\n * spends (AGL-3781). Routing serves a retired id's successor wherever a\n * setting or an override names it, so a retired id reaching here was asked\n * for by name — an override left in a run, or a comparison a spec kept — and\n * would bill a model production no longer serves.\n */\nfunction refuseRetiredModelInLiveRun(model: string): void {\n if (!aiUnderLiveRun()) return\n const successor = aiCatalogEntry(model)?.successor\n if (successor) {\n throw new AiRequestShapeError(`${model} is retired; ${successor} serves in its place`)\n }\n}\n\n/**\n * A request in a development process (AGL-3660): through the replay cache,\n * batch mode and the first-request-warms-the-prefix rule when this process\n * asked for them (`ai-dev-env.ts`), straight to the provider when it asked\n * for none. The file-backed half is loaded only when a layer is on, so a dev\n * server that never asked loads nothing it did not before.\n */\nasync function runInDevelopment(\n provider: AiProvider,\n request: AiProviderRequest,\n stream: boolean,\n): Promise<AsyncIterable<AiStreamEvent> | AiResult> {\n const mode = aiDevReplayMode()\n const batch = !stream && aiLiveBatchWanted() && Boolean(provider.completeBatch)\n if (mode === 'off' && !batch) {\n return stream ? provider.stream(request) : provider.complete(request)\n }\n const dev = await import('./ai-dev-replay')\n // The prefix runs through the last system marker, so it stays warm as long\n // as that marker's entry lives: five minutes behind a site's block (AGL-4050).\n const lastMarker = aiCacheMarkerTtls(request).system.filter((ttl) => ttl !== null).pop()\n const ttlMs = lastMarker === '1h' ? 60 * 60_000 : 5 * 60_000\n const prefix = dev.aiCachedPrefixKey(request)\n if (stream) {\n const live = () => dev.aiDevWarmPrefixFirst(prefix, ttlMs, () => provider.stream(request))\n return mode === 'off' ? live() : dev.aiDevReplayStream(provider, request, mode, live)\n }\n const live = async (): Promise<AiResult> => {\n const result = batch\n ? await aiCompleteInBatch(provider, request)\n : await dev.aiDevWarmPrefixFirst(prefix, ttlMs, () => provider.complete(request))\n // A request its batch left unanswered was asked on its own, at full price (AGL-3781).\n const discounted = batch && !aiAnsweredOutsideBatch(result)\n dev.aiDevCountLive(result.usage, request.model, discounted ? AI_BATCH_PRICE_FACTOR : 1)\n return result\n }\n return mode === 'off' ? live() : dev.aiDevReplayComplete(provider, request, mode, live)\n}\n\n/** What a batch's discount leaves of a request's price, for the development count. */\nconst AI_BATCH_PRICE_FACTOR = 0.5\n\n/**\n * Tells the operator when the failure is the platform's provider account —\n * a refused key, no credit — rather than the request (AGL-3377). Once per\n * provider and problem per window; loaded lazily so the runtime pulls no\n * Admin SDK until an account is actually broken. Never throws.\n */\nasync function reportAccountProblem(provider: AiProvider, error: unknown): Promise<void> {\n if (!(error instanceof AiUpstreamError) || !error.accountProblem) return\n try {\n const [{ raiseOperatorAlert }, { AI_PROVIDER_UNAVAILABLE }] = await Promise.all([\n import('./operator-alerts-seam'),\n import('../operator-alerts'),\n ])\n await raiseOperatorAlert(AI_PROVIDER_UNAVAILABLE, {\n dedupeKey: `${provider.id}:${error.accountProblem}`,\n context: {\n provider: provider.label,\n keyEnv: provider.apiKeyEnv,\n reason:\n error.accountProblem === 'credentials'\n ? `its API key was refused (HTTP ${error.status ?? 'unknown'})`\n : 'the account is out of credit',\n },\n })\n } catch (alertError) {\n console.error('[ai] the provider account alert could not be raised', alertError)\n }\n}\n"],"names":["aiCatalogEntry","aiModelCacheMinTokens","AI_IMAGE_MAX_BYTES","AI_IMAGE_MEDIA_TYPES","AI_REQUEST_MAX_IMAGES","AiRequestShapeError","AiUpstreamError","aiBase64Bytes","aiCacheMarkerTtls","aiMessageImages","resolveAiProvider","aiRequestCacheTtl","aiDevReplayMode","aiIsDeployedRuntime","aiLiveBatchWanted","aiUnderLiveRun","aiWarnIfReplayAskedInDeployment","aiAnsweredOutsideBatch","aiCompleteInBatch","AI_UPSTREAM_FAILURE_COPY","AI_MAX_CACHE_BREAKPOINTS","AI_ACCEPTABLE_USE_BLOCK","validateAiSystemBlocks","system","breakpoints","filter","block","cacheBreakpoint","length","lastBreakpoint","reduce","last","index","forEach","volatile","firstSite","findIndex","site","aiConversationCacheFits","aiModelReadsImages","provider","model","models","some","descriptor","id","capabilities","vision","aiRouteReadsImages","settings","NOT_BASE64","isBareBase64","data","body","endsWith","slice","test","validateAiMessages","messages","readsImages","message","content","Array","isArray","part","type","text","role","includes","mediaType","images","AI_CACHE_PREFIX_TOKEN_CHARS","aiCachedPrefixChars","tools","toolChars","total","tool","JSON","stringify","aiCachedPrefixCaches","chars","aiProviderReady","Boolean","readApiKey","aiActiveProvider","providerFor","input","Error","apiKey","apiKeyEnv","runAiRequest","cacheConversation","refuseRetiredModelInLiveRun","stream","_settings","_provider","request","cacheTtl","providerRequest","complete","runInDevelopment","error","reportAccountProblem","successor","mode","batch","completeBatch","dev","lastMarker","ttl","pop","ttlMs","prefix","aiCachedPrefixKey","live","aiDevWarmPrefixFirst","aiDevReplayStream","result","discounted","aiDevCountLive","usage","AI_BATCH_PRICE_FACTOR","aiDevReplayComplete","accountProblem","raiseOperatorAlert","AI_PROVIDER_UNAVAILABLE","Promise","all","dedupeKey","context","label","keyEnv","reason","status","alertError","console"],"mappings":"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"}