@aglyn/plugins-ai 1.0.0-beta.252 → 1.0.0-beta.254

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 (65) hide show
  1. package/package.json +17 -17
  2. package/src/lib/components/ai-site-start-card.component.js +16 -8
  3. package/src/lib/components/ai-site-start-card.component.js.map +1 -1
  4. package/src/lib/jobs/ai-job-form-dataset.d.ts +6 -1
  5. package/src/lib/jobs/ai-job-form-dataset.js +7 -2
  6. package/src/lib/jobs/ai-job-form-dataset.js.map +1 -1
  7. package/src/lib/jobs/ai-job-page-language.d.ts +10 -5
  8. package/src/lib/jobs/ai-job-page-language.js +10 -5
  9. package/src/lib/jobs/ai-job-page-language.js.map +1 -1
  10. package/src/lib/jobs/ai-job-plan-step.d.ts +13 -1
  11. package/src/lib/jobs/ai-job-plan-step.js +27 -8
  12. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  13. package/src/lib/jobs/ai-job-site-content.d.ts +32 -16
  14. package/src/lib/jobs/ai-job-site-content.js +43 -20
  15. package/src/lib/jobs/ai-job-site-content.js.map +1 -1
  16. package/src/lib/jobs/ai-job-site-datasets.d.ts +1 -1
  17. package/src/lib/jobs/ai-job-site-datasets.js +2 -2
  18. package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
  19. package/src/lib/jobs/ai-job-site-step.d.ts +3 -0
  20. package/src/lib/jobs/ai-job-site-step.js +25 -12
  21. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  22. package/src/lib/layout-language/ai-layout-language.js +5 -1
  23. package/src/lib/layout-language/ai-layout-language.js.map +1 -1
  24. package/src/lib/layout-language/ai-layout-links.js +5 -2
  25. package/src/lib/layout-language/ai-layout-links.js.map +1 -1
  26. package/src/lib/model/ai-credit-estimate.d.ts +7 -1
  27. package/src/lib/model/ai-credit-estimate.js +7 -1
  28. package/src/lib/model/ai-credit-estimate.js.map +1 -1
  29. package/src/lib/model/ai-plan-capabilities.d.ts +5 -0
  30. package/src/lib/model/ai-plan-capabilities.js.map +1 -1
  31. package/src/lib/model/ai-site-job.d.ts +32 -12
  32. package/src/lib/model/ai-site-job.js +35 -20
  33. package/src/lib/model/ai-site-job.js.map +1 -1
  34. package/src/lib/model/ai-site-kinds.d.ts +5 -5
  35. package/src/lib/model/ai-site-kinds.js +5 -5
  36. package/src/lib/model/ai-site-kinds.js.map +1 -1
  37. package/src/lib/model/ai-site-start-seo.d.ts +11 -2
  38. package/src/lib/model/ai-site-start-seo.js +29 -15
  39. package/src/lib/model/ai-site-start-seo.js.map +1 -1
  40. package/src/lib/providers/anthropic.js +29 -7
  41. package/src/lib/providers/anthropic.js.map +1 -1
  42. package/src/lib/providers/contract.d.ts +19 -1
  43. package/src/lib/providers/contract.js +10 -0
  44. package/src/lib/providers/contract.js.map +1 -1
  45. package/src/lib/providers/routing.js +24 -4
  46. package/src/lib/providers/routing.js.map +1 -1
  47. package/src/lib/runtime/ai-dev-env.d.ts +25 -4
  48. package/src/lib/runtime/ai-dev-env.js +41 -14
  49. package/src/lib/runtime/ai-dev-env.js.map +1 -1
  50. package/src/lib/runtime/ai-dev-replay.d.ts +4 -0
  51. package/src/lib/runtime/ai-dev-replay.js +4 -0
  52. package/src/lib/runtime/ai-dev-replay.js.map +1 -1
  53. package/src/lib/runtime/ai-doctrine-validators.js +13 -3
  54. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  55. package/src/lib/runtime/ai-eval-live.d.ts +35 -1
  56. package/src/lib/runtime/ai-eval-live.js +46 -2
  57. package/src/lib/runtime/ai-eval-live.js.map +1 -1
  58. package/src/lib/runtime/ai-live-batch.d.ts +20 -4
  59. package/src/lib/runtime/ai-live-batch.js +57 -10
  60. package/src/lib/runtime/ai-live-batch.js.map +1 -1
  61. package/src/lib/runtime/ai-runtime.js +4 -2
  62. package/src/lib/runtime/ai-runtime.js.map +1 -1
  63. package/src/lib/server/ai-media-image.d.ts +5 -1
  64. package/src/lib/server/ai-media-image.js +6 -2
  65. package/src/lib/server/ai-media-image.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\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 },\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 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 },\n 'job.template': {\n thinking: 'off',\n effort: null,\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 },\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 },\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 },\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.\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 },\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 },\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 },\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 },\n 'job.text': {\n thinking: 'adaptive',\n effort: null,\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 thinking: 'adaptive',\n effort: null,\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 },\n 'job.plan': {\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 },\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_ROUTING_TABLE","thinking","effort","maxTokens","maxTokensBasis","eval","kinds","scores","source","passRate","meanScore","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\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 },\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 },\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 },\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 },\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 },\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 },\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 },\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 },\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 },\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 },\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 },\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_ROUTING_TABLE","thinking","effort","maxTokens","maxTokensBasis","eval","kinds","scores","source","passRate","meanScore","aiRouteSettings","row","aiModelForStep","route","Error"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,mBAAmB,EAAEC,qBAAqB,QAAQ,sBAAkB;AAC7E,SACEC,gBAAgB,EAChBC,aAAa,EACbC,cAAc,EACdC,iBAAiB,QAEZ,eAAW;AAGlB,SAASC,cAAc,EAAEC,iBAAiB,QAAQ,gBAAY;AAE9D;;;;;;;;;;;;;CAaC,GAED,kDAAkD,GAClD,OAAO,MAAMC,uBAAuB,mBAAkB;AACtD,kEAAkE,GAClE,OAAO,MAAMC,mBAAmB,eAAc;AAK9C;;;;CAIC,GACD,OAAO,SAASC,iBAAiBC,OAAe;IAC9C,IAAIC,UAAUD;IACd,IAAK,IAAIE,OAAO,GAAGA,OAAOX,iBAAiBY,MAAM,EAAED,QAAQ,EAAG;YAC1CT;QAAlB,MAAMW,aAAYX,kBAAAA,eAAeQ,6BAAfR,gBAAyBW,SAAS;QACpD,IAAI,CAACA,WAAW,OAAOH;QACvBA,UAAUG;IACZ;IACA,OAAOH;AACT;AAOA,wFAAwF,GACxF,SAASI,OAAOC,QAAsC,EAAEC,GAAW;IACjE,MAAMC,QAAQF,4BAAAA,QAAU,CAACC,IAAI;IAC7B,OAAO,OAAOC,UAAU,YAAYA,SAASA,UAAUnB,sBACnDmB,QACAC;AACN;AAEA,2FAA2F,GAC3F,OAAO,SAASC,kBAAkBJ,QAA2B;IAC3D,MAAMK,SAASN,OAAOC,UAAU;IAChC,IAAIK,QAAQ;QACV,MAAMC,QAAQjB,eAAegB;QAC7B,IAAIC,OAAO,OAAOA;IACpB;IACA,OAAOhB;AACT;AAEA;;;;CAIC,GACD,OAAO,SAASiB,eACdC,IAAgB,EAChBR,QAA2B;QAWzBS,MAAAA,OAAAA;QACmCC,+BAC1BA;IAXX,MAAMC,WAAWP,kBAAkBJ;IACnC,IAAI,CAACW,UAAU,OAAOR;IACtB,uEAAuE;IACvE,gDAAgD;IAChD,MAAMM,WAAW,CAACf;YAEEP;QADlB,MAAMyB,UAAUlB,UAAUD,iBAAiBC,WAAWS;QACtD,OAAOS,WAAWzB,EAAAA,kBAAAA,eAAeyB,6BAAfzB,gBAAyBwB,QAAQ,MAAKA,SAASE,EAAE,GAAGD,UAAUT;IAClF;IACA,MAAMW,SACJL,QAAAA,SAAAA,YAAAA,SAASV,OAAOC,UAAUhB,qBAAqB,CAACwB,KAAK,cAArDC,YACCD,SAAS,gBAAgBC,UAASC,gCAAAA,QAAQK,GAAG,CAACvB,iBAAiB,qBAA7BkB,8BAA+BM,IAAI,MAAMb,qBAD5EM,QAEAA,UAASC,oCAAAA,QAAQK,GAAG,CAACxB,qBAAqB,qBAAjCmB,kCAAmCM,IAAI,eAFhDP,OAGArB,kBAAkBuB,SAASE,EAAE,EAAE3B,aAAa,CAACsB,KAAK;IACpD,OAAOM,QAAQ;QAAEH;QAAUG;IAAM,IAAIX;AACvC;AAmCA;;;;;;;;;;;;;;;;CAgBC,GACD,OAAO,MAAMc,mBAA+D;IAC1E,eAAe;QACb,2EAA2E;QAC3EC,UAAU;QACVC,QAAQ;QACRC,WAAW;QACXC,gBACE;QACFC,MAAM;YAAEC,OAAO;gBAAC;aAAO;YAAEC,QAAQ;YAAWC,QAAQ;YAAYC,UAAU;YAAGC,WAAW;QAAO;IACjG;IACA,gBAAgB;QACd,0EAA0E;QAC1E,yEAAyE;QACzE,iCAAiC;QACjCT,UAAU;QACVC,QAAQ;QACRC,WAAW;QACXC,gBAAgB;QAChBC,MAAM;YAAEC,OAAO;gBAAC;aAAU;YAAEC,QAAQ;YAAWC,QAAQ;YAAYC,UAAU;YAAGC,WAAW;QAAO;IACpG;IACA,gBAAgB;QACdT,UAAU;QACVC,QAAQ;QACRC,WAAW;QACXC,gBAAgB;QAChBC,MAAM;YAAEC,OAAO;gBAAC;aAAU;YAAEC,QAAQ;YAAWC,QAAQ;YAAYC,UAAU;YAAGC,WAAW;QAAO;IACpG;IACA,aAAa;QACXT,UAAU;QACVC,QAAQ;QACRC,WAAW;QACXC,gBAAgB;QAChBC,MAAM;YAAEC,OAAO;gBAAC;aAAO;YAAEC,QAAQ;YAAWC,QAAQ;YAAYC,UAAU;YAAGC,WAAW;QAAO;IACjG;IACA,oBAAoB;QAClBT,UAAU;QACVC,QAAQ;QACRC,WAAW;QACXC,gBAAgB;QAChBC,MAAM;YAAEC,OAAO;gBAAC;aAAU;YAAEC,QAAQ;YAAWC,QAAQ;YAAYC,UAAU;YAAGC,WAAW;QAAO;IACpG;IACA,cAAc;QACZ,0FAA0F;QAC1F,yEAAyE;QACzE,wEAAwE;QACxE,yDAAyD;QACzDT,UAAU;QACVC,QAAQ;QACRC,WAAW;QACXC,gBACE;QACFC,MAAM;YAAEC,OAAO;gBAAC;aAAS;YAAEC,QAAQ;YAAWC,QAAQ;YAAYC,UAAU;YAAGC,WAAW;QAAO;IACnG;IACA,gBAAgB;QACd,sEAAsE;QACtE,sEAAsE;QACtE,iBAAiB;QACjBT,UAAU;QACVC,QAAQ;QACRC,WAAW;QACXC,gBACE;QACFC,MAAM;YAAEC,OAAO;gBAAC;aAAW;YAAEC,QAAQ;YAAWC,QAAQ;YAAYC,UAAU;YAAGC,WAAW;QAAO;IACrG;IACA,iBAAiB;QACf,2EAA2E;QAC3E,8DAA8D;QAC9DT,UAAU;QACVC,QAAQ;QACRC,WAAW;QACXC,gBACE;QACFC,MAAM;YAAEC,OAAO;gBAAC;aAAY;YAAEC,QAAQ;YAAWC,QAAQ;YAAYC,UAAU;YAAGC,WAAW;QAAO;IACtG;IACA,WAAW;QACT,0EAA0E;QAC1E,wEAAwE;QACxE,wEAAwE;QACxE,wEAAwE;QACxE,uEAAuE;QACvE,uEAAuE;QACvET,UAAU;QACVC,QAAQ;QACRC,WAAW;QACXC,gBACE;QACFC,MAAM;YAAEC,OAAO;gBAAC;aAAM;YAAEC,QAAQ;YAAWC,QAAQ;YAAYC,UAAU;YAAGC,WAAW;QAAO;IAChG;IACA,kBAAkB;QAChB,yEAAyE;QACzE,uEAAuE;QACvE,0DAA0D;QAC1DT,UAAU;QACVC,QAAQ;QACRC,WAAW;QACXC,gBACE;QACFC,MAAM;YAAEC,OAAO;gBAAC;aAAa;YAAEC,QAAQ;YAAWC,QAAQ;YAAYC,UAAU;YAAGC,WAAW;QAAE;IAClG;IACA,YAAY;QACV,0EAA0E;QAC1E,2EAA2E;QAC3E,2EAA2E;QAC3E,gCAAgC;QAChCT,UAAU;QACVC,QAAQ;QACRC,WAAW;QACXC,gBACE;QACFC,MAAM;YAAEC,OAAO;gBAAC;aAAO;YAAEC,QAAQ;YAAWC,QAAQ;YAAYC,UAAU;YAAGC,WAAW;QAAO;IACjG;IACA,eAAe;QACb,wEAAwE;QACxE,oEAAoE;QACpE,sEAAsE;QACtE,sDAAsD;QACtDT,UAAU;QACVC,QAAQ;QACRC,WAAW;QACXC,gBACE;QACFC,MAAM;YAAEC,OAAO;gBAAC;aAAU;YAAEC,QAAQ;YAAWC,QAAQ;YAAYC,UAAU;YAAGC,WAAW;QAAO;IACpG;IACA,YAAY;QACV,iEAAiE;QACjE,wEAAwE;QACxE,0EAA0E;QAC1E,kEAAkE;QAClE,qEAAqE;QACrE,4EAA4E;QAC5E,qEAAqE;QACrE,2DAA2D;QAC3DT,UAAU;QACVC,QAAQ;QACRC,WAAW;QACXC,gBACE;QACFC,MAAM;YAAEC,OAAO;gBAAC;aAAO;YAAEC,QAAQ;YAAWC,QAAQ;YAAYC,UAAU;YAAGC,WAAW;QAAO;IACjG;IACA,gBAAgB;QACd,qEAAqE;QACrE,2EAA2E;QAC3E,oEAAoE;QACpET,UAAU;QACVC,QAAQ;QACRC,WAAW;QACXC,gBACE;QACFC,MAAM;YACJC,OAAO;gBAAC;gBAAW;gBAAW;aAAa;YAC3CC,QAAQ;YACRC,QAAQ;YACRC,UAAU;YACVC,WAAW;QACb;IACF;IACA,aAAa;QACX,0EAA0E;QAC1E,gEAAgE;QAChE,8CAA8C;QAC9CT,UAAU;QACVC,QAAQ;QACRC,WAAW;QACXC,gBACE;QACFC,MAAM;YAAEC,OAAO;gBAAC;aAAQ;YAAEC,QAAQ;YAAWC,QAAQ;YAAYC,UAAU;YAAGC,WAAW;QAAO;IAClG;IACA,gBAAgB;QACd,wEAAwE;QACxE,wEAAwE;QACxE,wBAAwB;QACxBT,UAAU;QACVC,QAAQ;QACRC,WAAW;QACXC,gBACE;QACFC,MAAM;YAAEC,OAAO;gBAAC;aAAQ;YAAEC,QAAQ;YAAWC,QAAQ;YAAYC,UAAU;YAAGC,WAAW;QAAO;IAClG;IACA,gBAAgB;QACd,yEAAyE;QACzE,wEAAwE;QACxE,8CAA8C;QAC9CT,UAAU;QACVC,QAAQ;QACRC,WAAW;QACXC,gBACE;QACFC,MAAM;YAAEC,OAAO;gBAAC;aAAW;YAAEC,QAAQ;YAAWC,QAAQ;YAAYC,UAAU;YAAGC,WAAW;QAAO;IACrG;IACA,aAAa;QACX,gEAAgE;QAChE,yEAAyE;QACzE,2DAA2D;QAC3DT,UAAU;QACVC,QAAQ;QACRC,WAAW;QACXC,gBACE;QACFC,MAAM;YAAEC,OAAO;gBAAC;aAAQ;YAAEC,QAAQ;YAAWC,QAAQ;YAAYC,UAAU;YAAGC,WAAW;QAAE;IAC7F;IACA,YAAY;QACV,yEAAyE;QACzE,wEAAwE;QACxE,8CAA8C;QAC9CT,UAAU;QACVC,QAAQ;QACRC,WAAW;QACXC,gBACE;QACFC,MAAM;YAAEC,OAAO;gBAAC;aAAO;YAAEC,QAAQ;YAAWC,QAAQ;YAAYC,UAAU;YAAGC,WAAW;QAAE;IAC5F;IACA,WAAW;QACT,yEAAyE;QACzE,yEAAyE;QACzE,0EAA0E;QAC1E,iEAAiE;QACjET,UAAU;QACVC,QAAQ;QACRC,WAAW;QACXC,gBACE;QACFC,MAAM;YAAEC,OAAO;gBAAC;aAAM;YAAEC,QAAQ;YAAWC,QAAQ;YAAYC,UAAU;YAAGC,WAAW;QAAE;IAC3F;IACA,YAAY;QACV,uEAAuE;QACvE,6DAA6D;QAC7DT,UAAU;QACVC,QAAQ;QACRC,WAAW;QACXC,gBAAgB;QAChBC,MAAM;YAAEC,OAAO;gBAAC;aAAO;YAAEC,QAAQ;YAAWC,QAAQ;YAAYC,UAAU;YAAGC,WAAW;QAAE;IAC5F;IACA,aAAa;QACX,wEAAwE;QACxE,yEAAyE;QACzE,oEAAoE;QACpET,UAAU;QACVC,QAAQ;QACRC,WAAW;QACXC,gBACE;QACFC,MAAM;YAAEC,OAAO;gBAAC;aAAQ;YAAEC,QAAQ;YAAWC,QAAQ;YAAYC,UAAU;YAAGC,WAAW;QAAO;IAClG;IACA,YAAY;QACV,yEAAyE;QACzE,2EAA2E;QAC3E,wEAAwE;QACxE,yEAAyE;QACzE,0EAA0E;QAC1E,0EAA0E;QAC1ET,UAAU;QACVC,QAAQ;QACRC,WAAW;QACXC,gBAAgB;QAChBC,MAAM;YACJC,OAAO;gBAAC;gBAAQ;gBAAY;gBAAa;gBAAU;aAAO;YAC1DC,QAAQ;YACRC,QAAQ;YACRC,UAAU;YACVC,WAAW;QACb;IACF;AACF,EAAC;AAED;;;;CAIC,GACD,OAAO,SAASC,gBAAgBpB,IAAgB;IAC9C,MAAMqB,MAAMZ,gBAAgB,CAACT,KAAK;IAClC,OAAO,aACDqB,IAAIX,QAAQ,GAAG;QAAEA,UAAUW,IAAIX,QAAQ;IAAC,IAAI,CAAC,GAC7CW,IAAIV,MAAM,GAAG;QAAEA,QAAQU,IAAIV,MAAM;IAAC,IAAI,CAAC;AAE/C;AAEA,uEAAuE,GACvE,OAAO,SAASW,eAAetB,IAAgB,EAAER,QAA2B;IAC1E,MAAM+B,QAAQxB,eAAeC,MAAMR;IACnC,IAAI,CAAC+B,OAAO;QACV,mEAAmE;QACnE,sEAAsE;QACtE,MAAM,IAAIC,MAAM;IAClB;IACA,OAAOD,MAAMjB,KAAK;AACpB"}
@@ -69,11 +69,25 @@ export declare const AI_LIVE_BATCH_ENV = "AGLYN_LIVE_AI_BATCH";
69
69
  * iteration wants back in seconds.
70
70
  */
71
71
  export declare function aiLiveBatchWanted(env?: AiDevEnv): boolean;
72
+ /** The variable that bounds how long a live run waits on one batch (AGL-3781). */
73
+ export declare const AI_LIVE_BATCH_WAIT_ENV = "AGLYN_LIVE_AI_BATCH_WAIT_MS";
74
+ /** How long a batch is waited on when the run names no wait. */
75
+ export declare const AI_LIVE_BATCH_DEFAULT_WAIT_MS: number;
72
76
  /**
73
- * The most a batched live run may take: the adapter waits up to an hour on
74
- * each batch, and a run's requests go as a few batches one after another —
75
- * the answers, their re-asks, the grades. A spec that timed out would keep
76
- * none of the answers it already paid for, so the bound is the worst case.
77
+ * How long a live run waits on one batch (AGL-3781): what
78
+ * `AGLYN_LIVE_AI_BATCH_WAIT_MS` says, or half an hour. Most batches end in
79
+ * minutes. One that has not by then is canceled, which keeps every answer it
80
+ * already has, and the requests it left unanswered are asked on their own at
81
+ * full price — so a slow batch costs a round part of its discount, never the
82
+ * round.
83
+ */
84
+ export declare function aiLiveBatchWaitMs(env?: AiDevEnv): number;
85
+ /**
86
+ * The most a batched live run may take: a run's requests go as a few batches
87
+ * one after another — the answers, their re-asks, the grades — and each is
88
+ * waited on for `aiLiveBatchWaitMs`, then given time to end its cancel and to
89
+ * have what it left asked on its own. A spec that timed out would keep none
90
+ * of the answers it already paid for, so the bound is the worst case.
77
91
  */
78
92
  export declare const AI_LIVE_BATCH_TIMEOUT_MS: number;
79
93
  /** A live spec's timeout: `baseMs`, or the batched run's when this run batches. */
@@ -85,5 +99,12 @@ export declare function aiLiveTimeoutMs(baseMs: number, env?: AiDevEnv): number;
85
99
  * runs them until a recorded round shows a lower effort holds.
86
100
  */
87
101
  export declare const AI_EFFORT_ARM_ENV = "AGLYN_AI_EFFORT";
102
+ /**
103
+ * Every step kind this development run's A/B arm names, with its effort, in
104
+ * kind order — `*` for every kind it does not name. A pair whose effort is no
105
+ * level is ignored; a kind named twice keeps its first effort, and `*` its
106
+ * last. Empty when the run names no arm, and in any deployed server.
107
+ */
108
+ export declare function aiDevEffortArm(env?: AiDevEnv): Record<string, AiEffort>;
88
109
  /** The effort this development run's A/B arm asks `step` at, or `undefined` for the step's own. */
89
110
  export declare function aiDevEffortFor(step: string | undefined, env?: AiDevEnv): AiEffort | undefined;
@@ -87,14 +87,33 @@ export function aiWarnIfReplayAskedInDeployment(env = process.env) {
87
87
  if (/^(?:0|false|no|off)$/i.test(raw)) return false;
88
88
  return aiUnderLiveRun(env);
89
89
  }
90
+ /** The variable that bounds how long a live run waits on one batch (AGL-3781). */ export const AI_LIVE_BATCH_WAIT_ENV = 'AGLYN_LIVE_AI_BATCH_WAIT_MS';
91
+ /** How long a batch is waited on when the run names no wait. */ export const AI_LIVE_BATCH_DEFAULT_WAIT_MS = 30 * 60000;
90
92
  /**
91
- * The most a batched live run may take: the adapter waits up to an hour on
92
- * each batch, and a run's requests go as a few batches one after another —
93
- * the answers, their re-asks, the grades. A spec that timed out would keep
94
- * none of the answers it already paid for, so the bound is the worst case.
93
+ * How long a live run waits on one batch (AGL-3781): what
94
+ * `AGLYN_LIVE_AI_BATCH_WAIT_MS` says, or half an hour. Most batches end in
95
+ * minutes. One that has not by then is canceled, which keeps every answer it
96
+ * already has, and the requests it left unanswered are asked on their own at
97
+ * full price — so a slow batch costs a round part of its discount, never the
98
+ * round.
99
+ */ export function aiLiveBatchWaitMs(env = process.env) {
100
+ var _env_AI_LIVE_BATCH_WAIT_ENV;
101
+ const raw = String((_env_AI_LIVE_BATCH_WAIT_ENV = env[AI_LIVE_BATCH_WAIT_ENV]) != null ? _env_AI_LIVE_BATCH_WAIT_ENV : '').trim();
102
+ const parsed = raw ? Number(raw) : Number.NaN;
103
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : AI_LIVE_BATCH_DEFAULT_WAIT_MS;
104
+ }
105
+ /**
106
+ * The most a batched live run may take: a run's requests go as a few batches
107
+ * one after another — the answers, their re-asks, the grades — and each is
108
+ * waited on for `aiLiveBatchWaitMs`, then given time to end its cancel and to
109
+ * have what it left asked on its own. A spec that timed out would keep none
110
+ * of the answers it already paid for, so the bound is the worst case.
95
111
  */ export const AI_LIVE_BATCH_TIMEOUT_MS = 5 * 60 * 60000;
96
112
  /** A live spec's timeout: `baseMs`, or the batched run's when this run batches. */ export function aiLiveTimeoutMs(baseMs, env = process.env) {
97
- return aiLiveBatchWanted(env) ? Math.max(baseMs, AI_LIVE_BATCH_TIMEOUT_MS) : baseMs;
113
+ if (!aiLiveBatchWanted(env)) return baseMs;
114
+ // Four batches in a row, each waited on, canceled and finished by hand.
115
+ const longWait = 4 * (aiLiveBatchWaitMs(env) + 20 * 60000);
116
+ return Math.max(baseMs, AI_LIVE_BATCH_TIMEOUT_MS, longWait);
98
117
  }
99
118
  /**
100
119
  * The variable an effort A/B names its arm with (AGL-3781): a step kind's
@@ -107,19 +126,27 @@ const AI_EFFORT_LEVELS = [
107
126
  'medium',
108
127
  'high'
109
128
  ];
110
- /** The effort this development run's A/B arm asks `step` at, or `undefined` for the step's own. */ export function aiDevEffortFor(step, env = process.env) {
129
+ /**
130
+ * Every step kind this development run's A/B arm names, with its effort, in
131
+ * kind order — `*` for every kind it does not name. A pair whose effort is no
132
+ * level is ignored; a kind named twice keeps its first effort, and `*` its
133
+ * last. Empty when the run names no arm, and in any deployed server.
134
+ */ export function aiDevEffortArm(env = process.env) {
111
135
  var _env_AI_EFFORT_ARM_ENV;
112
- if (aiIsDeployedRuntime(env)) return undefined;
136
+ if (aiIsDeployedRuntime(env)) return {};
113
137
  const raw = String((_env_AI_EFFORT_ARM_ENV = env[AI_EFFORT_ARM_ENV]) != null ? _env_AI_EFFORT_ARM_ENV : '').trim();
114
- if (!raw) return undefined;
115
- let fallback;
116
- for (const pair of raw.split(',')){
138
+ const arm = {};
139
+ for (const pair of raw ? raw.split(',') : []){
117
140
  const [kind, level] = pair.split(':').map((part)=>part.trim());
118
- if (!AI_EFFORT_LEVELS.includes(level)) continue;
119
- if (kind === step) return level;
120
- if (kind === '*') fallback = level;
141
+ if (!kind || !AI_EFFORT_LEVELS.includes(level)) continue;
142
+ if (kind === '*' || !(kind in arm)) arm[kind] = level;
121
143
  }
122
- return fallback;
144
+ return Object.fromEntries(Object.entries(arm).sort(([a], [b])=>a < b ? -1 : a > b ? 1 : 0));
145
+ }
146
+ /** The effort this development run's A/B arm asks `step` at, or `undefined` for the step's own. */ export function aiDevEffortFor(step, env = process.env) {
147
+ var _ref;
148
+ const arm = aiDevEffortArm(env);
149
+ return (_ref = step !== undefined && step !== '*' ? arm[step] : undefined) != null ? _ref : arm['*'];
123
150
  }
124
151
 
125
152
  //# sourceMappingURL=ai-dev-env.js.map
@@ -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. Production keeps the default — its\n * steps run a minute apart on the beat, inside five minutes — and never reads\n * this: `undefined` there.\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/** 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/**\n * The most a batched live run may take: the adapter waits up to an hour on\n * each batch, and a run's requests go as a few batches one after another —\n * the answers, their re-asks, the grades. A spec that timed out would keep\n * none 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 return aiLiveBatchWanted(env) ? Math.max(baseMs, AI_LIVE_BATCH_TIMEOUT_MS) : baseMs\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/** 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 if (aiIsDeployedRuntime(env)) return undefined\n const raw = String(env[AI_EFFORT_ARM_ENV] ?? '').trim()\n if (!raw) return undefined\n let fallback: AiEffort | undefined\n for (const pair of raw.split(',')) {\n const [kind, level] = pair.split(':').map((part) => part.trim())\n if (!AI_EFFORT_LEVELS.includes(level as AiEffort)) continue\n if (kind === step) return level as AiEffort\n if (kind === '*') fallback = level as AiEffort\n }\n return fallback\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","warnedDeployed","aiWarnIfReplayAskedInDeployment","console","error","AI_LIVE_BATCH_ENV","aiLiveBatchWanted","test","AI_LIVE_BATCH_TIMEOUT_MS","aiLiveTimeoutMs","baseMs","Math","max","AI_EFFORT_ARM_ENV","AI_EFFORT_LEVELS","aiDevEffortFor","step","fallback","pair","split","kind","level","map","part","includes"],"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 * breakpoints ask for the hour: the second run reads the prefix the first one\n * wrote instead of writing it again. Production keeps the default — its\n * steps run a minute apart on the beat, inside five minutes — and never reads\n * this: `undefined` there.\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/** 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","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":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;CAOC,GAMD,qEAAqE,GACrE,OAAO,MAAMA,gBAAgB,kBAAiB;AAC9C,oFAAoF,GACpF,OAAO,MAAMC,oBAAoB,sBAAqB;AACtD,wEAAwE,GACxE,OAAO,MAAMC,uBAAuB,qBAAoB;AACxD,8EAA8E,GAC9E,OAAO,MAAMC,uBAAuB,wBAAuB;AAI3D,mFAAmF,GACnF,OAAO,SAASC,oBAAoBC,MAAgBC,QAAQD,GAAG;IAC7D,OACEA,GAAG,CAAC,WAAW,KAAK,gBACpBA,GAAG,CAAC,aAAa,KAAK,gBACtBA,GAAG,CAAC,aAAa,KAAK,aACtBE,QAAQF,GAAG,CAAC,YAAY;AAE5B;AAEA,kDAAkD,GAClD,OAAO,MAAMG,kBAAkB,gBAAe;AAE9C;;;;;;;CAOC,GACD,OAAO,SAASC,eAAeJ,MAAgBC,QAAQD,GAAG;QAEJA;IADpD,IAAID,oBAAoBC,MAAM,OAAO;IACrC,OAAOE,QAAQF,GAAG,CAACF,qBAAqB,KAAKO,QAAOL,uBAAAA,GAAG,CAACG,gBAAgB,YAApBH,uBAAwB,IAAIM,IAAI,OAAO;AAC7F;AAEA,2EAA2E,GAC3E,OAAO,SAASC,gBAAgBP,MAAgBC,QAAQD,GAAG;QAEtCA;IADnB,IAAID,oBAAoBC,MAAM,OAAO;IACrC,MAAMQ,MAAMH,QAAOL,qBAAAA,GAAG,CAACL,cAAc,YAAlBK,qBAAsB,IAAIM,IAAI,GAAGG,WAAW;IAC/D,IAAID,QAAQ,WAAW,OAAO;IAC9B,IAAIA,QAAQ,OAAOA,QAAQ,QAAQA,QAAQ,UAAUA,QAAQ,UAAU,OAAO;IAC9E,IAAIA,QAAQ,OAAOA,QAAQ,SAASA,QAAQ,SAAS,OAAO;IAC5D,OAAOJ,eAAeJ,OAAO,WAAW;AAC1C;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASU,cAAcV,MAAgBC,QAAQD,GAAG;QAEpCA;IADnB,IAAID,oBAAoBC,MAAM,OAAOW;IACrC,MAAMH,MAAMH,QAAOL,4BAAAA,GAAG,CAACH,qBAAqB,YAAzBG,4BAA6B,IAAIM,IAAI,GAAGG,WAAW;IACtE,IAAID,QAAQ,MAAM,OAAO;IACzB,IAAIA,QAAQ,MAAM,OAAOG;IACzB,OAAOP,eAAeJ,OAAO,OAAOW;AACtC;AAEA,sFAAsF,GACtF,IAAIC,iBAAiB;AACrB,OAAO,SAASC,gCAAgCb,MAAgBC,QAAQD,GAAG;IACzE,IAAIY,kBAAkB,CAACb,oBAAoBC,MAAM;IACjD,IAAI,CAACA,GAAG,CAACL,cAAc,IAAI,CAACK,GAAG,CAACH,qBAAqB,EAAE;IACvDe,iBAAiB;IACjBE,QAAQC,KAAK,CACX,CAAC,KAAK,EAAEpB,cAAc,GAAG,EAAEE,qBAAqB,0DAA0D,CAAC;AAE/G;AAEA,wEAAwE,GACxE,OAAO,MAAMmB,oBAAoB,sBAAqB;AAEtD;;;;;;CAMC,GACD,OAAO,SAASC,kBAAkBjB,MAAgBC,QAAQD,GAAG;QAExCA;IADnB,IAAID,oBAAoBC,MAAM,OAAO;IACrC,MAAMQ,MAAMH,QAAOL,yBAAAA,GAAG,CAACgB,kBAAkB,YAAtBhB,yBAA0B,IAAIM,IAAI;IACrD,IAAI,uBAAuBY,IAAI,CAACV,MAAM,OAAO;IAC7C,IAAI,wBAAwBU,IAAI,CAACV,MAAM,OAAO;IAC9C,OAAOJ,eAAeJ;AACxB;AAEA,gFAAgF,GAChF,OAAO,MAAMmB,yBAAyB,8BAA6B;AAEnE,8DAA8D,GAC9D,OAAO,MAAMC,gCAAgC,KAAK,MAAM;AAExD;;;;;;;CAOC,GACD,OAAO,SAASC,kBAAkBrB,MAAgBC,QAAQD,GAAG;QACxCA;IAAnB,MAAMQ,MAAMH,QAAOL,8BAAAA,GAAG,CAACmB,uBAAuB,YAA3BnB,8BAA+B,IAAIM,IAAI;IAC1D,MAAMgB,SAASd,MAAMe,OAAOf,OAAOe,OAAOC,GAAG;IAC7C,OAAOD,OAAOE,QAAQ,CAACH,WAAWA,UAAU,IAAIA,SAASF;AAC3D;AAEA;;;;;;CAMC,GACD,OAAO,MAAMM,2BAA2B,IAAI,KAAK,MAAM;AAEvD,iFAAiF,GACjF,OAAO,SAASC,gBAAgBC,MAAc,EAAE5B,MAAgBC,QAAQD,GAAG;IACzE,IAAI,CAACiB,kBAAkBjB,MAAM,OAAO4B;IACpC,wEAAwE;IACxE,MAAMC,WAAW,IAAKR,CAAAA,kBAAkBrB,OAAO,KAAK,KAAK;IACzD,OAAO8B,KAAKC,GAAG,CAACH,QAAQF,0BAA0BG;AACpD;AAEA;;;;;CAKC,GACD,OAAO,MAAMG,oBAAoB,kBAAiB;AAElD,MAAMC,mBAAwC;IAAC;IAAO;IAAU;CAAO;AAEvE;;;;;CAKC,GACD,OAAO,SAASC,eAAelC,MAAgBC,QAAQD,GAAG;QAErCA;IADnB,IAAID,oBAAoBC,MAAM,OAAO,CAAC;IACtC,MAAMQ,MAAMH,QAAOL,yBAAAA,GAAG,CAACgC,kBAAkB,YAAtBhC,yBAA0B,IAAIM,IAAI;IACrD,MAAM6B,MAAgC,CAAC;IACvC,KAAK,MAAMC,QAAQ5B,MAAMA,IAAI6B,KAAK,CAAC,OAAO,EAAE,CAAE;QAC5C,MAAM,CAACC,MAAMC,MAAM,GAAGH,KAAKC,KAAK,CAAC,KAAKG,GAAG,CAAC,CAACC,OAASA,KAAKnC,IAAI;QAC7D,IAAI,CAACgC,QAAQ,CAACL,iBAAiBS,QAAQ,CAACH,QAAoB;QAC5D,IAAID,SAAS,OAAO,CAAEA,CAAAA,QAAQH,GAAE,GAAIA,GAAG,CAACG,KAAK,GAAGC;IAClD;IACA,OAAOI,OAAOC,WAAW,CAACD,OAAOE,OAAO,CAACV,KAAKW,IAAI,CAAC,CAAC,CAACC,EAAE,EAAE,CAACC,EAAE,GAAMD,IAAIC,IAAI,CAAC,IAAID,IAAIC,IAAI,IAAI;AAC7F;AAEA,iGAAiG,GACjG,OAAO,SAASC,eAAeC,IAAwB,EAAElD,MAAgBC,QAAQD,GAAG;QAE1EkD;IADR,MAAMf,MAAMD,eAAelC;IAC3B,QAAQkD,OAAAA,SAASvC,aAAauC,SAAS,MAAMf,GAAG,CAACe,KAAK,GAAGvC,qBAAjDuC,OAA+Df,GAAG,CAAC,IAAI;AACjF"}
@@ -108,9 +108,13 @@ export declare function resetAiDevWarmPrefixes(): void;
108
108
  * the live ones cost at list, and how many batches carried them. A table
109
109
  * whose `live` is 0 proved nothing new — every prompt in it had been answered
110
110
  * before — and is not a live pass; `AGLYN_AI_REPLAY=refresh` asks again.
111
+ * `batchedRequests` are the ones a batch answered, at half price;
112
+ * `batchFallbacks` are the ones a batch left unanswered past its wait and
113
+ * that were asked on their own, at full price (AGL-3781). Both count in `live`.
111
114
  */
112
115
  export declare function aiLiveRunLedger(env?: Env): AiDevReplayStats & {
113
116
  batches: number;
114
117
  batchedRequests: number;
118
+ batchFallbacks: number;
115
119
  allReplayed: boolean;
116
120
  };
@@ -300,12 +300,16 @@ export async function aiDevWarmPrefixFirst(prefixKey, ttlMs, send, now = Date.no
300
300
  * the live ones cost at list, and how many batches carried them. A table
301
301
  * whose `live` is 0 proved nothing new — every prompt in it had been answered
302
302
  * before — and is not a live pass; `AGLYN_AI_REPLAY=refresh` asks again.
303
+ * `batchedRequests` are the ones a batch answered, at half price;
304
+ * `batchFallbacks` are the ones a batch left unanswered past its wait and
305
+ * that were asked on their own, at full price (AGL-3781). Both count in `live`.
303
306
  */ export function aiLiveRunLedger(env = process.env) {
304
307
  const replay = aiDevReplayStats(env);
305
308
  const batch = aiLiveBatchStats();
306
309
  return _extends({}, replay, {
307
310
  batches: batch.batches,
308
311
  batchedRequests: batch.requests,
312
+ batchFallbacks: batch.fallbacks,
309
313
  allReplayed: replay.live === 0 && replay.replayed > 0
310
314
  });
311
315
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/runtime/ai-dev-replay.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 { createHash } from 'node:crypto'\nimport { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'\nimport { dirname, join } from 'node:path'\nimport { estimateAiProviderCostUsd } from '../providers/catalog'\nimport type {\n AiProvider,\n AiProviderRequest,\n AiResult,\n AiStreamEvent,\n AiSystemBlock,\n AiTool,\n} from '../providers/contract'\nimport { aiLiveBatchStats } from './ai-live-batch'\nimport { AI_REPLAY_DIR_ENV, aiDevReplayMode, type AiDevEnv as Env, type AiDevReplayMode } from './ai-dev-env'\n\n/** The key format's version: bumped when what a key covers changes, so no old answer is read under a new meaning. */\nconst KEY_VERSION = 1\n\n/**\n * THE DEVELOPMENT REPLAY CACHE (AGL-3660).\n *\n * Almost every dollar the provider bills in a month is a development run: a\n * live eval, the layout screenshots fed by one, a local guided start. Most of\n * the requests in a second run of the same eval are byte-identical to the\n * first — the change under test was to a compiler, a check or one prompt, not\n * to all of them — and each identical request bought the same sample twice.\n *\n * So, outside production only, the runtime keys each request by a hash of\n * everything that decides the answer — the provider, the model, the system\n * blocks, the tools, the messages, the ceiling, thinking and effort — and keeps\n * the provider's answer under `.cache/ai-replay/` (gitignored). The next\n * identical request is answered from disk for nothing; a request whose prompt\n * changed by one byte has a new key and goes to the provider. A replayed answer\n * is a real answer the same model gave to the same bytes, so an eval that\n * passes on replays has still seen every CHANGED prompt answered live: the\n * replay never stands in for a prompt the provider has not answered.\n *\n * `AGLYN_AI_REPLAY`:\n * - `1` / `on`: replay what is recorded, record what is not.\n * - `refresh`: every request live, its answer recorded over the old one.\n * - `off` / `0`: nothing read, nothing written.\n * - unset: on under the live-eval launcher (`AI_EVAL_LIVE_LAUNCHER`), off\n * everywhere else, so a dev server or a spec that never asked for it\n * behaves exactly as before.\n *\n * It never runs in a deployed server. `NODE_ENV=production`, `VERCEL_ENV`\n * of `production` or `preview`, or a Cloud Run service (`K_SERVICE`) turn it\n * off whatever the variable says, and the first request says so in the log:\n * a recorded answer handed to a customer would be another customer's page.\n *\n * Every live eval's table carries `aiDevReplayStats()`, so a run that was\n * all replays reads as one: `live: 0` is not a live pass.\n */\n\n// ── The key ──────────────────────────────────────────────────────────────\n\n/** JSON with every object's keys in sorted order: the same value, the same bytes, whatever built it. */\nexport function aiCanonicalJson(value: unknown): string {\n return JSON.stringify(sortKeys(value))\n}\n\nfunction sortKeys(value: unknown): unknown {\n if (Array.isArray(value)) return value.map(sortKeys)\n if (value && typeof value === 'object') {\n return Object.fromEntries(\n Object.keys(value as Record<string, unknown>)\n .sort()\n .filter((key) => (value as Record<string, unknown>)[key] !== undefined)\n .map((key) => [key, sortKeys((value as Record<string, unknown>)[key])]),\n )\n }\n return value\n}\n\n/** What a request's answer depends on. The key, the signal and the cache lifetime are not part of it. */\nexport interface AiReplayRequest {\n model: string\n system: readonly AiSystemBlock[]\n messages: AiProviderRequest['messages']\n tools?: readonly AiTool[]\n maxTokens: number\n thinking?: AiProviderRequest['thinking']\n effort?: AiProviderRequest['effort']\n}\n\n/** The request's replay key: a SHA-256 of everything that decides the answer. */\nexport function aiDevReplayKey(providerId: string, stream: boolean, request: AiReplayRequest): string {\n return createHash('sha256')\n .update(\n aiCanonicalJson({\n v: KEY_VERSION,\n provider: providerId,\n stream,\n model: request.model,\n // A breakpoint changes no answer, so the key reads the text alone.\n system: request.system.map((block) => block.text),\n tools: request.tools ?? [],\n messages: request.messages,\n maxTokens: request.maxTokens,\n thinking: request.thinking ?? null,\n effort: request.effort ?? null,\n }),\n )\n .digest('hex')\n}\n\n/**\n * The prefix a provider caches: the model, the tools and every system block\n * through the last breakpoint. Two requests with the same prefix key read one\n * cache entry; `null` when the request marks no breakpoint.\n */\nexport function aiCachedPrefixKey(request: Pick<AiReplayRequest, 'model' | 'system' | 'tools'>): string | null {\n const last = request.system.map((block) => Boolean(block.cacheBreakpoint)).lastIndexOf(true)\n if (last < 0) return null\n return createHash('sha256')\n .update(\n aiCanonicalJson({\n model: request.model,\n tools: request.tools ?? [],\n system: request.system.slice(0, last + 1).map((block) => block.text),\n }),\n )\n .digest('hex')\n}\n\n// ── The store ────────────────────────────────────────────────────────────\n\n/** The repository root: the nearest directory up from here holding `nx.json`, else the working directory. */\nfunction repositoryRoot(start: string = process.cwd()): string {\n let dir = start\n for (;;) {\n if (existsSync(join(dir, 'nx.json'))) return dir\n const parent = dirname(dir)\n if (parent === dir) return start\n dir = parent\n }\n}\n\n/** Where recorded answers are kept. */\nexport function aiDevReplayDir(env: Env = process.env): string {\n return env[AI_REPLAY_DIR_ENV] || join(repositoryRoot(), '.cache', 'ai-replay')\n}\n\ntype Recording =\n | { v: number; key: string; model: string; recordedAt: string; kind: 'result'; result: AiResult }\n | { v: number; key: string; model: string; recordedAt: string; kind: 'stream'; events: AiStreamEvent[] }\n\nfunction pathFor(dir: string, key: string): string {\n return join(dir, key.slice(0, 2), `${key}.json`)\n}\n\nfunction readRecording(dir: string, key: string): Recording | null {\n const path = pathFor(dir, key)\n if (!existsSync(path)) return null\n try {\n const parsed = JSON.parse(readFileSync(path, 'utf8')) as Recording\n return parsed?.v === KEY_VERSION && parsed.key === key ? parsed : null\n } catch {\n return null\n }\n}\n\nfunction writeRecording(dir: string, recording: Recording): void {\n const path = pathFor(dir, recording.key)\n mkdirSync(dirname(path), { recursive: true })\n // Written aside and renamed, so a run killed mid-write leaves no half file to replay.\n const temp = `${path}.${process.pid}.tmp`\n writeFileSync(temp, JSON.stringify(recording))\n renameSync(temp, path)\n}\n\n// ── What a run did ───────────────────────────────────────────────────────\n\n/** How many of this process's requests were sent, and how many were answered from disk. */\nexport interface AiDevReplayStats {\n mode: AiDevReplayMode\n /** Requests sent to the provider. */\n live: number\n /** Requests answered from a recording. */\n replayed: number\n /** What the live requests cost at the provider's list rates. */\n liveProviderUsd: number\n /** What the replayed ones would have cost, had they been sent. */\n savedProviderUsd: number\n}\n\nconst stats: AiDevReplayStats = {\n mode: 'off',\n live: 0,\n replayed: 0,\n liveProviderUsd: 0,\n savedProviderUsd: 0,\n}\n\n/** This process's count so far: what a live eval prints beside its table. */\nexport function aiDevReplayStats(env: Env = process.env): AiDevReplayStats {\n return {\n ...stats,\n mode: aiDevReplayMode(env),\n liveProviderUsd: round(stats.liveProviderUsd),\n savedProviderUsd: round(stats.savedProviderUsd),\n }\n}\n\n/** Starts the count again; a spec that runs two rounds reports each. */\nexport function resetAiDevReplayStats(): void {\n stats.live = 0\n stats.replayed = 0\n stats.liveProviderUsd = 0\n stats.savedProviderUsd = 0\n}\n\nfunction round(usd: number): number {\n return Math.round(usd * 1_000_000) / 1_000_000\n}\n\nfunction usageCost(usage: AiResult['usage'], model: string): number {\n return estimateAiProviderCostUsd(usage, model)\n}\n\n/** Counts one request a live path sent, at what the provider billed for it; `discount` is a batch's half. */\nexport function aiDevCountLive(usage: AiResult['usage'], model: string, discount = 1): void {\n stats.live += 1\n stats.liveProviderUsd += usageCost(usage, model) * discount\n}\n\n// ── The seam ─────────────────────────────────────────────────────────────\n\nasync function* replayEvents(events: readonly AiStreamEvent[]): AsyncGenerator<AiStreamEvent> {\n for (const event of events) yield event\n}\n\n/** A live stream passed through as it arrives, and recorded once it is done. */\nasync function* recordEvents(\n source: AsyncIterable<AiStreamEvent>,\n onDone: (events: AiStreamEvent[]) => void,\n): AsyncGenerator<AiStreamEvent> {\n const events: AiStreamEvent[] = []\n let completed = false\n for await (const event of source) {\n events.push(event)\n if (event.type === 'done') completed = true\n yield event\n }\n // A stream cut short, or one that carried a provider error, is not an\n // answer worth keeping: the next run asks again.\n if (completed && !events.some((event) => event.type === 'error')) onDone(events)\n}\n\n/**\n * One request through the replay cache: answered from disk when recorded and\n * the mode replays, otherwise sent through `send` and recorded. `send` is the\n * live path — the provider, or the batch queue — and counts its own spend.\n */\nexport async function aiDevReplayComplete(\n provider: Pick<AiProvider, 'id'>,\n request: AiReplayRequest,\n mode: Exclude<AiDevReplayMode, 'off'>,\n send: () => Promise<AiResult>,\n env: Env = process.env,\n): Promise<AiResult> {\n const dir = aiDevReplayDir(env)\n const key = aiDevReplayKey(provider.id, false, request)\n const recorded = mode === 'replay' ? readRecording(dir, key) : null\n if (recorded?.kind === 'result') {\n stats.replayed += 1\n stats.savedProviderUsd += usageCost(recorded.result.usage, request.model)\n return recorded.result\n }\n const result = await send()\n writeRecording(dir, {\n v: KEY_VERSION,\n key,\n model: request.model,\n recordedAt: new Date().toISOString(),\n kind: 'result',\n result,\n })\n return result\n}\n\n/** The streaming half of {@link aiDevReplayComplete}. */\nexport async function aiDevReplayStream(\n provider: Pick<AiProvider, 'id'>,\n request: AiReplayRequest,\n mode: Exclude<AiDevReplayMode, 'off'>,\n send: () => Promise<AsyncIterable<AiStreamEvent>>,\n env: Env = process.env,\n): Promise<AsyncIterable<AiStreamEvent>> {\n const dir = aiDevReplayDir(env)\n const key = aiDevReplayKey(provider.id, true, request)\n const recorded = mode === 'replay' ? readRecording(dir, key) : null\n if (recorded?.kind === 'stream') {\n stats.replayed += 1\n const done = recorded.events.find((event) => event.type === 'done')\n if (done?.type === 'done') stats.savedProviderUsd += usageCost(done.usage, request.model)\n return replayEvents(recorded.events)\n }\n const live = await send()\n return recordEvents(live, (events) => {\n const done = events.find((event) => event.type === 'done')\n if (done?.type === 'done') aiDevCountLive(done.usage, request.model)\n writeRecording(dir, {\n v: KEY_VERSION,\n key,\n model: request.model,\n recordedAt: new Date().toISOString(),\n kind: 'stream',\n events,\n })\n })\n}\n\n// ── Warming a shared prefix before a fan-out ─────────────────────────────\n\n/**\n * A cache entry is readable only once the request that writes it has started\n * answering, so N requests sent at once over one prefix all pay to write it\n * and none reads it — which is exactly what a live eval's `Promise.all` over\n * its briefs did. In a development run the first request over a prefix goes\n * alone; the others wait for it and then read what it wrote. A prefix\n * answered within the cache's lifetime is not held again.\n *\n * Development only, like everything here: production's beat runs one step at\n * a time, and holding a customer's request behind another's would trade\n * their seconds for our cents.\n */\nconst warming = new Map<string, Promise<unknown>>()\nconst warmedAt = new Map<string, number>()\n\nexport async function aiDevWarmPrefixFirst<T>(\n prefixKey: string | null,\n ttlMs: number,\n send: () => Promise<T>,\n now: () => number = Date.now,\n): Promise<T> {\n if (!prefixKey) return send()\n const warmed = warmedAt.get(prefixKey)\n if (warmed !== undefined && now() - warmed < ttlMs) return send()\n const pending = warming.get(prefixKey)\n if (pending) {\n await pending.catch(() => undefined)\n return send()\n }\n const first = send()\n warming.set(prefixKey, first)\n try {\n const result = await first\n warmedAt.set(prefixKey, now())\n return result\n } finally {\n warming.delete(prefixKey)\n }\n}\n\n/** Forgets every warmed prefix; for specs. */\nexport function resetAiDevWarmPrefixes(): void {\n warming.clear()\n warmedAt.clear()\n}\n\n// ── What a live eval prints ──────────────────────────────────────────────\n\n/**\n * The line every live eval's table carries (AGL-3660): how many of its\n * requests went to the provider and how many were answered from disk, what\n * the live ones cost at list, and how many batches carried them. A table\n * whose `live` is 0 proved nothing new — every prompt in it had been answered\n * before — and is not a live pass; `AGLYN_AI_REPLAY=refresh` asks again.\n */\nexport function aiLiveRunLedger(env: Env = process.env): AiDevReplayStats & {\n batches: number\n batchedRequests: number\n allReplayed: boolean\n} {\n const replay = aiDevReplayStats(env)\n const batch = aiLiveBatchStats()\n return {\n ...replay,\n batches: batch.batches,\n batchedRequests: batch.requests,\n allReplayed: replay.live === 0 && replay.replayed > 0,\n }\n}\n"],"names":["createHash","existsSync","mkdirSync","readFileSync","renameSync","writeFileSync","dirname","join","estimateAiProviderCostUsd","aiLiveBatchStats","AI_REPLAY_DIR_ENV","aiDevReplayMode","KEY_VERSION","aiCanonicalJson","value","JSON","stringify","sortKeys","Array","isArray","map","Object","fromEntries","keys","sort","filter","key","undefined","aiDevReplayKey","providerId","stream","request","update","v","provider","model","system","block","text","tools","messages","maxTokens","thinking","effort","digest","aiCachedPrefixKey","last","Boolean","cacheBreakpoint","lastIndexOf","slice","repositoryRoot","start","process","cwd","dir","parent","aiDevReplayDir","env","pathFor","readRecording","path","parsed","parse","writeRecording","recording","recursive","temp","pid","stats","mode","live","replayed","liveProviderUsd","savedProviderUsd","aiDevReplayStats","round","resetAiDevReplayStats","usd","Math","usageCost","usage","aiDevCountLive","discount","replayEvents","events","event","recordEvents","source","onDone","completed","push","type","some","aiDevReplayComplete","send","id","recorded","kind","result","recordedAt","Date","toISOString","aiDevReplayStream","done","find","warming","Map","warmedAt","aiDevWarmPrefixFirst","prefixKey","ttlMs","now","warmed","get","pending","catch","first","set","delete","resetAiDevWarmPrefixes","clear","aiLiveRunLedger","replay","batch","batches","batchedRequests","requests","allReplayed"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/runtime/ai-dev-replay.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 { createHash } from 'node:crypto'\nimport { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'\nimport { dirname, join } from 'node:path'\nimport { estimateAiProviderCostUsd } from '../providers/catalog'\nimport type {\n AiProvider,\n AiProviderRequest,\n AiResult,\n AiStreamEvent,\n AiSystemBlock,\n AiTool,\n} from '../providers/contract'\nimport { aiLiveBatchStats } from './ai-live-batch'\nimport { AI_REPLAY_DIR_ENV, aiDevReplayMode, type AiDevEnv as Env, type AiDevReplayMode } from './ai-dev-env'\n\n/** The key format's version: bumped when what a key covers changes, so no old answer is read under a new meaning. */\nconst KEY_VERSION = 1\n\n/**\n * THE DEVELOPMENT REPLAY CACHE (AGL-3660).\n *\n * Almost every dollar the provider bills in a month is a development run: a\n * live eval, the layout screenshots fed by one, a local guided start. Most of\n * the requests in a second run of the same eval are byte-identical to the\n * first — the change under test was to a compiler, a check or one prompt, not\n * to all of them — and each identical request bought the same sample twice.\n *\n * So, outside production only, the runtime keys each request by a hash of\n * everything that decides the answer — the provider, the model, the system\n * blocks, the tools, the messages, the ceiling, thinking and effort — and keeps\n * the provider's answer under `.cache/ai-replay/` (gitignored). The next\n * identical request is answered from disk for nothing; a request whose prompt\n * changed by one byte has a new key and goes to the provider. A replayed answer\n * is a real answer the same model gave to the same bytes, so an eval that\n * passes on replays has still seen every CHANGED prompt answered live: the\n * replay never stands in for a prompt the provider has not answered.\n *\n * `AGLYN_AI_REPLAY`:\n * - `1` / `on`: replay what is recorded, record what is not.\n * - `refresh`: every request live, its answer recorded over the old one.\n * - `off` / `0`: nothing read, nothing written.\n * - unset: on under the live-eval launcher (`AI_EVAL_LIVE_LAUNCHER`), off\n * everywhere else, so a dev server or a spec that never asked for it\n * behaves exactly as before.\n *\n * It never runs in a deployed server. `NODE_ENV=production`, `VERCEL_ENV`\n * of `production` or `preview`, or a Cloud Run service (`K_SERVICE`) turn it\n * off whatever the variable says, and the first request says so in the log:\n * a recorded answer handed to a customer would be another customer's page.\n *\n * Every live eval's table carries `aiDevReplayStats()`, so a run that was\n * all replays reads as one: `live: 0` is not a live pass.\n */\n\n// ── The key ──────────────────────────────────────────────────────────────\n\n/** JSON with every object's keys in sorted order: the same value, the same bytes, whatever built it. */\nexport function aiCanonicalJson(value: unknown): string {\n return JSON.stringify(sortKeys(value))\n}\n\nfunction sortKeys(value: unknown): unknown {\n if (Array.isArray(value)) return value.map(sortKeys)\n if (value && typeof value === 'object') {\n return Object.fromEntries(\n Object.keys(value as Record<string, unknown>)\n .sort()\n .filter((key) => (value as Record<string, unknown>)[key] !== undefined)\n .map((key) => [key, sortKeys((value as Record<string, unknown>)[key])]),\n )\n }\n return value\n}\n\n/** What a request's answer depends on. The key, the signal and the cache lifetime are not part of it. */\nexport interface AiReplayRequest {\n model: string\n system: readonly AiSystemBlock[]\n messages: AiProviderRequest['messages']\n tools?: readonly AiTool[]\n maxTokens: number\n thinking?: AiProviderRequest['thinking']\n effort?: AiProviderRequest['effort']\n}\n\n/** The request's replay key: a SHA-256 of everything that decides the answer. */\nexport function aiDevReplayKey(providerId: string, stream: boolean, request: AiReplayRequest): string {\n return createHash('sha256')\n .update(\n aiCanonicalJson({\n v: KEY_VERSION,\n provider: providerId,\n stream,\n model: request.model,\n // A breakpoint changes no answer, so the key reads the text alone.\n system: request.system.map((block) => block.text),\n tools: request.tools ?? [],\n messages: request.messages,\n maxTokens: request.maxTokens,\n thinking: request.thinking ?? null,\n effort: request.effort ?? null,\n }),\n )\n .digest('hex')\n}\n\n/**\n * The prefix a provider caches: the model, the tools and every system block\n * through the last breakpoint. Two requests with the same prefix key read one\n * cache entry; `null` when the request marks no breakpoint.\n */\nexport function aiCachedPrefixKey(request: Pick<AiReplayRequest, 'model' | 'system' | 'tools'>): string | null {\n const last = request.system.map((block) => Boolean(block.cacheBreakpoint)).lastIndexOf(true)\n if (last < 0) return null\n return createHash('sha256')\n .update(\n aiCanonicalJson({\n model: request.model,\n tools: request.tools ?? [],\n system: request.system.slice(0, last + 1).map((block) => block.text),\n }),\n )\n .digest('hex')\n}\n\n// ── The store ────────────────────────────────────────────────────────────\n\n/** The repository root: the nearest directory up from here holding `nx.json`, else the working directory. */\nfunction repositoryRoot(start: string = process.cwd()): string {\n let dir = start\n for (;;) {\n if (existsSync(join(dir, 'nx.json'))) return dir\n const parent = dirname(dir)\n if (parent === dir) return start\n dir = parent\n }\n}\n\n/** Where recorded answers are kept. */\nexport function aiDevReplayDir(env: Env = process.env): string {\n return env[AI_REPLAY_DIR_ENV] || join(repositoryRoot(), '.cache', 'ai-replay')\n}\n\ntype Recording =\n | { v: number; key: string; model: string; recordedAt: string; kind: 'result'; result: AiResult }\n | { v: number; key: string; model: string; recordedAt: string; kind: 'stream'; events: AiStreamEvent[] }\n\nfunction pathFor(dir: string, key: string): string {\n return join(dir, key.slice(0, 2), `${key}.json`)\n}\n\nfunction readRecording(dir: string, key: string): Recording | null {\n const path = pathFor(dir, key)\n if (!existsSync(path)) return null\n try {\n const parsed = JSON.parse(readFileSync(path, 'utf8')) as Recording\n return parsed?.v === KEY_VERSION && parsed.key === key ? parsed : null\n } catch {\n return null\n }\n}\n\nfunction writeRecording(dir: string, recording: Recording): void {\n const path = pathFor(dir, recording.key)\n mkdirSync(dirname(path), { recursive: true })\n // Written aside and renamed, so a run killed mid-write leaves no half file to replay.\n const temp = `${path}.${process.pid}.tmp`\n writeFileSync(temp, JSON.stringify(recording))\n renameSync(temp, path)\n}\n\n// ── What a run did ───────────────────────────────────────────────────────\n\n/** How many of this process's requests were sent, and how many were answered from disk. */\nexport interface AiDevReplayStats {\n mode: AiDevReplayMode\n /** Requests sent to the provider. */\n live: number\n /** Requests answered from a recording. */\n replayed: number\n /** What the live requests cost at the provider's list rates. */\n liveProviderUsd: number\n /** What the replayed ones would have cost, had they been sent. */\n savedProviderUsd: number\n}\n\nconst stats: AiDevReplayStats = {\n mode: 'off',\n live: 0,\n replayed: 0,\n liveProviderUsd: 0,\n savedProviderUsd: 0,\n}\n\n/** This process's count so far: what a live eval prints beside its table. */\nexport function aiDevReplayStats(env: Env = process.env): AiDevReplayStats {\n return {\n ...stats,\n mode: aiDevReplayMode(env),\n liveProviderUsd: round(stats.liveProviderUsd),\n savedProviderUsd: round(stats.savedProviderUsd),\n }\n}\n\n/** Starts the count again; a spec that runs two rounds reports each. */\nexport function resetAiDevReplayStats(): void {\n stats.live = 0\n stats.replayed = 0\n stats.liveProviderUsd = 0\n stats.savedProviderUsd = 0\n}\n\nfunction round(usd: number): number {\n return Math.round(usd * 1_000_000) / 1_000_000\n}\n\nfunction usageCost(usage: AiResult['usage'], model: string): number {\n return estimateAiProviderCostUsd(usage, model)\n}\n\n/** Counts one request a live path sent, at what the provider billed for it; `discount` is a batch's half. */\nexport function aiDevCountLive(usage: AiResult['usage'], model: string, discount = 1): void {\n stats.live += 1\n stats.liveProviderUsd += usageCost(usage, model) * discount\n}\n\n// ── The seam ─────────────────────────────────────────────────────────────\n\nasync function* replayEvents(events: readonly AiStreamEvent[]): AsyncGenerator<AiStreamEvent> {\n for (const event of events) yield event\n}\n\n/** A live stream passed through as it arrives, and recorded once it is done. */\nasync function* recordEvents(\n source: AsyncIterable<AiStreamEvent>,\n onDone: (events: AiStreamEvent[]) => void,\n): AsyncGenerator<AiStreamEvent> {\n const events: AiStreamEvent[] = []\n let completed = false\n for await (const event of source) {\n events.push(event)\n if (event.type === 'done') completed = true\n yield event\n }\n // A stream cut short, or one that carried a provider error, is not an\n // answer worth keeping: the next run asks again.\n if (completed && !events.some((event) => event.type === 'error')) onDone(events)\n}\n\n/**\n * One request through the replay cache: answered from disk when recorded and\n * the mode replays, otherwise sent through `send` and recorded. `send` is the\n * live path — the provider, or the batch queue — and counts its own spend.\n */\nexport async function aiDevReplayComplete(\n provider: Pick<AiProvider, 'id'>,\n request: AiReplayRequest,\n mode: Exclude<AiDevReplayMode, 'off'>,\n send: () => Promise<AiResult>,\n env: Env = process.env,\n): Promise<AiResult> {\n const dir = aiDevReplayDir(env)\n const key = aiDevReplayKey(provider.id, false, request)\n const recorded = mode === 'replay' ? readRecording(dir, key) : null\n if (recorded?.kind === 'result') {\n stats.replayed += 1\n stats.savedProviderUsd += usageCost(recorded.result.usage, request.model)\n return recorded.result\n }\n const result = await send()\n writeRecording(dir, {\n v: KEY_VERSION,\n key,\n model: request.model,\n recordedAt: new Date().toISOString(),\n kind: 'result',\n result,\n })\n return result\n}\n\n/** The streaming half of {@link aiDevReplayComplete}. */\nexport async function aiDevReplayStream(\n provider: Pick<AiProvider, 'id'>,\n request: AiReplayRequest,\n mode: Exclude<AiDevReplayMode, 'off'>,\n send: () => Promise<AsyncIterable<AiStreamEvent>>,\n env: Env = process.env,\n): Promise<AsyncIterable<AiStreamEvent>> {\n const dir = aiDevReplayDir(env)\n const key = aiDevReplayKey(provider.id, true, request)\n const recorded = mode === 'replay' ? readRecording(dir, key) : null\n if (recorded?.kind === 'stream') {\n stats.replayed += 1\n const done = recorded.events.find((event) => event.type === 'done')\n if (done?.type === 'done') stats.savedProviderUsd += usageCost(done.usage, request.model)\n return replayEvents(recorded.events)\n }\n const live = await send()\n return recordEvents(live, (events) => {\n const done = events.find((event) => event.type === 'done')\n if (done?.type === 'done') aiDevCountLive(done.usage, request.model)\n writeRecording(dir, {\n v: KEY_VERSION,\n key,\n model: request.model,\n recordedAt: new Date().toISOString(),\n kind: 'stream',\n events,\n })\n })\n}\n\n// ── Warming a shared prefix before a fan-out ─────────────────────────────\n\n/**\n * A cache entry is readable only once the request that writes it has started\n * answering, so N requests sent at once over one prefix all pay to write it\n * and none reads it — which is exactly what a live eval's `Promise.all` over\n * its briefs did. In a development run the first request over a prefix goes\n * alone; the others wait for it and then read what it wrote. A prefix\n * answered within the cache's lifetime is not held again.\n *\n * Development only, like everything here: production's beat runs one step at\n * a time, and holding a customer's request behind another's would trade\n * their seconds for our cents.\n */\nconst warming = new Map<string, Promise<unknown>>()\nconst warmedAt = new Map<string, number>()\n\nexport async function aiDevWarmPrefixFirst<T>(\n prefixKey: string | null,\n ttlMs: number,\n send: () => Promise<T>,\n now: () => number = Date.now,\n): Promise<T> {\n if (!prefixKey) return send()\n const warmed = warmedAt.get(prefixKey)\n if (warmed !== undefined && now() - warmed < ttlMs) return send()\n const pending = warming.get(prefixKey)\n if (pending) {\n await pending.catch(() => undefined)\n return send()\n }\n const first = send()\n warming.set(prefixKey, first)\n try {\n const result = await first\n warmedAt.set(prefixKey, now())\n return result\n } finally {\n warming.delete(prefixKey)\n }\n}\n\n/** Forgets every warmed prefix; for specs. */\nexport function resetAiDevWarmPrefixes(): void {\n warming.clear()\n warmedAt.clear()\n}\n\n// ── What a live eval prints ──────────────────────────────────────────────\n\n/**\n * The line every live eval's table carries (AGL-3660): how many of its\n * requests went to the provider and how many were answered from disk, what\n * the live ones cost at list, and how many batches carried them. A table\n * whose `live` is 0 proved nothing new — every prompt in it had been answered\n * before — and is not a live pass; `AGLYN_AI_REPLAY=refresh` asks again.\n * `batchedRequests` are the ones a batch answered, at half price;\n * `batchFallbacks` are the ones a batch left unanswered past its wait and\n * that were asked on their own, at full price (AGL-3781). Both count in `live`.\n */\nexport function aiLiveRunLedger(env: Env = process.env): AiDevReplayStats & {\n batches: number\n batchedRequests: number\n batchFallbacks: number\n allReplayed: boolean\n} {\n const replay = aiDevReplayStats(env)\n const batch = aiLiveBatchStats()\n return {\n ...replay,\n batches: batch.batches,\n batchedRequests: batch.requests,\n batchFallbacks: batch.fallbacks,\n allReplayed: replay.live === 0 && replay.replayed > 0,\n }\n}\n"],"names":["createHash","existsSync","mkdirSync","readFileSync","renameSync","writeFileSync","dirname","join","estimateAiProviderCostUsd","aiLiveBatchStats","AI_REPLAY_DIR_ENV","aiDevReplayMode","KEY_VERSION","aiCanonicalJson","value","JSON","stringify","sortKeys","Array","isArray","map","Object","fromEntries","keys","sort","filter","key","undefined","aiDevReplayKey","providerId","stream","request","update","v","provider","model","system","block","text","tools","messages","maxTokens","thinking","effort","digest","aiCachedPrefixKey","last","Boolean","cacheBreakpoint","lastIndexOf","slice","repositoryRoot","start","process","cwd","dir","parent","aiDevReplayDir","env","pathFor","readRecording","path","parsed","parse","writeRecording","recording","recursive","temp","pid","stats","mode","live","replayed","liveProviderUsd","savedProviderUsd","aiDevReplayStats","round","resetAiDevReplayStats","usd","Math","usageCost","usage","aiDevCountLive","discount","replayEvents","events","event","recordEvents","source","onDone","completed","push","type","some","aiDevReplayComplete","send","id","recorded","kind","result","recordedAt","Date","toISOString","aiDevReplayStream","done","find","warming","Map","warmedAt","aiDevWarmPrefixFirst","prefixKey","ttlMs","now","warmed","get","pending","catch","first","set","delete","resetAiDevWarmPrefixes","clear","aiLiveRunLedger","replay","batch","batches","batchedRequests","requests","batchFallbacks","fallbacks","allReplayed"],"mappings":"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"}