@aglyn/plugins-ai 1.0.0-beta.285 → 1.0.0-beta.287
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +17 -17
- package/src/lib/assets/site-styles/nonprofit.webp +0 -0
- package/src/lib/assets/site-styles/professional.webp +0 -0
- package/src/lib/assets/site-styles/store.webp +0 -0
- package/src/lib/assets/site-styles/trades.webp +0 -0
- package/src/lib/components/ai-theme-proposal-card.component.js +12 -3
- package/src/lib/components/ai-theme-proposal-card.component.js.map +1 -1
- package/src/lib/declarations.js +5 -2
- package/src/lib/declarations.js.map +1 -1
- package/src/lib/declarations.server.js +12 -0
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/jobs/ai-build-capabilities.d.ts +3 -1
- package/src/lib/jobs/ai-build-capabilities.js +27 -2
- package/src/lib/jobs/ai-build-capabilities.js.map +1 -1
- package/src/lib/jobs/ai-image-reads.d.ts +59 -0
- package/src/lib/jobs/ai-image-reads.js +138 -0
- package/src/lib/jobs/ai-image-reads.js.map +1 -0
- package/src/lib/jobs/ai-job-build-step.d.ts +29 -0
- package/src/lib/jobs/ai-job-build-step.js +83 -12
- package/src/lib/jobs/ai-job-build-step.js.map +1 -1
- package/src/lib/jobs/ai-job-duplicate-step.d.ts +2 -1
- package/src/lib/jobs/ai-job-duplicate-step.js +6 -6
- package/src/lib/jobs/ai-job-duplicate-step.js.map +1 -1
- package/src/lib/jobs/ai-job-import-step.d.ts +4 -2
- package/src/lib/jobs/ai-job-import-step.js +15 -11
- package/src/lib/jobs/ai-job-import-step.js.map +1 -1
- package/src/lib/jobs/ai-job-insight-step.d.ts +10 -0
- package/src/lib/jobs/ai-job-insight-step.js +23 -5
- package/src/lib/jobs/ai-job-insight-step.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-language.js +2 -1
- package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
- package/src/lib/jobs/ai-job-media-step.d.ts +2 -1
- package/src/lib/jobs/ai-job-media-step.js +6 -8
- package/src/lib/jobs/ai-job-media-step.js.map +1 -1
- package/src/lib/jobs/ai-job-navigation-step.d.ts +2 -1
- package/src/lib/jobs/ai-job-navigation-step.js +7 -9
- package/src/lib/jobs/ai-job-navigation-step.js.map +1 -1
- package/src/lib/jobs/ai-job-page-language.js +7 -1
- package/src/lib/jobs/ai-job-page-language.js.map +1 -1
- package/src/lib/jobs/ai-job-page-sections.js +16 -3
- package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
- package/src/lib/jobs/ai-job-page-step.js +9 -1
- package/src/lib/jobs/ai-job-page-step.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-step.d.ts +30 -8
- package/src/lib/jobs/ai-job-plan-step.js +78 -13
- package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
- package/src/lib/jobs/ai-job-privacy-step.d.ts +4 -2
- package/src/lib/jobs/ai-job-privacy-step.js +14 -15
- package/src/lib/jobs/ai-job-privacy-step.js.map +1 -1
- package/src/lib/jobs/ai-job-products-step.js +5 -3
- package/src/lib/jobs/ai-job-products-step.js.map +1 -1
- package/src/lib/jobs/ai-job-settings-step.d.ts +2 -1
- package/src/lib/jobs/ai-job-settings-step.js +7 -8
- package/src/lib/jobs/ai-job-settings-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-content.d.ts +34 -1
- package/src/lib/jobs/ai-job-site-content.js +67 -8
- package/src/lib/jobs/ai-job-site-content.js.map +1 -1
- package/src/lib/jobs/ai-job-site-datasets.d.ts +53 -3
- package/src/lib/jobs/ai-job-site-datasets.js +298 -30
- package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
- package/src/lib/jobs/ai-job-site-step.js +6 -3
- package/src/lib/jobs/ai-job-site-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-store-pages.js +3 -3
- package/src/lib/jobs/ai-job-site-store-pages.js.map +1 -1
- package/src/lib/jobs/ai-job-tracking-step.d.ts +2 -1
- package/src/lib/jobs/ai-job-tracking-step.js +7 -8
- package/src/lib/jobs/ai-job-tracking-step.js.map +1 -1
- package/src/lib/jobs/ai-jobs-beat.d.ts +11 -7
- package/src/lib/jobs/ai-jobs-beat.js +18 -8
- package/src/lib/jobs/ai-jobs-beat.js.map +1 -1
- package/src/lib/jobs/ai-jobs-notify.d.ts +66 -1
- package/src/lib/jobs/ai-jobs-notify.js +136 -3
- package/src/lib/jobs/ai-jobs-notify.js.map +1 -1
- package/src/lib/jobs/ai-layout-stock-photos.d.ts +59 -4
- package/src/lib/jobs/ai-layout-stock-photos.js +897 -81
- package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
- package/src/lib/jobs/ai-product-image.d.ts +14 -0
- package/src/lib/jobs/ai-product-image.js +73 -21
- package/src/lib/jobs/ai-product-image.js.map +1 -1
- package/src/lib/jobs/ai-theme-brand-inputs.d.ts +9 -1
- package/src/lib/jobs/ai-theme-brand-inputs.js +28 -2
- package/src/lib/jobs/ai-theme-brand-inputs.js.map +1 -1
- package/src/lib/jobs/fixtures/ai-site-agl4065-datasets.d.ts +175 -0
- package/src/lib/jobs/fixtures/ai-site-agl4065-datasets.js +601 -0
- package/src/lib/jobs/fixtures/ai-site-agl4065-datasets.js.map +1 -0
- package/src/lib/jobs/fixtures/ai-site-agl4065-pages.d.ts +46 -0
- package/src/lib/jobs/fixtures/ai-site-agl4065-pages.js +2862 -0
- package/src/lib/jobs/fixtures/ai-site-agl4065-pages.js.map +1 -0
- package/src/lib/jobs/fixtures/ai-site-agl4065-plans.d.ts +74 -0
- package/src/lib/jobs/fixtures/ai-site-agl4065-plans.js +888 -0
- package/src/lib/jobs/fixtures/ai-site-agl4065-plans.js.map +1 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-judge.d.ts +29 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-judge.js +81 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-judge.js.map +1 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-recordings.d.ts +87 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-recordings.js +225 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-recordings.js.map +1 -0
- package/src/lib/layout-language/ai-layout-compiler.d.ts +11 -0
- package/src/lib/layout-language/ai-layout-compiler.js +194 -18
- package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
- package/src/lib/layout-language/ai-layout-copy.d.ts +37 -0
- package/src/lib/layout-language/ai-layout-copy.js +383 -3
- package/src/lib/layout-language/ai-layout-copy.js.map +1 -1
- package/src/lib/layout-language/ai-layout-design.d.ts +19 -0
- package/src/lib/layout-language/ai-layout-design.js +31 -11
- package/src/lib/layout-language/ai-layout-design.js.map +1 -1
- package/src/lib/layout-language/ai-layout-listings.d.ts +7 -1
- package/src/lib/layout-language/ai-layout-listings.js +9 -1
- package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
- package/src/lib/layout-language/ai-layout-pictures.d.ts +30 -0
- package/src/lib/layout-language/ai-layout-pictures.js +131 -3
- package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
- package/src/lib/layout-language/ai-layout-storefront.d.ts +18 -4
- package/src/lib/layout-language/ai-layout-storefront.js +100 -20
- package/src/lib/layout-language/ai-layout-storefront.js.map +1 -1
- package/src/lib/model/ai-build-job.d.ts +7 -0
- package/src/lib/model/ai-build-job.js +36 -7
- package/src/lib/model/ai-build-job.js.map +1 -1
- package/src/lib/model/ai-build-plan.d.ts +28 -4
- package/src/lib/model/ai-build-plan.js +49 -9
- package/src/lib/model/ai-build-plan.js.map +1 -1
- package/src/lib/model/ai-build-progress.js +3 -1
- package/src/lib/model/ai-build-progress.js.map +1 -1
- package/src/lib/model/ai-job-notice.d.ts +8 -0
- package/src/lib/model/ai-job-notice.js +33 -0
- package/src/lib/model/ai-job-notice.js.map +1 -1
- package/src/lib/model/ai-job-plan-lines.d.ts +2 -1
- package/src/lib/model/ai-job-plan-lines.js +6 -1
- package/src/lib/model/ai-job-plan-lines.js.map +1 -1
- package/src/lib/model/ai-jobs.types.d.ts +8 -0
- package/src/lib/model/ai-jobs.types.js.map +1 -1
- package/src/lib/model/ai-permissions.d.ts +13 -0
- package/src/lib/model/ai-permissions.js +29 -1
- package/src/lib/model/ai-permissions.js.map +1 -1
- package/src/lib/model/ai-products.d.ts +18 -0
- package/src/lib/model/ai-products.js +32 -0
- package/src/lib/model/ai-products.js.map +1 -1
- package/src/lib/model/ai-site-inspiration.d.ts +3 -2
- package/src/lib/model/ai-site-inspiration.js +3 -2
- package/src/lib/model/ai-site-inspiration.js.map +1 -1
- package/src/lib/plugin.js +4 -13
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/providers/anthropic.js +9 -2
- package/src/lib/providers/anthropic.js.map +1 -1
- package/src/lib/providers/catalog.js +14 -5
- package/src/lib/providers/catalog.js.map +1 -1
- package/src/lib/providers/contract.d.ts +24 -1
- package/src/lib/providers/contract.js.map +1 -1
- package/src/lib/providers/routing.js +5 -2
- package/src/lib/providers/routing.js.map +1 -1
- package/src/lib/runtime/ai-dataset-generation.d.ts +99 -1
- package/src/lib/runtime/ai-dataset-generation.js +251 -3
- package/src/lib/runtime/ai-dataset-generation.js.map +1 -1
- package/src/lib/runtime/ai-dev-replay.d.ts +1 -0
- package/src/lib/runtime/ai-dev-replay.js +4 -2
- package/src/lib/runtime/ai-dev-replay.js.map +1 -1
- package/src/lib/runtime/ai-doctrine-validators.d.ts +47 -3
- package/src/lib/runtime/ai-doctrine-validators.js +343 -27
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-doctrine.js +8 -2
- package/src/lib/runtime/ai-doctrine.js.map +1 -1
- package/src/lib/runtime/ai-icon-library.d.ts +27 -1
- package/src/lib/runtime/ai-icon-library.js +30 -3
- package/src/lib/runtime/ai-icon-library.js.map +1 -1
- package/src/lib/runtime/ai-products-generation.d.ts +11 -1
- package/src/lib/runtime/ai-products-generation.js +51 -4
- package/src/lib/runtime/ai-products-generation.js.map +1 -1
- package/src/lib/runtime/assist-answer-cache.d.ts +2 -0
- package/src/lib/runtime/assist-answer-cache.js +5 -2
- package/src/lib/runtime/assist-answer-cache.js.map +1 -1
- package/src/lib/server/ai-allotments.js +7 -2
- package/src/lib/server/ai-allotments.js.map +1 -1
- package/src/lib/server/ai-experiment-versions.js +5 -5
- package/src/lib/server/ai-experiment-versions.js.map +1 -1
- package/src/lib/server/ai-host-permissions-roster.d.ts +25 -0
- package/src/lib/server/ai-host-permissions-roster.js +33 -0
- package/src/lib/server/ai-host-permissions-roster.js.map +1 -0
- package/src/lib/server/ai-host-permissions.js +8 -6
- package/src/lib/server/ai-host-permissions.js.map +1 -1
- package/src/lib/server/ai-image-reads-route.d.ts +17 -0
- package/src/lib/server/ai-image-reads-route.js +106 -0
- package/src/lib/server/ai-image-reads-route.js.map +1 -0
- package/src/lib/server/ai-seo-apply.js +5 -5
- package/src/lib/server/ai-seo-apply.js.map +1 -1
- package/src/lib/server/ai-site-import-route.js +7 -4
- package/src/lib/server/ai-site-import-route.js.map +1 -1
- package/src/lib/server/assist-build.js +1 -0
- package/src/lib/server/assist-build.js.map +1 -1
- package/src/lib/server/assist-chat.d.ts +11 -7
- package/src/lib/server/assist-chat.js +185 -48
- package/src/lib/server/assist-chat.js.map +1 -1
- package/src/lib/server/assist-docs-query.d.ts +51 -0
- package/src/lib/server/assist-docs-query.js +57 -0
- package/src/lib/server/assist-docs-query.js.map +1 -0
- package/src/lib/server/assist-jobs-context.d.ts +102 -0
- package/src/lib/server/assist-jobs-context.js +227 -0
- package/src/lib/server/assist-jobs-context.js.map +1 -0
- package/src/lib/server/assist-view-context.d.ts +8 -4
- package/src/lib/server/assist-view-context.js +12 -7
- package/src/lib/server/assist-view-context.js.map +1 -1
- package/src/lib/server.js +12 -0
- package/src/lib/server.js.map +1 -1
- package/src/lib/tools/ai-dataset-tool.d.ts +11 -1
- package/src/lib/tools/ai-dataset-tool.js +24 -7
- package/src/lib/tools/ai-dataset-tool.js.map +1 -1
- package/src/lib/tools/ai-duplicate-resource-tool.js +5 -6
- package/src/lib/tools/ai-duplicate-resource-tool.js.map +1 -1
- package/src/lib/components/ai-collaborator-permissions-column.component.d.ts +0 -32
- package/src/lib/components/ai-collaborator-permissions-column.component.js +0 -132
- package/src/lib/components/ai-collaborator-permissions-column.component.js.map +0 -1
|
@@ -41,7 +41,11 @@ import { UpstreamServiceError } from '@aglyn/shared-util-errors';
|
|
|
41
41
|
* A `tools` entry is sent strict, with additional properties forbidden on
|
|
42
42
|
* its schema, so a tool call's `input` validates against the schema
|
|
43
43
|
* exactly. Tool choice stays automatic — forced tool use is not portable
|
|
44
|
-
* across models — and the caller's
|
|
44
|
+
* across models — and the caller's own words name the tool it wants. A door
|
|
45
|
+
* that sends several tools may also name the one a call is for
|
|
46
|
+
* (`toolChoice`), which an adapter forces only on a model whose descriptor
|
|
47
|
+
* says it takes a forced choice (`forcedToolChoice`) and only with thinking
|
|
48
|
+
* off, and sends as automatic everywhere else (AGL-4076). An
|
|
45
49
|
* adapter delivers `input` as an object; a stream assembles it from
|
|
46
50
|
* fragments and parses the whole once, at the block's end. Nothing
|
|
47
51
|
* string-matches the serialized form: models differ in how they escape it.
|
|
@@ -234,6 +238,18 @@ export interface AiRequestBase {
|
|
|
234
238
|
* It changes no answer, so a replay key does not read it.
|
|
235
239
|
*/
|
|
236
240
|
cacheConversation?: boolean;
|
|
241
|
+
/**
|
|
242
|
+
* The tool this call is for, by name: one of `tools` (AGL-4076). A step
|
|
243
|
+
* whose calls each want a different tool sends every one of them on every
|
|
244
|
+
* call, so the tools that open the cached prefix are the same bytes each
|
|
245
|
+
* time and the later calls read the prefix the first one wrote; this names
|
|
246
|
+
* the one wanted now. A tool choice sits outside the cached prefix, so
|
|
247
|
+
* naming a different one costs no cache read. The adapter forces it only
|
|
248
|
+
* where the model's descriptor says `forcedToolChoice` and the request
|
|
249
|
+
* thinks with thinking off, which a forced choice requires; everywhere else
|
|
250
|
+
* the choice stays automatic and the door's own words name the tool.
|
|
251
|
+
*/
|
|
252
|
+
toolChoice?: string;
|
|
237
253
|
}
|
|
238
254
|
/** The request an adapter receives: the runtime has already validated it. */
|
|
239
255
|
export interface AiProviderRequest extends AiRequestBase {
|
|
@@ -463,6 +479,13 @@ export interface AiModelDescriptor {
|
|
|
463
479
|
* sent an image it did not say it can read.
|
|
464
480
|
*/
|
|
465
481
|
vision?: boolean;
|
|
482
|
+
/**
|
|
483
|
+
* Whether the model takes a forced tool choice (AGL-4076): a request that
|
|
484
|
+
* names the one tool it must call. Absent reads as no, so a request that
|
|
485
|
+
* names its tool is sent an automatic choice instead. Claude Opus 5.5 and
|
|
486
|
+
* Claude Sonnet 5.5 refuse a forced choice outright.
|
|
487
|
+
*/
|
|
488
|
+
forcedToolChoice?: boolean;
|
|
466
489
|
};
|
|
467
490
|
}
|
|
468
491
|
/** The one sentence a customer may read when a provider fails. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/providers/contract.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 { UpstreamServiceError } from '@aglyn/shared-util-errors'\n\n/**\n * The provider contract (AGL-2939): the one shape every AI door speaks, and\n * the one thing an adapter has to implement. The runtime, the gate, the\n * jobs, the doctrine and the usage code import this and never a vendor\n * URL, header or model literal — Anthropic is one adapter behind it, and\n * an OpenAI-compatible endpoint is the second, which is what proves the\n * seam holds.\n *\n * ── Prompt caching, by construction ───────────────────────────────────────\n *\n * `system` is an ordered list of blocks, and caching is a PREFIX match: a\n * breakpoint covers every block before it, and one byte that differs per\n * request anywhere inside that prefix gives every distinct value its own\n * copy of it. The AGL-2352 rule is that no per-org byte may sit inside a\n * cached prefix — a white-label brand name interpolated into a cached block\n * multiplies the prefix count by the number of brands, each written off its\n * own traffic and expired before it is read. So a block marked `volatile`\n * may not sit at or before the last `cacheBreakpoint`, and the runtime\n * refuses the request before a byte leaves the process. A provider with no\n * cache honors the order and ignores the markers.\n *\n * ── Structured output ─────────────────────────────────────────────────────\n *\n * A `tools` entry is sent strict, with additional properties forbidden on\n * its schema, so a tool call's `input` validates against the schema\n * exactly. Tool choice stays automatic — forced tool use is not portable\n * across models — and the caller's system text names the tool it wants. An\n * adapter delivers `input` as an object; a stream assembles it from\n * fragments and parses the whole once, at the block's end. Nothing\n * string-matches the serialized form: models differ in how they escape it.\n * A provider bounds how much strict schema one request may carry, and an\n * adapter declares those bounds as `toolSchemaLimits`: three bounds a vendor\n * publishes and counts, and a fourth, the size of the grammar the schemas\n * compile to, that none publishes and only a live request settles.\n *\n * ── Thinking ──────────────────────────────────────────────────────────────\n *\n * `thinking: 'off'` and `'adaptive'` are sent when the provider has the\n * control; an omitted field sends NOTHING, leaving the model's own default\n * in force. The omission is deliberate rather than a gap: some models\n * reject an explicit setting, and the copy assistant serves its element\n * mode on such a model. `effort` rides the same rule.\n *\n * ── Pictures ──────────────────────────────────────────────────────────────\n *\n * A user turn may carry pictures beside its text (AGL-2916): `content` is\n * then an ordered list of parts, and a turn with nothing but text stays a\n * string, which every adapter passes through as it always has. A picture is\n * base64 bytes of one of `AI_IMAGE_MEDIA_TYPES`, never a URL, so no provider\n * fetches anything on the platform's behalf. The runtime refuses a picture\n * on an assistant turn, a picture for a model whose descriptor does not say\n * `vision`, and a picture past the size or count bounds, all before any\n * network is touched. Each adapter maps the parts onto its vendor's shape.\n */\n\n/**\n * One system block. `cacheBreakpoint` marks the end of a cacheable prefix;\n * `volatile` declares that the text carries a per-request or per-org byte\n * and therefore may never sit inside one.\n *\n * `site` declares a block that describes ONE site (AGL-3661): its business\n * profile, status and remembered preferences. As a breakpoint it is cached\n * once per site, so it must close the cached span — every platform-wide\n * block sits ahead of it, where every workspace still shares one entry —\n * and the runtime refuses a request that puts another breakpoint after it.\n */\nexport interface AiSystemBlock {\n text: string\n cacheBreakpoint?: true\n volatile?: true\n site?: true\n}\n\n/** A structured-output tool. `strict` is required, not optional. */\nexport interface AiTool {\n name: string\n description: string\n /** A JSON schema of `type: 'object'`; `additionalProperties` is forced off. */\n inputSchema: Record<string, unknown>\n strict: true\n}\n\n/**\n * How much of a request's strict tools a provider can compile (AGL-3096). A\n * provider that constrains decoding to a tool's schema compiles every strict\n * schema of a request together, before the model runs, and refuses a request\n * whose schemas are past its bounds. That refusal is permanent: the schema is\n * the same on every retry. Each bound is a TOTAL over every strict tool one\n * request sends, as `aiToolSchemaCounts` counts them. A bound left out is one\n * the provider does not state.\n *\n * A provider may also refuse a request whose schemas compile to too large a\n * grammar, which none of the published counts can see: a request counting 1\n * strict tool, 0 optional parameters and 1 union was refused for exactly that\n * (AGL-3096). `compiledSchemaBytes` stands in for it, on live evidence.\n */\nexport interface AiToolSchemaLimits {\n /** The most strict tools one request may send. */\n strictTools?: number\n /** The most properties, across every strict schema, left out of their object's `required`. */\n optionalParameters?: number\n /** The most schemas, across every strict schema, that are a union: an `anyOf` or `oneOf`, or a list of types. */\n unionParameters?: number\n /**\n * The most bytes of schema a request's grammar may be compiled from, as\n * `aiToolSchemaCompiledBytes` measures them.\n *\n * Unlike the bounds above, this one is EMPIRICAL. A provider refuses a\n * request whose compiled grammar is too large without publishing the size,\n * so the number here is not read off a vendor's page: it is the largest a\n * live request has been watched to carry, and every number above it is\n * unknown rather than safe. Raising it on an offline measurement alone\n * puts the guard back where it could not see the break.\n */\n compiledSchemaBytes?: number\n}\n\n/** A request's strict tools, counted on the measures `AiToolSchemaLimits` bounds. */\nexport type AiToolSchemaCounts = Required<AiToolSchemaLimits>\n\n/**\n * One schema as a grammar compiler reads it: every local `$ref` written out\n * at each place it is used, and every description, title and `$defs` dropped,\n * since none of them constrains a single token. A schema that refers to\n * itself is written out once around and then left as the reference it is.\n */\nfunction compiledForm(node: unknown, root: Record<string, unknown>, resolving: ReadonlySet<string>): unknown {\n if (Array.isArray(node)) return node.map((entry) => compiledForm(entry, root, resolving))\n if (!isSchemaObject(node)) return node\n const ref = node['$ref']\n if (typeof ref === 'string') {\n if (resolving.has(ref)) return { $ref: ref }\n return compiledForm(localSchemaAt(root, ref), root, new Set([...resolving, ref]))\n }\n const written: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(node)) {\n if (key === 'description' || key === 'title' || key === '$defs') continue\n written[key] = compiledForm(value, root, resolving)\n }\n return written\n}\n\n/**\n * The bytes of schema a request's grammar is compiled from, over all its\n * strict tools. It is a stand-in for the size of the grammar itself, which\n * nothing outside the provider can measure, and it errs toward more: a `$ref`\n * counts at each place it is used, because a compiler that writes it out\n * there pays for it there.\n */\nexport function aiToolSchemaCompiledBytes(tools: readonly AiTool[]): number {\n return tools.reduce(\n (bytes, tool) => bytes + JSON.stringify(compiledForm(tool.inputSchema, tool.inputSchema, new Set())).length,\n 0,\n )\n}\n\nfunction isSchemaObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\n/** The schema a local `$ref` (`#/…`) points at, or `undefined`. */\nfunction localSchemaAt(root: Record<string, unknown>, ref: string): unknown {\n if (!ref.startsWith('#/')) return undefined\n return ref\n .slice(2)\n .split('/')\n .map((key) => key.replace(/~1/g, '/').replace(/~0/g, '~'))\n .reduce<unknown>((node, key) => (isSchemaObject(node) ? node[key] : undefined), root)\n}\n\nfunction countSchema(\n node: unknown,\n root: Record<string, unknown>,\n counts: AiToolSchemaCounts,\n resolving: ReadonlySet<string>,\n): void {\n if (!isSchemaObject(node)) return\n if (Array.isArray(node['anyOf']) || Array.isArray(node['oneOf']) || Array.isArray(node['type'])) {\n counts.unionParameters += 1\n }\n const ref = node['$ref']\n if (typeof ref === 'string' && !resolving.has(ref)) {\n countSchema(localSchemaAt(root, ref), root, counts, new Set([...resolving, ref]))\n }\n const properties = node['properties']\n if (isSchemaObject(properties)) {\n const required = Array.isArray(node['required']) ? node['required'] : []\n for (const [key, property] of Object.entries(properties)) {\n if (!required.includes(key)) counts.optionalParameters += 1\n countSchema(property, root, counts, resolving)\n }\n }\n const items = node['items']\n for (const item of Array.isArray(items) ? items : [items]) countSchema(item, root, counts, resolving)\n for (const key of ['anyOf', 'allOf', 'oneOf']) {\n const branches = node[key]\n if (Array.isArray(branches)) for (const branch of branches) countSchema(branch, root, counts, resolving)\n }\n}\n\n/**\n * The strict tools of one request, counted on each measure a provider bounds.\n * Every tool the contract sends is strict. Each schema is walked whole: every\n * property, every `items`, every branch of an `anyOf`, `allOf` or `oneOf`, and\n * a local `$ref` at each place it is used. A union nested in an array's items\n * or in another union's branch counts on its own, as it does for a provider\n * that compiles it; a `$ref` counted where it is used errs toward more.\n */\nexport function aiToolSchemaCounts(tools: readonly AiTool[]): AiToolSchemaCounts {\n const counts: AiToolSchemaCounts = {\n strictTools: tools.length,\n optionalParameters: 0,\n unionParameters: 0,\n compiledSchemaBytes: aiToolSchemaCompiledBytes(tools),\n }\n for (const tool of tools) countSchema(tool.inputSchema, tool.inputSchema, counts, new Set())\n return counts\n}\n\n/**\n * Each bound, as one, as many, and where its number comes from — a published\n * bound reads as the provider's own, a measured one as what has been proved.\n */\nconst TOOL_SCHEMA_MEASURES: ReadonlyArray<[keyof AiToolSchemaLimits, string, string, string]> = [\n ['strictTools', 'strict tool', 'strict tools', 'the provider compiles'],\n ['optionalParameters', 'optional parameter', 'optional parameters', 'the provider compiles'],\n ['unionParameters', 'union-typed parameter', 'union-typed parameters', 'the provider compiles'],\n ['compiledSchemaBytes', 'byte of compiled schema', 'bytes of compiled schema', 'a live request has proved'],\n]\n\n/** Each bound a request's tools are past, as a sentence; empty when every one holds. */\nexport function aiToolSchemaBreaches(\n tools: readonly AiTool[],\n limits: AiToolSchemaLimits | undefined,\n): string[] {\n if (!limits) return []\n const counts = aiToolSchemaCounts(tools)\n return TOOL_SCHEMA_MEASURES.flatMap(([measure, one, many, source]) => {\n const limit = limits[measure]\n const count = counts[measure]\n return limit !== undefined && count > limit\n ? [`${count} ${count === 1 ? one : many} in one request, over the ${limit} ${source}`]\n : []\n })\n}\n\nexport type AiEffort = 'low' | 'medium' | 'high'\nexport type AiThinking = 'off' | 'adaptive'\n\n/**\n * The picture formats a request may carry (AGL-2916): the four every model\n * the catalog marks as reading images accepts. A door converts anything else\n * — an AVIF, an SVG — before it attaches the picture, or attaches none.\n */\nexport const AI_IMAGE_MEDIA_TYPES = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'] as const\nexport type AiImageMediaType = (typeof AI_IMAGE_MEDIA_TYPES)[number]\n\n/**\n * The largest picture one part may carry, in decoded bytes: the ceiling the\n * providers behind the contract accept for one image. A door that attaches a\n * picture resizes it far below this; the runtime refuses anything above it\n * before a byte leaves the process, rather than letting the provider do it\n * after the request was paid for in time.\n */\nexport const AI_IMAGE_MAX_BYTES = 5 * 1024 * 1024\n\n/** The most pictures one request may carry. */\nexport const AI_REQUEST_MAX_IMAGES = 8\n\n/** Text in a message made of parts. */\nexport interface AiTextPart {\n type: 'text'\n text: string\n}\n\n/**\n * A picture in a message (AGL-2916): its bytes, base64-encoded, and their\n * format. Only a USER turn carries one, and only to a model whose descriptor\n * says it reads images; the runtime refuses any other request before a byte\n * leaves the process, so a door cannot send a picture by accident to a model\n * that would reject it, or to a turn the model wrote.\n */\nexport interface AiImagePart {\n type: 'image'\n mediaType: AiImageMediaType\n /** The picture's bytes, base64-encoded, with no `data:` prefix. */\n data: string\n}\n\nexport type AiMessagePart = AiTextPart | AiImagePart\n\nexport interface AiMessage {\n role: 'user' | 'assistant'\n /**\n * The turn's text, or its parts in order — text and pictures — for a door\n * that shows the model an image. A door with nothing but text sends a\n * string, which every adapter passes through as it always has.\n */\n content: string | readonly AiMessagePart[]\n}\n\n/** A message's text: its parts' text joined, with every picture left out. */\nexport function aiMessageText(message: Pick<AiMessage, 'content'>): string {\n if (typeof message.content === 'string') return message.content\n return message.content\n .filter((part): part is AiTextPart => part.type === 'text')\n .map((part) => part.text)\n .join('\\n\\n')\n}\n\n/** Every picture a list of messages carries, in order. */\nexport function aiMessageImages(messages: readonly Pick<AiMessage, 'content'>[]): AiImagePart[] {\n return messages.flatMap((message) =>\n typeof message.content === 'string'\n ? []\n : message.content.filter((part): part is AiImagePart => part.type === 'image'),\n )\n}\n\n/** The decoded size of a base64 string, without decoding it. */\nexport function aiBase64Bytes(data: string): number {\n const padding = data.endsWith('==') ? 2 : data.endsWith('=') ? 1 : 0\n return Math.max(0, Math.floor((data.length * 3) / 4) - padding)\n}\n\n/** What every door hands the runtime; `provider` and `model` resolve there. */\nexport interface AiRequestBase {\n model: string\n system: AiSystemBlock[]\n messages: AiMessage[]\n tools?: AiTool[]\n maxTokens: number\n effort?: AiEffort\n thinking?: AiThinking\n signal?: AbortSignal\n /**\n * How long a cache entry the SHARED breakpoints write should live, where\n * the provider offers a choice: the provider's default (five minutes on the\n * Messages API) when absent. A step a site job runs asks for `1h` through\n * its routing row (`AiRoutingRow.cacheTtl`, AGL-3830), in production too,\n * and a development live run asks for it on every request\n * (`runtime/ai-dev-env.ts`). A job's own markers keep five minutes whatever\n * this says (`aiCacheMarkerTtls`, AGL-4050). It changes no answer, only what\n * a request within the hour pays for the prefix, so a replay key does not\n * read it.\n */\n cacheTtl?: '1h'\n /**\n * Cache the conversation through its last message as well as the system\n * blocks (AGL-3809), for a request the door expects to continue: a\n * generation that looks the site up or is asked again re-sends its whole\n * first turn, which without this is billed at the full input rate on every\n * continuation. One more breakpoint, on the last message, counted against\n * the same limit as the system's; a provider without a cache ignores it.\n * It changes no answer, so a replay key does not read it.\n */\n cacheConversation?: boolean\n}\n\n/** The request an adapter receives: the runtime has already validated it. */\nexport interface AiProviderRequest extends AiRequestBase {\n /** The provider's own key, resolved by the runtime from the environment or the org's setting. */\n apiKey: string\n}\n\n/** How long one cache marker's entry lives: the provider's five minutes, or the hour. */\nexport type AiCacheTtl = '5m' | '1h'\n\n/**\n * Who reads a cache marker's entry (AGL-4050). A `shared` marker closes a\n * span of the platform's own prompt (the tools, the doctrine, a door's\n * rules), the same bytes for every org and every job, so the next job can\n * read what this one wrote: it lives as long as the request's `cacheTtl`\n * asks. A `job` marker closes bytes only one job reads: a site's block\n * (`site`) or the request's own conversation (`cacheConversation`). That job\n * reads it again within minutes and no other job ever does, so it keeps the\n * provider's five minutes whatever the route asks; at the hour, its 2x write\n * would never be read back.\n */\nexport type AiCacheScope = 'shared' | 'job'\n\n/** A system block's marker scope: a site's block is one job's, every other block the platform's. */\nexport function aiCacheScopeOf(block: Pick<AiSystemBlock, 'site'>): AiCacheScope {\n return block.site ? 'job' : 'shared'\n}\n\n/**\n * The lifetime of every cache marker a request carries (AGL-4050), in prefix\n * order: one per system block, `null` where the block marks none, then the\n * conversation's, `null` where it is not cached. Tools carry no marker of\n * their own; they sit ahead of the first system marker and live as long as it.\n *\n * A provider refuses a request whose longer-lived entry comes after a\n * shorter-lived one, so once a marker is five minutes every marker after it\n * is too. A shared marker behind a job's is downgraded, never sent at the\n * hour. The runtime already keeps a site's block last among the system\n * breakpoints, so a door's request never meets the downgrade; it is the\n * adapter's guarantee all the same.\n */\nexport function aiCacheMarkerTtls(\n request: Pick<AiRequestBase, 'system' | 'messages' | 'cacheTtl' | 'cacheConversation'>,\n): { system: Array<AiCacheTtl | null>; conversation: AiCacheTtl | null } {\n let ceiling: AiCacheTtl = request.cacheTtl === '1h' ? '1h' : '5m'\n const lifetime = (scope: AiCacheScope): AiCacheTtl => {\n if (scope === 'job') ceiling = '5m'\n return ceiling\n }\n const system = request.system.map((block) => (block.cacheBreakpoint ? lifetime(aiCacheScopeOf(block)) : null))\n const conversation = request.cacheConversation && request.messages.length > 0 ? lifetime('job') : null\n return { system, conversation }\n}\n\n/**\n * Token usage in the meter's shape. Cache reads and writes are separate\n * because they are priced separately; a provider with no cache reports 0.\n */\nexport interface AiUsage {\n inputTokens: number\n outputTokens: number\n cacheReadTokens: number\n cacheWriteTokens: number\n /**\n * How many of `cacheWriteTokens` went to an hour-long cache entry\n * (AGL-3830), where the provider breaks the writes down; absent where none\n * did or the provider does not say.\n *\n * Inside `cacheWriteTokens`, never beside it, like `thinkingTokens` below.\n * An hour-long write is billed at twice the input price where a rate\n * table's `cacheWritePerToken` is the five-minute 1.25 times, so a price\n * reads this to add the difference (`aiCacheWriteHourSurchargeUsd`).\n */\n cacheWriteHourTokens?: number\n /**\n * How many of `outputTokens` the model spent thinking, where the provider\n * reports it; absent where it does not (AGL-3143).\n *\n * Inside `outputTokens`, never beside it: a rate table prices the four\n * figures above and would charge these twice if it read this one.\n *\n * It is here because it is the figure that says where a generation's\n * output went. A tool call that comes back far smaller than the tokens it\n * cost has three shapes, and only this separates the first from the other\n * two: the model thought and then answered briefly, which is a model\n * working as asked; a runaway string the partial parse discarded; or\n * decoding that wrote nothing usable. Kept on every answer, because a\n * generation burns its budget thinking without ever reaching its ceiling,\n * and `rawOutput` is only there when it does.\n */\n thinkingTokens?: number\n}\n\nexport interface AiToolUse {\n name: string\n input: Record<string, unknown>\n}\n\n/** A completed, non-streaming answer. */\nexport interface AiCompletion {\n kind: 'completion'\n /** Every text block, joined. */\n text: string\n toolUse: AiToolUse[]\n usage: AiUsage\n /**\n * The exchange at the serving model's BILLED rates — what a customer's\n * credits are drawn from, and never a margin's cost side (AGL-3015). What\n * the same exchange cost us is `estimateAiProviderCostUsd`, recorded by\n * the meter; a result travels no further than the credit path, so it\n * carries the one figure that path needs.\n */\n estCostUsd: number\n stopReason: string | null\n /**\n * WHERE THE OUTPUT WENT (AGL-3143). The provider's own output for this\n * call, as it answered it, kept ONLY when the call stopped at its ceiling\n * and cut to `AI_RAW_OUTPUT_MAX_CHARS`.\n *\n * A tool call truncated mid-answer reaches `toolUse` as whatever the\n * provider could still parse, and the parsed input alone cannot say where\n * the tokens went: a runaway string the partial parse discarded and a\n * decoding stall that wrote nothing usable both leave a small input\n * against a spent ceiling. This is what tells them apart. These are the\n * model's own words, so they belong to the trace alone: never to a reader,\n * and never to a log, which carries the figures that separate the two\n * shapes — `outputTokens`, `parsedChars`, `rawChars`.\n */\n rawOutput?: string\n}\n\n/**\n * Whether a model call stopped because it reached the output ceiling it was\n * asked for (AGL-3042). `max_tokens` is the contract's word, and the\n * OpenAI-compatible adapter maps its endpoint's `length` onto it; `length` is\n * read as well, for an adapter that passes its provider's own word through.\n */\nexport function aiStoppedAtCeiling(stopReason: string | null): boolean {\n return stopReason === 'max_tokens' || stopReason === 'length'\n}\n\n/** The most characters of raw output a call cut off at its ceiling keeps. */\nexport const AI_RAW_OUTPUT_MAX_CHARS = 20_000\n\n/**\n * A cut-off call's output as the trace keeps it: what the provider answered,\n * serialized where it is not already text, and cut to the bound with an\n * ellipsis so a reader can tell a cut trace from a short answer.\n */\nexport function aiRawOutputOf(output: unknown): string {\n const text = typeof output === 'string' ? output : (JSON.stringify(output) ?? '')\n return text.length > AI_RAW_OUTPUT_MAX_CHARS ? `${text.slice(0, AI_RAW_OUTPUT_MAX_CHARS)}…` : text\n}\n\n/**\n * WHERE THE OUTPUT WENT, as a log may carry it (AGL-3143): the figures that\n * separate the shapes a call cut off at its ceiling comes in, and nothing\n * else.\n *\n * Thinking accounts for the spend first, because the thinking budget is\n * drawn from the same ceiling as the answer: a call can spend nearly all of\n * one thinking and still be cut off with a correct, tiny tool input, which\n * is a ceiling to raise rather than a decoder to fix. What thinking does not\n * account for is told apart by how much was written — long, and a runaway\n * string the partial parse discarded; short, and decoding wrote nothing\n * usable.\n *\n * It takes the raw output rather than a count on purpose. The bytes are the\n * model's own words and therefore the site's content, so they belong to the\n * trace alone and never to a log; passing them through one function that can\n * only ever return their LENGTH is what holds that rule at every door,\n * instead of asking each one to remember it.\n */\nexport function aiCutOffFigures(call: {\n usage: AiUsage\n /** The tool input that still parsed, or null where none did. */\n parsed: unknown\n rawOutput: string | null | undefined\n}): {\n outputTokens: number\n thinkingTokens: number | null\n parsedChars: number\n rawChars: number | null\n} {\n const parsed =\n call.parsed === null || call.parsed === undefined ? '' : (JSON.stringify(call.parsed) ?? '')\n return {\n outputTokens: call.usage.outputTokens,\n thinkingTokens: call.usage.thinkingTokens ?? null,\n parsedChars: parsed.length,\n rawChars: call.rawOutput?.length ?? null,\n }\n}\n\n/**\n * The model declined. Tokens were spent, so the usage is real and the\n * caller meters it; the text is whatever partial answer preceded the stop,\n * usually nothing. Every adapter maps its provider's own decline signal —\n * a refusal stop, a content filter — onto this one kind.\n */\nexport interface AiRefusal {\n kind: 'refusal'\n text: string\n usage: AiUsage\n estCostUsd: number\n stopReason: 'refusal'\n}\n\nexport type AiResult = AiCompletion | AiRefusal\n\nexport type AiStreamEvent =\n | { type: 'delta'; text: string }\n | { type: 'tool'; name: string; input: Record<string, unknown> }\n /**\n * The provider's in-stream error. The stream is still open and ends with\n * `done`, so the caller decides what the reader is told; the provider's\n * own words went to the log, not here.\n */\n | { type: 'error'; retryable: boolean }\n | {\n type: 'done'\n usage: AiUsage\n estCostUsd: number\n stopReason: string | null\n /**\n * WHERE THE OUTPUT WENT, for a stream (AGL-3143): the bytes the\n * stream's tool calls were written in, kept ONLY when the stream\n * stopped at its ceiling and cut to `AI_RAW_OUTPUT_MAX_CHARS`. The\n * streaming counterpart of `AiCompletion.rawOutput`.\n *\n * A `tool` event carries what `aiToolInputOf` could still parse, which\n * for a call cut mid-answer is a prefix of the object or nothing at\n * all; the fragments those events were assembled from are the only\n * record of what the model actually emitted, and without them a\n * runaway string and a decoding stall look identical from the\n * outside. These are the model's own words, so they belong to the\n * trace alone: never to a reader, and never to a log, which carries\n * the figures that separate the two shapes.\n */\n rawOutput?: string\n }\n\n/** What a provider says about one of its models; the catalog carries both rates. */\nexport interface AiModelDescriptor {\n id: string\n /** The provider id the model is served by. */\n provider: string\n label: string\n capabilities: {\n streaming: boolean\n tools: boolean\n /** Whether the provider accepts an explicit `thinking` setting for this model. */\n thinking: boolean\n /** Whether the provider caches a system prefix for this model. */\n promptCache: boolean\n /**\n * Whether the model reads a picture in a user turn (AGL-2916). Absent\n * reads as no, so a provider registered before the field existed is never\n * sent an image it did not say it can read.\n */\n vision?: boolean\n }\n}\n\n/** The one sentence a customer may read when a provider fails. */\nexport const AI_UPSTREAM_FAILURE_COPY = 'The AI request failed — try again.'\n\n/**\n * A non-2xx answer from the provider. `message` is the fixed customer-safe\n * sentence; the provider's payload is in the log under `requestId`.\n */\nexport class AiUpstreamError extends UpstreamServiceError {\n override readonly name = 'AiUpstreamError'\n /**\n * A failure about the PLATFORM's account with the provider rather than the\n * request (AGL-3377): the key was refused, or the account is out of\n * credit. Every AI feature on the install fails until the operator fixes\n * it, so the runtime tells them. Each adapter classifies its own vendor's\n * answers; absent is an ordinary failure.\n */\n readonly accountProblem: AiAccountProblem | null\n\n constructor(\n status: number | null,\n retryable: boolean,\n requestId: string | null,\n accountProblem: AiAccountProblem | null = null,\n ) {\n super(AI_UPSTREAM_FAILURE_COPY, { status, retryable, requestId })\n this.accountProblem = accountProblem\n }\n}\n\n/** See {@link AiUpstreamError.accountProblem}. */\nexport type AiAccountProblem = 'credentials' | 'credit'\n\n/**\n * A request the runtime refuses to send. Thrown synchronously before any\n * network is touched, and it names the block at fault: this is a\n * programming error in the caller's prompt assembly, not a condition to\n * handle at runtime.\n */\nexport class AiRequestShapeError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'AiRequestShapeError'\n }\n}\n\n/**\n * A provider: one vendor's wire protocol behind the contract. `id` is the\n * routing key (`anthropic`, `openai-compatible`); `models` is what the\n * provider can serve; `complete` and `stream` are the two shapes every door\n * uses. An adapter maps its vendor's errors onto `AiUpstreamError` and its\n * declines onto `AiRefusal`, and reports usage in the meter's shape.\n */\nexport interface AiProvider {\n readonly id: string\n readonly label: string\n /** The environment variable that carries this provider's key, for operator messages. */\n readonly apiKeyEnv: string\n /**\n * The key from the server environment, or `undefined` when the provider\n * is not configured to answer. Each adapter reads its own variable by\n * name in its own module, so every key read is one the key-exposure\n * guard can see (AGL-2240) and no door names a vendor's variable.\n */\n readApiKey(): string | undefined\n /** The host the provider is reached at, for the subprocessor inventory. */\n readonly endpointHost: string\n /**\n * How much of one request's strict tools this provider compiles, where it\n * states a bound. The doors never read it: a spec holds every tool set a\n * door sends to every registered provider's limits, so a schema a provider\n * would refuse is red before it ships rather than a 400 on every request.\n */\n readonly toolSchemaLimits?: AiToolSchemaLimits\n models(): readonly AiModelDescriptor[]\n complete(request: AiProviderRequest): Promise<AiResult>\n /**\n * Resolves once the provider has ACCEPTED the request — after the\n * headers, before the first token — with an iterator of events. A non-2xx\n * answer rejects with `AiUpstreamError` before the iterator exists, so a\n * door can still send a plain error status and hand back its reservation.\n */\n stream(request: AiProviderRequest): Promise<AsyncIterable<AiStreamEvent>>\n /**\n * Many requests answered together, asynchronously, at the provider's batch\n * discount, where it has one: each entry is that request's result, or the\n * error it failed with, in the order asked. Only a development run's live\n * eval calls it (`runtime/ai-live-batch.ts`); no door does.\n */\n completeBatch?(requests: readonly AiProviderRequest[], options?: AiBatchOptions): Promise<Array<AiResult | Error>>\n}\n\n/** How a batch is waited on. */\nexport interface AiBatchOptions {\n /** Between two looks at a batch still processing; 10 s when absent. */\n pollMs?: number\n /**\n * The most a batch is waited for; an hour when absent. Past it the batch\n * is canceled, which keeps every answer it already has, and each request\n * it did not answer comes back as an `AiBatchUnfinishedError` (AGL-3781).\n */\n timeoutMs?: number\n /** How long a canceled batch is given to end, so its answers can be read; 10 minutes when absent. */\n cancelGraceMs?: number\n}\n\n/**\n * A request a batch did not answer (AGL-3781): canceled when its wait ran\n * out, expired, errored, or missing from the results. Not the request's own\n * failure — the caller answers it with a request of its own rather than\n * losing it, and a failure that request meets is the real one.\n */\nexport class AiBatchUnfinishedError extends Error {\n override readonly name = 'AiBatchUnfinishedError'\n constructor(\n readonly batchId: string,\n readonly reason: string,\n ) {\n super(`batch ${batchId} did not answer this request (${reason})`)\n }\n}\n\n/** A count off the wire: finite, non-negative, whole; anything else is 0. */\nexport function aiTokenCount(value: unknown): number {\n const parsed = Number(value ?? 0)\n return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : 0\n}\n\n/**\n * A tool call's `input` as an object. A provider sends an object or, for\n * the one shape a stream assembles by hand, a JSON string.\n *\n * A string that does not parse to an object is an EMPTY input, not a throw.\n * The case that produces one is a stream cut off mid-call by the output\n * ceiling, and a throw there would end the stream before its `done` event —\n * the event that carries the usage a door must meter for tokens already\n * spent. An empty input is one every door's own validation refuses.\n */\nexport function aiToolInputOf(value: unknown): Record<string, unknown> {\n if (typeof value === 'string') {\n if (!value.trim()) return {}\n try {\n const parsed: unknown = JSON.parse(value)\n return parsed && typeof parsed === 'object' && !Array.isArray(parsed)\n ? (parsed as Record<string, unknown>)\n : {}\n } catch {\n return {}\n }\n }\n return value && typeof value === 'object' ? (value as Record<string, unknown>) : {}\n}\n"],"names":["UpstreamServiceError","compiledForm","node","root","resolving","Array","isArray","map","entry","isSchemaObject","ref","has","$ref","localSchemaAt","Set","written","key","value","Object","entries","aiToolSchemaCompiledBytes","tools","reduce","bytes","tool","JSON","stringify","inputSchema","length","startsWith","undefined","slice","split","replace","countSchema","counts","unionParameters","properties","required","property","includes","optionalParameters","items","item","branches","branch","aiToolSchemaCounts","strictTools","compiledSchemaBytes","TOOL_SCHEMA_MEASURES","aiToolSchemaBreaches","limits","flatMap","measure","one","many","source","limit","count","AI_IMAGE_MEDIA_TYPES","AI_IMAGE_MAX_BYTES","AI_REQUEST_MAX_IMAGES","aiMessageText","message","content","filter","part","type","text","join","aiMessageImages","messages","aiBase64Bytes","data","padding","endsWith","Math","max","floor","aiCacheScopeOf","block","site","aiCacheMarkerTtls","request","ceiling","cacheTtl","lifetime","scope","system","cacheBreakpoint","conversation","cacheConversation","aiStoppedAtCeiling","stopReason","AI_RAW_OUTPUT_MAX_CHARS","aiRawOutputOf","output","aiCutOffFigures","call","parsed","outputTokens","usage","thinkingTokens","parsedChars","rawChars","rawOutput","AI_UPSTREAM_FAILURE_COPY","AiUpstreamError","status","retryable","requestId","accountProblem","name","AiRequestShapeError","Error","AiBatchUnfinishedError","batchId","reason","aiTokenCount","Number","isFinite","aiToolInputOf","trim","parse"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/providers/contract.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 { UpstreamServiceError } from '@aglyn/shared-util-errors'\n\n/**\n * The provider contract (AGL-2939): the one shape every AI door speaks, and\n * the one thing an adapter has to implement. The runtime, the gate, the\n * jobs, the doctrine and the usage code import this and never a vendor\n * URL, header or model literal — Anthropic is one adapter behind it, and\n * an OpenAI-compatible endpoint is the second, which is what proves the\n * seam holds.\n *\n * ── Prompt caching, by construction ───────────────────────────────────────\n *\n * `system` is an ordered list of blocks, and caching is a PREFIX match: a\n * breakpoint covers every block before it, and one byte that differs per\n * request anywhere inside that prefix gives every distinct value its own\n * copy of it. The AGL-2352 rule is that no per-org byte may sit inside a\n * cached prefix — a white-label brand name interpolated into a cached block\n * multiplies the prefix count by the number of brands, each written off its\n * own traffic and expired before it is read. So a block marked `volatile`\n * may not sit at or before the last `cacheBreakpoint`, and the runtime\n * refuses the request before a byte leaves the process. A provider with no\n * cache honors the order and ignores the markers.\n *\n * ── Structured output ─────────────────────────────────────────────────────\n *\n * A `tools` entry is sent strict, with additional properties forbidden on\n * its schema, so a tool call's `input` validates against the schema\n * exactly. Tool choice stays automatic — forced tool use is not portable\n * across models — and the caller's own words name the tool it wants. A door\n * that sends several tools may also name the one a call is for\n * (`toolChoice`), which an adapter forces only on a model whose descriptor\n * says it takes a forced choice (`forcedToolChoice`) and only with thinking\n * off, and sends as automatic everywhere else (AGL-4076). An\n * adapter delivers `input` as an object; a stream assembles it from\n * fragments and parses the whole once, at the block's end. Nothing\n * string-matches the serialized form: models differ in how they escape it.\n * A provider bounds how much strict schema one request may carry, and an\n * adapter declares those bounds as `toolSchemaLimits`: three bounds a vendor\n * publishes and counts, and a fourth, the size of the grammar the schemas\n * compile to, that none publishes and only a live request settles.\n *\n * ── Thinking ──────────────────────────────────────────────────────────────\n *\n * `thinking: 'off'` and `'adaptive'` are sent when the provider has the\n * control; an omitted field sends NOTHING, leaving the model's own default\n * in force. The omission is deliberate rather than a gap: some models\n * reject an explicit setting, and the copy assistant serves its element\n * mode on such a model. `effort` rides the same rule.\n *\n * ── Pictures ──────────────────────────────────────────────────────────────\n *\n * A user turn may carry pictures beside its text (AGL-2916): `content` is\n * then an ordered list of parts, and a turn with nothing but text stays a\n * string, which every adapter passes through as it always has. A picture is\n * base64 bytes of one of `AI_IMAGE_MEDIA_TYPES`, never a URL, so no provider\n * fetches anything on the platform's behalf. The runtime refuses a picture\n * on an assistant turn, a picture for a model whose descriptor does not say\n * `vision`, and a picture past the size or count bounds, all before any\n * network is touched. Each adapter maps the parts onto its vendor's shape.\n */\n\n/**\n * One system block. `cacheBreakpoint` marks the end of a cacheable prefix;\n * `volatile` declares that the text carries a per-request or per-org byte\n * and therefore may never sit inside one.\n *\n * `site` declares a block that describes ONE site (AGL-3661): its business\n * profile, status and remembered preferences. As a breakpoint it is cached\n * once per site, so it must close the cached span — every platform-wide\n * block sits ahead of it, where every workspace still shares one entry —\n * and the runtime refuses a request that puts another breakpoint after it.\n */\nexport interface AiSystemBlock {\n text: string\n cacheBreakpoint?: true\n volatile?: true\n site?: true\n}\n\n/** A structured-output tool. `strict` is required, not optional. */\nexport interface AiTool {\n name: string\n description: string\n /** A JSON schema of `type: 'object'`; `additionalProperties` is forced off. */\n inputSchema: Record<string, unknown>\n strict: true\n}\n\n/**\n * How much of a request's strict tools a provider can compile (AGL-3096). A\n * provider that constrains decoding to a tool's schema compiles every strict\n * schema of a request together, before the model runs, and refuses a request\n * whose schemas are past its bounds. That refusal is permanent: the schema is\n * the same on every retry. Each bound is a TOTAL over every strict tool one\n * request sends, as `aiToolSchemaCounts` counts them. A bound left out is one\n * the provider does not state.\n *\n * A provider may also refuse a request whose schemas compile to too large a\n * grammar, which none of the published counts can see: a request counting 1\n * strict tool, 0 optional parameters and 1 union was refused for exactly that\n * (AGL-3096). `compiledSchemaBytes` stands in for it, on live evidence.\n */\nexport interface AiToolSchemaLimits {\n /** The most strict tools one request may send. */\n strictTools?: number\n /** The most properties, across every strict schema, left out of their object's `required`. */\n optionalParameters?: number\n /** The most schemas, across every strict schema, that are a union: an `anyOf` or `oneOf`, or a list of types. */\n unionParameters?: number\n /**\n * The most bytes of schema a request's grammar may be compiled from, as\n * `aiToolSchemaCompiledBytes` measures them.\n *\n * Unlike the bounds above, this one is EMPIRICAL. A provider refuses a\n * request whose compiled grammar is too large without publishing the size,\n * so the number here is not read off a vendor's page: it is the largest a\n * live request has been watched to carry, and every number above it is\n * unknown rather than safe. Raising it on an offline measurement alone\n * puts the guard back where it could not see the break.\n */\n compiledSchemaBytes?: number\n}\n\n/** A request's strict tools, counted on the measures `AiToolSchemaLimits` bounds. */\nexport type AiToolSchemaCounts = Required<AiToolSchemaLimits>\n\n/**\n * One schema as a grammar compiler reads it: every local `$ref` written out\n * at each place it is used, and every description, title and `$defs` dropped,\n * since none of them constrains a single token. A schema that refers to\n * itself is written out once around and then left as the reference it is.\n */\nfunction compiledForm(node: unknown, root: Record<string, unknown>, resolving: ReadonlySet<string>): unknown {\n if (Array.isArray(node)) return node.map((entry) => compiledForm(entry, root, resolving))\n if (!isSchemaObject(node)) return node\n const ref = node['$ref']\n if (typeof ref === 'string') {\n if (resolving.has(ref)) return { $ref: ref }\n return compiledForm(localSchemaAt(root, ref), root, new Set([...resolving, ref]))\n }\n const written: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(node)) {\n if (key === 'description' || key === 'title' || key === '$defs') continue\n written[key] = compiledForm(value, root, resolving)\n }\n return written\n}\n\n/**\n * The bytes of schema a request's grammar is compiled from, over all its\n * strict tools. It is a stand-in for the size of the grammar itself, which\n * nothing outside the provider can measure, and it errs toward more: a `$ref`\n * counts at each place it is used, because a compiler that writes it out\n * there pays for it there.\n */\nexport function aiToolSchemaCompiledBytes(tools: readonly AiTool[]): number {\n return tools.reduce(\n (bytes, tool) => bytes + JSON.stringify(compiledForm(tool.inputSchema, tool.inputSchema, new Set())).length,\n 0,\n )\n}\n\nfunction isSchemaObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\n/** The schema a local `$ref` (`#/…`) points at, or `undefined`. */\nfunction localSchemaAt(root: Record<string, unknown>, ref: string): unknown {\n if (!ref.startsWith('#/')) return undefined\n return ref\n .slice(2)\n .split('/')\n .map((key) => key.replace(/~1/g, '/').replace(/~0/g, '~'))\n .reduce<unknown>((node, key) => (isSchemaObject(node) ? node[key] : undefined), root)\n}\n\nfunction countSchema(\n node: unknown,\n root: Record<string, unknown>,\n counts: AiToolSchemaCounts,\n resolving: ReadonlySet<string>,\n): void {\n if (!isSchemaObject(node)) return\n if (Array.isArray(node['anyOf']) || Array.isArray(node['oneOf']) || Array.isArray(node['type'])) {\n counts.unionParameters += 1\n }\n const ref = node['$ref']\n if (typeof ref === 'string' && !resolving.has(ref)) {\n countSchema(localSchemaAt(root, ref), root, counts, new Set([...resolving, ref]))\n }\n const properties = node['properties']\n if (isSchemaObject(properties)) {\n const required = Array.isArray(node['required']) ? node['required'] : []\n for (const [key, property] of Object.entries(properties)) {\n if (!required.includes(key)) counts.optionalParameters += 1\n countSchema(property, root, counts, resolving)\n }\n }\n const items = node['items']\n for (const item of Array.isArray(items) ? items : [items]) countSchema(item, root, counts, resolving)\n for (const key of ['anyOf', 'allOf', 'oneOf']) {\n const branches = node[key]\n if (Array.isArray(branches)) for (const branch of branches) countSchema(branch, root, counts, resolving)\n }\n}\n\n/**\n * The strict tools of one request, counted on each measure a provider bounds.\n * Every tool the contract sends is strict. Each schema is walked whole: every\n * property, every `items`, every branch of an `anyOf`, `allOf` or `oneOf`, and\n * a local `$ref` at each place it is used. A union nested in an array's items\n * or in another union's branch counts on its own, as it does for a provider\n * that compiles it; a `$ref` counted where it is used errs toward more.\n */\nexport function aiToolSchemaCounts(tools: readonly AiTool[]): AiToolSchemaCounts {\n const counts: AiToolSchemaCounts = {\n strictTools: tools.length,\n optionalParameters: 0,\n unionParameters: 0,\n compiledSchemaBytes: aiToolSchemaCompiledBytes(tools),\n }\n for (const tool of tools) countSchema(tool.inputSchema, tool.inputSchema, counts, new Set())\n return counts\n}\n\n/**\n * Each bound, as one, as many, and where its number comes from — a published\n * bound reads as the provider's own, a measured one as what has been proved.\n */\nconst TOOL_SCHEMA_MEASURES: ReadonlyArray<[keyof AiToolSchemaLimits, string, string, string]> = [\n ['strictTools', 'strict tool', 'strict tools', 'the provider compiles'],\n ['optionalParameters', 'optional parameter', 'optional parameters', 'the provider compiles'],\n ['unionParameters', 'union-typed parameter', 'union-typed parameters', 'the provider compiles'],\n ['compiledSchemaBytes', 'byte of compiled schema', 'bytes of compiled schema', 'a live request has proved'],\n]\n\n/** Each bound a request's tools are past, as a sentence; empty when every one holds. */\nexport function aiToolSchemaBreaches(\n tools: readonly AiTool[],\n limits: AiToolSchemaLimits | undefined,\n): string[] {\n if (!limits) return []\n const counts = aiToolSchemaCounts(tools)\n return TOOL_SCHEMA_MEASURES.flatMap(([measure, one, many, source]) => {\n const limit = limits[measure]\n const count = counts[measure]\n return limit !== undefined && count > limit\n ? [`${count} ${count === 1 ? one : many} in one request, over the ${limit} ${source}`]\n : []\n })\n}\n\nexport type AiEffort = 'low' | 'medium' | 'high'\nexport type AiThinking = 'off' | 'adaptive'\n\n/**\n * The picture formats a request may carry (AGL-2916): the four every model\n * the catalog marks as reading images accepts. A door converts anything else\n * — an AVIF, an SVG — before it attaches the picture, or attaches none.\n */\nexport const AI_IMAGE_MEDIA_TYPES = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'] as const\nexport type AiImageMediaType = (typeof AI_IMAGE_MEDIA_TYPES)[number]\n\n/**\n * The largest picture one part may carry, in decoded bytes: the ceiling the\n * providers behind the contract accept for one image. A door that attaches a\n * picture resizes it far below this; the runtime refuses anything above it\n * before a byte leaves the process, rather than letting the provider do it\n * after the request was paid for in time.\n */\nexport const AI_IMAGE_MAX_BYTES = 5 * 1024 * 1024\n\n/** The most pictures one request may carry. */\nexport const AI_REQUEST_MAX_IMAGES = 8\n\n/** Text in a message made of parts. */\nexport interface AiTextPart {\n type: 'text'\n text: string\n}\n\n/**\n * A picture in a message (AGL-2916): its bytes, base64-encoded, and their\n * format. Only a USER turn carries one, and only to a model whose descriptor\n * says it reads images; the runtime refuses any other request before a byte\n * leaves the process, so a door cannot send a picture by accident to a model\n * that would reject it, or to a turn the model wrote.\n */\nexport interface AiImagePart {\n type: 'image'\n mediaType: AiImageMediaType\n /** The picture's bytes, base64-encoded, with no `data:` prefix. */\n data: string\n}\n\nexport type AiMessagePart = AiTextPart | AiImagePart\n\nexport interface AiMessage {\n role: 'user' | 'assistant'\n /**\n * The turn's text, or its parts in order — text and pictures — for a door\n * that shows the model an image. A door with nothing but text sends a\n * string, which every adapter passes through as it always has.\n */\n content: string | readonly AiMessagePart[]\n}\n\n/** A message's text: its parts' text joined, with every picture left out. */\nexport function aiMessageText(message: Pick<AiMessage, 'content'>): string {\n if (typeof message.content === 'string') return message.content\n return message.content\n .filter((part): part is AiTextPart => part.type === 'text')\n .map((part) => part.text)\n .join('\\n\\n')\n}\n\n/** Every picture a list of messages carries, in order. */\nexport function aiMessageImages(messages: readonly Pick<AiMessage, 'content'>[]): AiImagePart[] {\n return messages.flatMap((message) =>\n typeof message.content === 'string'\n ? []\n : message.content.filter((part): part is AiImagePart => part.type === 'image'),\n )\n}\n\n/** The decoded size of a base64 string, without decoding it. */\nexport function aiBase64Bytes(data: string): number {\n const padding = data.endsWith('==') ? 2 : data.endsWith('=') ? 1 : 0\n return Math.max(0, Math.floor((data.length * 3) / 4) - padding)\n}\n\n/** What every door hands the runtime; `provider` and `model` resolve there. */\nexport interface AiRequestBase {\n model: string\n system: AiSystemBlock[]\n messages: AiMessage[]\n tools?: AiTool[]\n maxTokens: number\n effort?: AiEffort\n thinking?: AiThinking\n signal?: AbortSignal\n /**\n * How long a cache entry the SHARED breakpoints write should live, where\n * the provider offers a choice: the provider's default (five minutes on the\n * Messages API) when absent. A step a site job runs asks for `1h` through\n * its routing row (`AiRoutingRow.cacheTtl`, AGL-3830), in production too,\n * and a development live run asks for it on every request\n * (`runtime/ai-dev-env.ts`). A job's own markers keep five minutes whatever\n * this says (`aiCacheMarkerTtls`, AGL-4050). It changes no answer, only what\n * a request within the hour pays for the prefix, so a replay key does not\n * read it.\n */\n cacheTtl?: '1h'\n /**\n * Cache the conversation through its last message as well as the system\n * blocks (AGL-3809), for a request the door expects to continue: a\n * generation that looks the site up or is asked again re-sends its whole\n * first turn, which without this is billed at the full input rate on every\n * continuation. One more breakpoint, on the last message, counted against\n * the same limit as the system's; a provider without a cache ignores it.\n * It changes no answer, so a replay key does not read it.\n */\n cacheConversation?: boolean\n /**\n * The tool this call is for, by name: one of `tools` (AGL-4076). A step\n * whose calls each want a different tool sends every one of them on every\n * call, so the tools that open the cached prefix are the same bytes each\n * time and the later calls read the prefix the first one wrote; this names\n * the one wanted now. A tool choice sits outside the cached prefix, so\n * naming a different one costs no cache read. The adapter forces it only\n * where the model's descriptor says `forcedToolChoice` and the request\n * thinks with thinking off, which a forced choice requires; everywhere else\n * the choice stays automatic and the door's own words name the tool.\n */\n toolChoice?: string\n}\n\n/** The request an adapter receives: the runtime has already validated it. */\nexport interface AiProviderRequest extends AiRequestBase {\n /** The provider's own key, resolved by the runtime from the environment or the org's setting. */\n apiKey: string\n}\n\n/** How long one cache marker's entry lives: the provider's five minutes, or the hour. */\nexport type AiCacheTtl = '5m' | '1h'\n\n/**\n * Who reads a cache marker's entry (AGL-4050). A `shared` marker closes a\n * span of the platform's own prompt (the tools, the doctrine, a door's\n * rules), the same bytes for every org and every job, so the next job can\n * read what this one wrote: it lives as long as the request's `cacheTtl`\n * asks. A `job` marker closes bytes only one job reads: a site's block\n * (`site`) or the request's own conversation (`cacheConversation`). That job\n * reads it again within minutes and no other job ever does, so it keeps the\n * provider's five minutes whatever the route asks; at the hour, its 2x write\n * would never be read back.\n */\nexport type AiCacheScope = 'shared' | 'job'\n\n/** A system block's marker scope: a site's block is one job's, every other block the platform's. */\nexport function aiCacheScopeOf(block: Pick<AiSystemBlock, 'site'>): AiCacheScope {\n return block.site ? 'job' : 'shared'\n}\n\n/**\n * The lifetime of every cache marker a request carries (AGL-4050), in prefix\n * order: one per system block, `null` where the block marks none, then the\n * conversation's, `null` where it is not cached. Tools carry no marker of\n * their own; they sit ahead of the first system marker and live as long as it.\n *\n * A provider refuses a request whose longer-lived entry comes after a\n * shorter-lived one, so once a marker is five minutes every marker after it\n * is too. A shared marker behind a job's is downgraded, never sent at the\n * hour. The runtime already keeps a site's block last among the system\n * breakpoints, so a door's request never meets the downgrade; it is the\n * adapter's guarantee all the same.\n */\nexport function aiCacheMarkerTtls(\n request: Pick<AiRequestBase, 'system' | 'messages' | 'cacheTtl' | 'cacheConversation'>,\n): { system: Array<AiCacheTtl | null>; conversation: AiCacheTtl | null } {\n let ceiling: AiCacheTtl = request.cacheTtl === '1h' ? '1h' : '5m'\n const lifetime = (scope: AiCacheScope): AiCacheTtl => {\n if (scope === 'job') ceiling = '5m'\n return ceiling\n }\n const system = request.system.map((block) => (block.cacheBreakpoint ? lifetime(aiCacheScopeOf(block)) : null))\n const conversation = request.cacheConversation && request.messages.length > 0 ? lifetime('job') : null\n return { system, conversation }\n}\n\n/**\n * Token usage in the meter's shape. Cache reads and writes are separate\n * because they are priced separately; a provider with no cache reports 0.\n */\nexport interface AiUsage {\n inputTokens: number\n outputTokens: number\n cacheReadTokens: number\n cacheWriteTokens: number\n /**\n * How many of `cacheWriteTokens` went to an hour-long cache entry\n * (AGL-3830), where the provider breaks the writes down; absent where none\n * did or the provider does not say.\n *\n * Inside `cacheWriteTokens`, never beside it, like `thinkingTokens` below.\n * An hour-long write is billed at twice the input price where a rate\n * table's `cacheWritePerToken` is the five-minute 1.25 times, so a price\n * reads this to add the difference (`aiCacheWriteHourSurchargeUsd`).\n */\n cacheWriteHourTokens?: number\n /**\n * How many of `outputTokens` the model spent thinking, where the provider\n * reports it; absent where it does not (AGL-3143).\n *\n * Inside `outputTokens`, never beside it: a rate table prices the four\n * figures above and would charge these twice if it read this one.\n *\n * It is here because it is the figure that says where a generation's\n * output went. A tool call that comes back far smaller than the tokens it\n * cost has three shapes, and only this separates the first from the other\n * two: the model thought and then answered briefly, which is a model\n * working as asked; a runaway string the partial parse discarded; or\n * decoding that wrote nothing usable. Kept on every answer, because a\n * generation burns its budget thinking without ever reaching its ceiling,\n * and `rawOutput` is only there when it does.\n */\n thinkingTokens?: number\n}\n\nexport interface AiToolUse {\n name: string\n input: Record<string, unknown>\n}\n\n/** A completed, non-streaming answer. */\nexport interface AiCompletion {\n kind: 'completion'\n /** Every text block, joined. */\n text: string\n toolUse: AiToolUse[]\n usage: AiUsage\n /**\n * The exchange at the serving model's BILLED rates — what a customer's\n * credits are drawn from, and never a margin's cost side (AGL-3015). What\n * the same exchange cost us is `estimateAiProviderCostUsd`, recorded by\n * the meter; a result travels no further than the credit path, so it\n * carries the one figure that path needs.\n */\n estCostUsd: number\n stopReason: string | null\n /**\n * WHERE THE OUTPUT WENT (AGL-3143). The provider's own output for this\n * call, as it answered it, kept ONLY when the call stopped at its ceiling\n * and cut to `AI_RAW_OUTPUT_MAX_CHARS`.\n *\n * A tool call truncated mid-answer reaches `toolUse` as whatever the\n * provider could still parse, and the parsed input alone cannot say where\n * the tokens went: a runaway string the partial parse discarded and a\n * decoding stall that wrote nothing usable both leave a small input\n * against a spent ceiling. This is what tells them apart. These are the\n * model's own words, so they belong to the trace alone: never to a reader,\n * and never to a log, which carries the figures that separate the two\n * shapes — `outputTokens`, `parsedChars`, `rawChars`.\n */\n rawOutput?: string\n}\n\n/**\n * Whether a model call stopped because it reached the output ceiling it was\n * asked for (AGL-3042). `max_tokens` is the contract's word, and the\n * OpenAI-compatible adapter maps its endpoint's `length` onto it; `length` is\n * read as well, for an adapter that passes its provider's own word through.\n */\nexport function aiStoppedAtCeiling(stopReason: string | null): boolean {\n return stopReason === 'max_tokens' || stopReason === 'length'\n}\n\n/** The most characters of raw output a call cut off at its ceiling keeps. */\nexport const AI_RAW_OUTPUT_MAX_CHARS = 20_000\n\n/**\n * A cut-off call's output as the trace keeps it: what the provider answered,\n * serialized where it is not already text, and cut to the bound with an\n * ellipsis so a reader can tell a cut trace from a short answer.\n */\nexport function aiRawOutputOf(output: unknown): string {\n const text = typeof output === 'string' ? output : (JSON.stringify(output) ?? '')\n return text.length > AI_RAW_OUTPUT_MAX_CHARS ? `${text.slice(0, AI_RAW_OUTPUT_MAX_CHARS)}…` : text\n}\n\n/**\n * WHERE THE OUTPUT WENT, as a log may carry it (AGL-3143): the figures that\n * separate the shapes a call cut off at its ceiling comes in, and nothing\n * else.\n *\n * Thinking accounts for the spend first, because the thinking budget is\n * drawn from the same ceiling as the answer: a call can spend nearly all of\n * one thinking and still be cut off with a correct, tiny tool input, which\n * is a ceiling to raise rather than a decoder to fix. What thinking does not\n * account for is told apart by how much was written — long, and a runaway\n * string the partial parse discarded; short, and decoding wrote nothing\n * usable.\n *\n * It takes the raw output rather than a count on purpose. The bytes are the\n * model's own words and therefore the site's content, so they belong to the\n * trace alone and never to a log; passing them through one function that can\n * only ever return their LENGTH is what holds that rule at every door,\n * instead of asking each one to remember it.\n */\nexport function aiCutOffFigures(call: {\n usage: AiUsage\n /** The tool input that still parsed, or null where none did. */\n parsed: unknown\n rawOutput: string | null | undefined\n}): {\n outputTokens: number\n thinkingTokens: number | null\n parsedChars: number\n rawChars: number | null\n} {\n const parsed =\n call.parsed === null || call.parsed === undefined ? '' : (JSON.stringify(call.parsed) ?? '')\n return {\n outputTokens: call.usage.outputTokens,\n thinkingTokens: call.usage.thinkingTokens ?? null,\n parsedChars: parsed.length,\n rawChars: call.rawOutput?.length ?? null,\n }\n}\n\n/**\n * The model declined. Tokens were spent, so the usage is real and the\n * caller meters it; the text is whatever partial answer preceded the stop,\n * usually nothing. Every adapter maps its provider's own decline signal —\n * a refusal stop, a content filter — onto this one kind.\n */\nexport interface AiRefusal {\n kind: 'refusal'\n text: string\n usage: AiUsage\n estCostUsd: number\n stopReason: 'refusal'\n}\n\nexport type AiResult = AiCompletion | AiRefusal\n\nexport type AiStreamEvent =\n | { type: 'delta'; text: string }\n | { type: 'tool'; name: string; input: Record<string, unknown> }\n /**\n * The provider's in-stream error. The stream is still open and ends with\n * `done`, so the caller decides what the reader is told; the provider's\n * own words went to the log, not here.\n */\n | { type: 'error'; retryable: boolean }\n | {\n type: 'done'\n usage: AiUsage\n estCostUsd: number\n stopReason: string | null\n /**\n * WHERE THE OUTPUT WENT, for a stream (AGL-3143): the bytes the\n * stream's tool calls were written in, kept ONLY when the stream\n * stopped at its ceiling and cut to `AI_RAW_OUTPUT_MAX_CHARS`. The\n * streaming counterpart of `AiCompletion.rawOutput`.\n *\n * A `tool` event carries what `aiToolInputOf` could still parse, which\n * for a call cut mid-answer is a prefix of the object or nothing at\n * all; the fragments those events were assembled from are the only\n * record of what the model actually emitted, and without them a\n * runaway string and a decoding stall look identical from the\n * outside. These are the model's own words, so they belong to the\n * trace alone: never to a reader, and never to a log, which carries\n * the figures that separate the two shapes.\n */\n rawOutput?: string\n }\n\n/** What a provider says about one of its models; the catalog carries both rates. */\nexport interface AiModelDescriptor {\n id: string\n /** The provider id the model is served by. */\n provider: string\n label: string\n capabilities: {\n streaming: boolean\n tools: boolean\n /** Whether the provider accepts an explicit `thinking` setting for this model. */\n thinking: boolean\n /** Whether the provider caches a system prefix for this model. */\n promptCache: boolean\n /**\n * Whether the model reads a picture in a user turn (AGL-2916). Absent\n * reads as no, so a provider registered before the field existed is never\n * sent an image it did not say it can read.\n */\n vision?: boolean\n /**\n * Whether the model takes a forced tool choice (AGL-4076): a request that\n * names the one tool it must call. Absent reads as no, so a request that\n * names its tool is sent an automatic choice instead. Claude Opus 5.5 and\n * Claude Sonnet 5.5 refuse a forced choice outright.\n */\n forcedToolChoice?: boolean\n }\n}\n\n/** The one sentence a customer may read when a provider fails. */\nexport const AI_UPSTREAM_FAILURE_COPY = 'The AI request failed — try again.'\n\n/**\n * A non-2xx answer from the provider. `message` is the fixed customer-safe\n * sentence; the provider's payload is in the log under `requestId`.\n */\nexport class AiUpstreamError extends UpstreamServiceError {\n override readonly name = 'AiUpstreamError'\n /**\n * A failure about the PLATFORM's account with the provider rather than the\n * request (AGL-3377): the key was refused, or the account is out of\n * credit. Every AI feature on the install fails until the operator fixes\n * it, so the runtime tells them. Each adapter classifies its own vendor's\n * answers; absent is an ordinary failure.\n */\n readonly accountProblem: AiAccountProblem | null\n\n constructor(\n status: number | null,\n retryable: boolean,\n requestId: string | null,\n accountProblem: AiAccountProblem | null = null,\n ) {\n super(AI_UPSTREAM_FAILURE_COPY, { status, retryable, requestId })\n this.accountProblem = accountProblem\n }\n}\n\n/** See {@link AiUpstreamError.accountProblem}. */\nexport type AiAccountProblem = 'credentials' | 'credit'\n\n/**\n * A request the runtime refuses to send. Thrown synchronously before any\n * network is touched, and it names the block at fault: this is a\n * programming error in the caller's prompt assembly, not a condition to\n * handle at runtime.\n */\nexport class AiRequestShapeError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'AiRequestShapeError'\n }\n}\n\n/**\n * A provider: one vendor's wire protocol behind the contract. `id` is the\n * routing key (`anthropic`, `openai-compatible`); `models` is what the\n * provider can serve; `complete` and `stream` are the two shapes every door\n * uses. An adapter maps its vendor's errors onto `AiUpstreamError` and its\n * declines onto `AiRefusal`, and reports usage in the meter's shape.\n */\nexport interface AiProvider {\n readonly id: string\n readonly label: string\n /** The environment variable that carries this provider's key, for operator messages. */\n readonly apiKeyEnv: string\n /**\n * The key from the server environment, or `undefined` when the provider\n * is not configured to answer. Each adapter reads its own variable by\n * name in its own module, so every key read is one the key-exposure\n * guard can see (AGL-2240) and no door names a vendor's variable.\n */\n readApiKey(): string | undefined\n /** The host the provider is reached at, for the subprocessor inventory. */\n readonly endpointHost: string\n /**\n * How much of one request's strict tools this provider compiles, where it\n * states a bound. The doors never read it: a spec holds every tool set a\n * door sends to every registered provider's limits, so a schema a provider\n * would refuse is red before it ships rather than a 400 on every request.\n */\n readonly toolSchemaLimits?: AiToolSchemaLimits\n models(): readonly AiModelDescriptor[]\n complete(request: AiProviderRequest): Promise<AiResult>\n /**\n * Resolves once the provider has ACCEPTED the request — after the\n * headers, before the first token — with an iterator of events. A non-2xx\n * answer rejects with `AiUpstreamError` before the iterator exists, so a\n * door can still send a plain error status and hand back its reservation.\n */\n stream(request: AiProviderRequest): Promise<AsyncIterable<AiStreamEvent>>\n /**\n * Many requests answered together, asynchronously, at the provider's batch\n * discount, where it has one: each entry is that request's result, or the\n * error it failed with, in the order asked. Only a development run's live\n * eval calls it (`runtime/ai-live-batch.ts`); no door does.\n */\n completeBatch?(requests: readonly AiProviderRequest[], options?: AiBatchOptions): Promise<Array<AiResult | Error>>\n}\n\n/** How a batch is waited on. */\nexport interface AiBatchOptions {\n /** Between two looks at a batch still processing; 10 s when absent. */\n pollMs?: number\n /**\n * The most a batch is waited for; an hour when absent. Past it the batch\n * is canceled, which keeps every answer it already has, and each request\n * it did not answer comes back as an `AiBatchUnfinishedError` (AGL-3781).\n */\n timeoutMs?: number\n /** How long a canceled batch is given to end, so its answers can be read; 10 minutes when absent. */\n cancelGraceMs?: number\n}\n\n/**\n * A request a batch did not answer (AGL-3781): canceled when its wait ran\n * out, expired, errored, or missing from the results. Not the request's own\n * failure — the caller answers it with a request of its own rather than\n * losing it, and a failure that request meets is the real one.\n */\nexport class AiBatchUnfinishedError extends Error {\n override readonly name = 'AiBatchUnfinishedError'\n constructor(\n readonly batchId: string,\n readonly reason: string,\n ) {\n super(`batch ${batchId} did not answer this request (${reason})`)\n }\n}\n\n/** A count off the wire: finite, non-negative, whole; anything else is 0. */\nexport function aiTokenCount(value: unknown): number {\n const parsed = Number(value ?? 0)\n return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : 0\n}\n\n/**\n * A tool call's `input` as an object. A provider sends an object or, for\n * the one shape a stream assembles by hand, a JSON string.\n *\n * A string that does not parse to an object is an EMPTY input, not a throw.\n * The case that produces one is a stream cut off mid-call by the output\n * ceiling, and a throw there would end the stream before its `done` event —\n * the event that carries the usage a door must meter for tokens already\n * spent. An empty input is one every door's own validation refuses.\n */\nexport function aiToolInputOf(value: unknown): Record<string, unknown> {\n if (typeof value === 'string') {\n if (!value.trim()) return {}\n try {\n const parsed: unknown = JSON.parse(value)\n return parsed && typeof parsed === 'object' && !Array.isArray(parsed)\n ? (parsed as Record<string, unknown>)\n : {}\n } catch {\n return {}\n }\n }\n return value && typeof value === 'object' ? (value as Record<string, unknown>) : {}\n}\n"],"names":["UpstreamServiceError","compiledForm","node","root","resolving","Array","isArray","map","entry","isSchemaObject","ref","has","$ref","localSchemaAt","Set","written","key","value","Object","entries","aiToolSchemaCompiledBytes","tools","reduce","bytes","tool","JSON","stringify","inputSchema","length","startsWith","undefined","slice","split","replace","countSchema","counts","unionParameters","properties","required","property","includes","optionalParameters","items","item","branches","branch","aiToolSchemaCounts","strictTools","compiledSchemaBytes","TOOL_SCHEMA_MEASURES","aiToolSchemaBreaches","limits","flatMap","measure","one","many","source","limit","count","AI_IMAGE_MEDIA_TYPES","AI_IMAGE_MAX_BYTES","AI_REQUEST_MAX_IMAGES","aiMessageText","message","content","filter","part","type","text","join","aiMessageImages","messages","aiBase64Bytes","data","padding","endsWith","Math","max","floor","aiCacheScopeOf","block","site","aiCacheMarkerTtls","request","ceiling","cacheTtl","lifetime","scope","system","cacheBreakpoint","conversation","cacheConversation","aiStoppedAtCeiling","stopReason","AI_RAW_OUTPUT_MAX_CHARS","aiRawOutputOf","output","aiCutOffFigures","call","parsed","outputTokens","usage","thinkingTokens","parsedChars","rawChars","rawOutput","AI_UPSTREAM_FAILURE_COPY","AiUpstreamError","status","retryable","requestId","accountProblem","name","AiRequestShapeError","Error","AiBatchUnfinishedError","batchId","reason","aiTokenCount","Number","isFinite","aiToolInputOf","trim","parse"],"mappings":"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"}
|
|
@@ -107,8 +107,11 @@ import { aiProviderById, defaultAiProvider } from "./registry.js";
|
|
|
107
107
|
// A scoped, latency-sensitive chat turn: no thinking, the low effort rung.
|
|
108
108
|
thinking: 'off',
|
|
109
109
|
effort: 'low',
|
|
110
|
-
|
|
111
|
-
|
|
110
|
+
// 2,048 since AGL-4075: a non-Latin script spends two to four times the
|
|
111
|
+
// tokens per character, and 5 of 17 non-English answers stopped at 1,024
|
|
112
|
+
// against 1 of 33 English ones. Output is billed only as it is written.
|
|
113
|
+
maxTokens: 2048,
|
|
114
|
+
maxTokensBasis: 'a docs-grounded answer of a few short paragraphs in any language; the edit rung raises it for its tool call',
|
|
112
115
|
eval: {
|
|
113
116
|
kinds: [
|
|
114
117
|
'chat'
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/providers/routing.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { AI_SETTING_PLATFORM, AI_STEP_MODEL_SETTING } from '../plugin-config'\nimport {\n AI_MODEL_CATALOG,\n AI_STEP_TIERS,\n aiCatalogEntry,\n aiDefaultModelFor,\n type AiStepKind,\n} from './catalog'\nimport type { AiEvalKind } from '../runtime/ai-eval'\nimport type { AiEffort, AiProvider, AiThinking } from './contract'\nimport { aiProviderById, defaultAiProvider } from './registry'\n\n/**\n * The routing table (AGL-2937, AGL-2939): step kind → provider and model,\n * through the catalog and never a vendor literal.\n *\n * Precedence, dearest override first:\n *\n * 1. the org's `pluginSettings/ai` — a provider, and a model per step kind\n * that must be one the provider serves;\n * 2. the deployment's environment — `AI_PROVIDER` for the provider,\n * `AI_DEFAULT_MODEL` for every step, and `ASSIST_MODEL` for the\n * assistant alone (the incident-response override the chat door has\n * always honored);\n * 3. the catalog's tier for the step kind on the resolved provider.\n */\n\n/** The environment override for every step kind. */\nexport const AI_DEFAULT_MODEL_ENV = 'AI_DEFAULT_MODEL'\n/** The assistant's own override, honored before the general one. */\nexport const ASSIST_MODEL_ENV = 'ASSIST_MODEL'\n\n/** The org's resolved `pluginSettings/ai` values, as `getPluginConfig` merges them. */\nexport type AiPluginSettings = Record<string, unknown>\n\n/**\n * The model that serves a catalog id (AGL-3781): the id itself, or for a\n * retired row the successor it names, followed to a current row. An id the\n * catalog does not know is returned as it is, for the caller to refuse.\n */\nexport function aiServingModelId(modelId: string): string {\n let current = modelId\n for (let hops = 0; hops < AI_MODEL_CATALOG.length; hops += 1) {\n const successor = aiCatalogEntry(current)?.successor\n if (!successor) return current\n current = successor\n }\n return current\n}\n\nexport interface AiRoute {\n provider: AiProvider\n model: string\n}\n\n/** A settings value that names a real choice, or `undefined` for the platform default. */\nfunction chosen(settings: AiPluginSettings | undefined, key: string): string | undefined {\n const value = settings?.[key]\n return typeof value === 'string' && value && value !== AI_SETTING_PLATFORM\n ? value\n : undefined\n}\n\n/** The provider a workspace runs on: its own choice when registered, else the platform's. */\nexport function resolveAiProvider(settings?: AiPluginSettings): AiProvider | undefined {\n const wanted = chosen(settings, 'provider')\n if (wanted) {\n const named = aiProviderById(wanted)\n if (named) return named\n }\n return defaultAiProvider()\n}\n\n/**\n * The model a step kind runs on for a workspace, on the resolved provider.\n * A chosen model on another provider is not served — the provider decides\n * what it can run — and falls back to the provider's default for the tier.\n */\nexport function resolveAiRoute(\n kind: AiStepKind,\n settings?: AiPluginSettings,\n): AiRoute | undefined {\n const provider = resolveAiProvider(settings)\n if (!provider) return undefined\n // A setting or an override that names a retired model is served by its\n // successor, never by the retired model itself.\n const servedBy = (modelId: string | undefined): string | undefined => {\n const serving = modelId ? aiServingModelId(modelId) : undefined\n return serving && aiCatalogEntry(serving)?.provider === provider.id ? serving : undefined\n }\n const model =\n servedBy(chosen(settings, AI_STEP_MODEL_SETTING[kind])) ??\n (kind === 'assist.chat' ? servedBy(process.env[ASSIST_MODEL_ENV]?.trim()) : undefined) ??\n servedBy(process.env[AI_DEFAULT_MODEL_ENV]?.trim()) ??\n aiDefaultModelFor(provider.id, AI_STEP_TIERS[kind])\n return model ? { provider, model } : undefined\n}\n\n/**\n * What one step kind asks of its model, beside the tier it is served from\n * (`AI_STEP_TIERS` in the catalog): whether it thinks and how hard, its\n * answer ceiling, and the eval score that tier holds on the step's golden\n * briefs (AGL-2937).\n */\nexport interface AiRoutingRow {\n /**\n * `null` sends no setting: the model's own default, which on every current\n * model is adaptive thinking. A row whose ceiling holds the answer alone\n * says `off` (AGL-3781).\n */\n thinking: AiThinking | null\n effort: AiEffort | null\n /** The answer ceiling, thinking included. */\n maxTokens: number\n /** What the ceiling is sized from. */\n maxTokensBasis: string\n eval: AiRoutingEval\n /**\n * How long the step's prompt cache lives (AGL-3830): `1h` on every step a\n * site job runs, in production as in a live run, and the provider's five\n * minutes on a row that leaves it out. A guided start's routes each write\n * their prefix once and read it only inside the job, a beat or more apart,\n * so a five-minute entry is gone before the next job could read it; at an\n * hour the next site job reads what the last one wrote. The write costs 2x\n * the input price instead of 1.25x, so a step no job runs leaves it out.\n * The hour is the SHARED markers' alone (AGL-4050): a site's block and the\n * plan's own conversation are read by one job only, so they keep five\n * minutes on every row (`aiCacheMarkerTtls` in `providers/contract.ts`).\n * Read by `runValidatedGeneration` into the request's `cacheTtl`; a door\n * that sends its own request on the same step (the copy assistant's blog\n * draft) keeps five minutes.\n */\n cacheTtl?: '1h'\n}\n\n/**\n * The cache lifetime every step a site job runs asks for its shared prompt\n * (AGL-3830, AGL-4050): the plan, the look, the header and footer, every page\n * and its listing, the creations, the first posts and products, and the\n * welcome email.\n */\nconst AI_SITE_JOB_CACHE_TTL = '1h' as const\n\nexport interface AiRoutingEval {\n /** The golden-brief kinds the step answers. */\n kinds: readonly AiEvalKind[]\n /** `answers` scores the step's answers; `plans` scores the plans those kinds' answers carry. */\n scores: 'answers' | 'plans'\n /** `authored` while the score is the hand-written references'; `recorded` once a live run records answers. */\n source: 'authored' | 'recorded'\n passRate: number\n /** The mean answer score; `null` for a plan score, which passes or fails. */\n meanScore: number | null\n}\n\n/**\n * THE ROUTING TABLE (AGL-2937): what each step kind asks of the model, in one\n * place, with the evidence beside it.\n *\n * A step runs on the smallest tier that holds its eval floor. A row moves to a\n * cheaper tier, or a lower effort, only when a recorded eval\n * (`npm run eval:ai-live`) holds the floor there; while a row's score is the\n * authored references', it says so, and the row stays where production has\n * run it. `runtime/ai-eval.spec.ts` recomputes every row's score from the\n * harness, so a row cannot claim a score its briefs do not hold.\n *\n * A ceiling is sized from the p95 output the staff signals page reports for\n * the kind once production has served it; until then `maxTokensBasis` names\n * what it is sized from. The same spec holds every reference answer under its\n * row's ceiling at three characters a token, a reading that errs high for a\n * tokenizer that splits finer than four.\n */\nexport const AI_ROUTING_TABLE: Readonly<Record<AiStepKind, AiRoutingRow>> = {\n 'assist.chat': {\n // A scoped, latency-sensitive chat turn: no thinking, the low effort rung.\n thinking: 'off',\n effort: 'low',\n maxTokens: 1024,\n maxTokensBasis:\n 'a docs-grounded answer of a few short paragraphs; the edit rung raises it for its tool call',\n eval: { kinds: ['chat'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 0.9688 },\n },\n 'copy.element': {\n // One element's copy on the fast tier, whose model thinks unless told not\n // to (AGL-3781): the ceiling holds the copy alone, as it always has, and\n // the effort is the model's own.\n thinking: 'off',\n effort: null,\n maxTokens: 1024,\n maxTokensBasis: 'one element of copy, about the length of the text it replaces',\n eval: { kinds: ['element'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 0.9688 },\n },\n 'copy.section': {\n thinking: null,\n effort: null,\n maxTokens: 3000,\n maxTokensBasis: 'a section of four to twelve nodes written as JSON',\n eval: { kinds: ['section'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 0.9688 },\n },\n 'copy.blog': {\n thinking: null,\n effort: null,\n maxTokens: 2048,\n maxTokensBasis: 'one blog post body in markdown-lite',\n eval: { kinds: ['blog'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 0.9688 },\n cacheTtl: AI_SITE_JOB_CACHE_TTL,\n },\n 'generate.section': {\n thinking: null,\n effort: null,\n maxTokens: 3000,\n maxTokensBasis: 'a section of four to twelve nodes written as JSON',\n eval: { kinds: ['section'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 0.9688 },\n },\n 'job.layout': {\n // The plan was reasoned out and confirmed before the step, so it writes without thinking.\n // A site's header and footer in the layout language think adaptively, at\n // low since AGL-3781 (`AI_LAYOUT_LANGUAGE_THINKING`): the live round of\n // 2026-10-14 built 13 of 13 frames at low, none cut off.\n thinking: 'off',\n effort: null,\n maxTokens: 8000,\n maxTokensBasis:\n \"tighter than the doctrine's default for a layout, so one step and its re-ask fit the job beat's budget; the tree is held to its rule 17 budget either way\",\n eval: { kinds: ['layout'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 0.9833 },\n cacheTtl: AI_SITE_JOB_CACHE_TTL,\n },\n 'job.template': {\n // Medium since the golden A/B of 2026-10-14 (AGL-3781): both template\n // briefs passed at medium as at the default, with about a third fewer\n // output tokens.\n thinking: 'off',\n effort: 'medium',\n maxTokens: 8000,\n maxTokensBasis:\n \"tighter than the doctrine's default for a template, so one step and its re-ask fit the job beat's budget; the tree is held to its rule 17 budget either way\",\n eval: { kinds: ['template'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 0.9833 },\n cacheTtl: AI_SITE_JOB_CACHE_TTL,\n },\n 'job.component': {\n // A component's tree and the properties it declares, in one answer, from a\n // plan reasoned out and confirmed before the step (AGL-2908).\n thinking: 'off',\n effort: null,\n maxTokens: 8000,\n maxTokensBasis:\n \"the ceiling the layout and template steps keep, so one step and its re-ask fit the job beat's budget; the tree is held to its rule 17 budget either way\",\n eval: { kinds: ['component'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 0.9833 },\n cacheTtl: AI_SITE_JOB_CACHE_TTL,\n },\n 'job.crm': {\n // CRM by AI (AGL-2917): short answers through a strict tool, written from\n // the facts the CRM reports about the record the member opened, with no\n // thinking. The ceiling fits an inline door, so a member is answered in\n // the request that asked, and has no room for thinking: the fast tier's\n // model thinks unless told not to, so the row says off, and its effort\n // is the low one a short answer read off given facts needs (AGL-3781).\n thinking: 'off',\n effort: 'low',\n maxTokens: 700,\n maxTokensBasis:\n \"the largest answer a CRM tool accepts: sixty columns matched to fields by number, or an email draft at its length limits, at three characters a token with room, as ai-job-crm-step.spec.ts measures it\",\n eval: { kinds: ['crm'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 0.9938 },\n },\n 'job.experiment': {\n // Variants of one piece of copy, or a result explained against a verdict\n // code reached before the request was made (AGL-2914). The judgment is\n // already made either way, so it writes without thinking.\n thinking: 'off',\n effort: null,\n maxTokens: 1500,\n maxTokensBasis:\n \"four variants at the tool's own lengths — a name, a headline, a body and a rationale each — written as JSON, about 3,000 characters at three characters a token with room; an explanation is a headline, four sentences and a next step, well under it, as ai-job-experiment-step.spec.ts measures both\",\n eval: { kinds: ['experiment'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 1 },\n },\n 'job.form': {\n // A form's two halves in one answer: the field declaration and the design\n // that renders it, as JSON, with no extended thinking (AGL-2913). The step\n // asks this ceiling, which is the doctrine's own for a form, and registers\n // the time it takes (AGL-3035).\n thinking: 'off',\n effort: null,\n maxTokens: 6000,\n maxTokensBasis:\n \"the largest form the doctrine's output budget admits, a form's fields, its consent and routing declaration and the design that renders them written as JSON at the wordiest golden's characters a stored byte, under it with room; ai-job-form-step.spec.ts measures it\",\n eval: { kinds: ['form'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 0.9833 },\n cacheTtl: AI_SITE_JOB_CACHE_TTL,\n },\n 'job.insight': {\n // Readers chosen, then their tables phrased (AGL-2915): short sentences\n // over figures code computed, each held to the rows it cites, so no\n // extended thinking. The read call before the answer asks for at most\n // `AI_INSIGHT_READ_MAX_TOKENS` of the same allowance.\n thinking: 'off',\n effort: null,\n maxTokens: 1500,\n maxTokensBasis:\n 'five insights at their 280-character bound, each citing three tables of ten rows, and the gap sentence, written as JSON: about 2,700 characters, under 1,000 tokens at three characters a token, with room',\n eval: { kinds: ['insight'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 0.9688 },\n },\n 'job.page': {\n // One section of a page, written against a plan a member already\n // confirmed, so it writes without thinking (AGL-2907). The ceiling here\n // is the MOST a pass may ask for; each pass sizes its own down from it to\n // what fits the time the job beat can give the step, which on the\n // balanced tier is about half of this. A page in the layout language\n // thinks adaptively, at low since AGL-3781 (`AI_LAYOUT_LANGUAGE_THINKING`):\n // the live round of 2026-10-14 built 39 of 39 pages at low, 64 of 65\n // answers held their rules on the first ask, none cut off.\n thinking: 'off',\n effort: null,\n maxTokens: 2000,\n maxTokensBasis:\n \"a section of a page written as JSON, about forty elements at the step's measured 45 tokens an element; a pass lowers it to the worst case that fits AI_JOB_PAGE_STEP_MINIMUM_MS on the model it runs\",\n eval: { kinds: ['page'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 0.9833 },\n cacheTtl: AI_SITE_JOB_CACHE_TTL,\n },\n 'job.products': {\n // A product's copy from its name, text and photo, or a store's first\n // products, categories and discounts from a brief (AGL-2916): answers held\n // to the storefront claim rules, written with no extended thinking.\n thinking: 'off',\n effort: null,\n maxTokens: 8000,\n maxTokensBasis:\n \"the largest catalog the tool accepts, twelve proposed products at every bound written as JSON, which the balanced tier asks as much of as fits a beat; one product's copy asks 1,500 and categories with discounts 2,000, as ai-job-products-step.spec.ts measures them\",\n eval: {\n kinds: ['product', 'catalog', 'categories'],\n scores: 'answers',\n source: 'authored',\n passRate: 1,\n meanScore: 0.9922,\n },\n cacheTtl: AI_SITE_JOB_CACHE_TTL,\n },\n 'job.email': {\n // One email design from the email palette, written against the brief with\n // no extended thinking (AGL-2912). A campaign job runs the same\n // generation, so it carries the same ceiling.\n thinking: 'off',\n effort: null,\n maxTokens: 6000,\n maxTokensBasis:\n \"an email's node map written as JSON, with three subject lines and three preheaders beside it\",\n eval: { kinds: ['email'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 0.9688 },\n cacheTtl: AI_SITE_JOB_CACHE_TTL,\n },\n 'job.campaign': {\n // The campaign job generates its email through the email step, so it is\n // routed identically; the draft campaign it also writes asks nothing of\n // the model (AGL-2912).\n thinking: 'off',\n effort: null,\n maxTokens: 6000,\n maxTokensBasis:\n \"an email's node map written as JSON, with three subject lines and three preheaders beside it\",\n eval: { kinds: ['email'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 0.9688 },\n },\n 'job.workflow': {\n // An automation drafted from a description, or one explained (AGL-2919):\n // which trigger and which steps a description means is the judgment the\n // step sells, so it thinks before it answers.\n thinking: 'adaptive',\n effort: null,\n maxTokens: 4000,\n maxTokensBasis:\n 'the largest automation the tool accepts, 6,000 characters written out as JSON, at three characters a token with as much again to think in, as ai-job-workflow-step.spec.ts measures it',\n eval: { kinds: ['workflow'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 0.9688 },\n },\n 'job.logic': {\n // A site function or variable proposed, or a function explained\n // (AGL-3603): which conditions and arithmetic a description means is the\n // judgment the step sells, so it thinks before it answers.\n thinking: 'adaptive',\n effort: null,\n maxTokens: 3000,\n maxTokensBasis:\n 'the largest function the tool accepts — twelve parameters, twenty-four operations — is far past any description; a function of eight operations is about 2,400 characters of JSON, at three characters a token with as much again to think in, as ai-job-logic-step.spec.ts measures it',\n eval: { kinds: ['logic'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 1 },\n },\n 'job.edit': {\n // A change to a page or a layout the site has (AGL-3616): which elements\n // a request means, on an outline with no selection, is the judgment the\n // step sells, so it thinks before it answers.\n thinking: 'adaptive',\n effort: null,\n maxTokens: 6144,\n maxTokensBasis:\n \"the Assist edit rung's own ceiling for the proposal's tool call (ASSIST_EDIT_MAX_OUTPUT_TOKENS, 4,096: forty new elements written as name/value pairs), with half as much again to think in, as ai-job-edit-step.spec.ts holds it; the whole of it twice, as a re-ask may, still fits one beat on the balanced tier\",\n eval: { kinds: ['edit'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 1 },\n },\n 'job.seo': {\n // A listing written from the page's own text to fixed lengths, sized for\n // the answer alone: no thinking, which the fast tier's model does unless\n // told not to, at the low effort a short bounded answer needs (AGL-3781).\n // The audit's site proposal and fix batches run on the same row.\n thinking: 'off',\n effort: 'low',\n maxTokens: 1024,\n maxTokensBasis:\n \"the largest listing the tool accepts, at three characters a token with a quarter again, as ai-job-seo-step.spec.ts measures it; an audit's site proposal and fix batches keep ceilings of their own, measured the same way\",\n eval: { kinds: ['seo'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 1 },\n cacheTtl: AI_SITE_JOB_CACHE_TTL,\n },\n 'job.text': {\n // Low since the golden A/B of 2026-10-14 (AGL-3781): the tagline brief\n // passed at low, its copy scored higher than at the default.\n thinking: 'adaptive',\n effort: 'low',\n maxTokens: 1024,\n maxTokensBasis: 'a few paragraphs of copy; a brief asking for more gets a draft to extend',\n eval: { kinds: ['text'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 1 },\n },\n 'job.theme': {\n // Medium since the golden A/B of 2026-10-14 (AGL-3781): the theme brief\n // passed at medium with the default's scores and half its output tokens.\n // A site start's look runs its own setting (`ai-job-site-look.ts`).\n thinking: 'adaptive',\n effort: 'medium',\n maxTokens: 8000,\n maxTokensBasis:\n 'the largest answer the theme tool accepts, about 2,500 tokens of JSON as ai-job-theme-step.spec.ts measures it, with as much again to think in',\n eval: { kinds: ['theme'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 0.9688 },\n cacheTtl: AI_SITE_JOB_CACHE_TTL,\n },\n 'job.plan': {\n // A site's plan is an outline written without thinking (AGL-3594), and a\n // paid site's asks medium (`AI_PAID_SITE_PLAN_EFFORT`, AGL-3781): the live\n // round of 2026-10-14 planned 11 of 11 paid site briefs at medium in 16\n // attempts to the default's 22, $0.129 to $0.164, with no refusal and no\n // rule broken. Every other plan, and a Free site's on the fast tier, asks\n // this row's effort — the model's default — until a round holds it lower.\n thinking: 'adaptive',\n effort: null,\n maxTokens: 8000,\n maxTokensBasis: 'a plan of up to twelve screens with sixteen sections each, with room to think',\n eval: {\n kinds: ['page', 'template', 'component', 'layout', 'form'],\n scores: 'plans',\n source: 'authored',\n passRate: 1,\n meanScore: null,\n },\n cacheTtl: AI_SITE_JOB_CACHE_TTL,\n },\n}\n\n/**\n * What a step kind's row asks of its model, as the fields a request carries:\n * only those the row names, so a row that names none sends none and the\n * model's own default stands.\n */\nexport function aiRouteSettings(kind: AiStepKind): { thinking?: AiThinking; effort?: AiEffort } {\n const row = AI_ROUTING_TABLE[kind]\n return {\n ...(row.thinking ? { thinking: row.thinking } : {}),\n ...(row.effort ? { effort: row.effort } : {}),\n }\n}\n\n/** The model id alone — what a door records on the meter and the job. */\nexport function aiModelForStep(kind: AiStepKind, settings?: AiPluginSettings): string {\n const route = resolveAiRoute(kind, settings)\n if (!route) {\n // Every door gates on the provider being configured before it asks\n // for a model, so this is a wiring fault rather than a customer path.\n throw new Error('no AI provider is registered')\n }\n return route.model\n}\n"],"names":["AI_SETTING_PLATFORM","AI_STEP_MODEL_SETTING","AI_MODEL_CATALOG","AI_STEP_TIERS","aiCatalogEntry","aiDefaultModelFor","aiProviderById","defaultAiProvider","AI_DEFAULT_MODEL_ENV","ASSIST_MODEL_ENV","aiServingModelId","modelId","current","hops","length","successor","chosen","settings","key","value","undefined","resolveAiProvider","wanted","named","resolveAiRoute","kind","servedBy","process","provider","serving","id","model","env","trim","AI_SITE_JOB_CACHE_TTL","AI_ROUTING_TABLE","thinking","effort","maxTokens","maxTokensBasis","eval","kinds","scores","source","passRate","meanScore","cacheTtl","aiRouteSettings","row","aiModelForStep","route","Error"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/providers/routing.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { AI_SETTING_PLATFORM, AI_STEP_MODEL_SETTING } from '../plugin-config'\nimport {\n AI_MODEL_CATALOG,\n AI_STEP_TIERS,\n aiCatalogEntry,\n aiDefaultModelFor,\n type AiStepKind,\n} from './catalog'\nimport type { AiEvalKind } from '../runtime/ai-eval'\nimport type { AiEffort, AiProvider, AiThinking } from './contract'\nimport { aiProviderById, defaultAiProvider } from './registry'\n\n/**\n * The routing table (AGL-2937, AGL-2939): step kind → provider and model,\n * through the catalog and never a vendor literal.\n *\n * Precedence, dearest override first:\n *\n * 1. the org's `pluginSettings/ai` — a provider, and a model per step kind\n * that must be one the provider serves;\n * 2. the deployment's environment — `AI_PROVIDER` for the provider,\n * `AI_DEFAULT_MODEL` for every step, and `ASSIST_MODEL` for the\n * assistant alone (the incident-response override the chat door has\n * always honored);\n * 3. the catalog's tier for the step kind on the resolved provider.\n */\n\n/** The environment override for every step kind. */\nexport const AI_DEFAULT_MODEL_ENV = 'AI_DEFAULT_MODEL'\n/** The assistant's own override, honored before the general one. */\nexport const ASSIST_MODEL_ENV = 'ASSIST_MODEL'\n\n/** The org's resolved `pluginSettings/ai` values, as `getPluginConfig` merges them. */\nexport type AiPluginSettings = Record<string, unknown>\n\n/**\n * The model that serves a catalog id (AGL-3781): the id itself, or for a\n * retired row the successor it names, followed to a current row. An id the\n * catalog does not know is returned as it is, for the caller to refuse.\n */\nexport function aiServingModelId(modelId: string): string {\n let current = modelId\n for (let hops = 0; hops < AI_MODEL_CATALOG.length; hops += 1) {\n const successor = aiCatalogEntry(current)?.successor\n if (!successor) return current\n current = successor\n }\n return current\n}\n\nexport interface AiRoute {\n provider: AiProvider\n model: string\n}\n\n/** A settings value that names a real choice, or `undefined` for the platform default. */\nfunction chosen(settings: AiPluginSettings | undefined, key: string): string | undefined {\n const value = settings?.[key]\n return typeof value === 'string' && value && value !== AI_SETTING_PLATFORM\n ? value\n : undefined\n}\n\n/** The provider a workspace runs on: its own choice when registered, else the platform's. */\nexport function resolveAiProvider(settings?: AiPluginSettings): AiProvider | undefined {\n const wanted = chosen(settings, 'provider')\n if (wanted) {\n const named = aiProviderById(wanted)\n if (named) return named\n }\n return defaultAiProvider()\n}\n\n/**\n * The model a step kind runs on for a workspace, on the resolved provider.\n * A chosen model on another provider is not served — the provider decides\n * what it can run — and falls back to the provider's default for the tier.\n */\nexport function resolveAiRoute(\n kind: AiStepKind,\n settings?: AiPluginSettings,\n): AiRoute | undefined {\n const provider = resolveAiProvider(settings)\n if (!provider) return undefined\n // A setting or an override that names a retired model is served by its\n // successor, never by the retired model itself.\n const servedBy = (modelId: string | undefined): string | undefined => {\n const serving = modelId ? aiServingModelId(modelId) : undefined\n return serving && aiCatalogEntry(serving)?.provider === provider.id ? serving : undefined\n }\n const model =\n servedBy(chosen(settings, AI_STEP_MODEL_SETTING[kind])) ??\n (kind === 'assist.chat' ? servedBy(process.env[ASSIST_MODEL_ENV]?.trim()) : undefined) ??\n servedBy(process.env[AI_DEFAULT_MODEL_ENV]?.trim()) ??\n aiDefaultModelFor(provider.id, AI_STEP_TIERS[kind])\n return model ? { provider, model } : undefined\n}\n\n/**\n * What one step kind asks of its model, beside the tier it is served from\n * (`AI_STEP_TIERS` in the catalog): whether it thinks and how hard, its\n * answer ceiling, and the eval score that tier holds on the step's golden\n * briefs (AGL-2937).\n */\nexport interface AiRoutingRow {\n /**\n * `null` sends no setting: the model's own default, which on every current\n * model is adaptive thinking. A row whose ceiling holds the answer alone\n * says `off` (AGL-3781).\n */\n thinking: AiThinking | null\n effort: AiEffort | null\n /** The answer ceiling, thinking included. */\n maxTokens: number\n /** What the ceiling is sized from. */\n maxTokensBasis: string\n eval: AiRoutingEval\n /**\n * How long the step's prompt cache lives (AGL-3830): `1h` on every step a\n * site job runs, in production as in a live run, and the provider's five\n * minutes on a row that leaves it out. A guided start's routes each write\n * their prefix once and read it only inside the job, a beat or more apart,\n * so a five-minute entry is gone before the next job could read it; at an\n * hour the next site job reads what the last one wrote. The write costs 2x\n * the input price instead of 1.25x, so a step no job runs leaves it out.\n * The hour is the SHARED markers' alone (AGL-4050): a site's block and the\n * plan's own conversation are read by one job only, so they keep five\n * minutes on every row (`aiCacheMarkerTtls` in `providers/contract.ts`).\n * Read by `runValidatedGeneration` into the request's `cacheTtl`; a door\n * that sends its own request on the same step (the copy assistant's blog\n * draft) keeps five minutes.\n */\n cacheTtl?: '1h'\n}\n\n/**\n * The cache lifetime every step a site job runs asks for its shared prompt\n * (AGL-3830, AGL-4050): the plan, the look, the header and footer, every page\n * and its listing, the creations, the first posts and products, and the\n * welcome email.\n */\nconst AI_SITE_JOB_CACHE_TTL = '1h' as const\n\nexport interface AiRoutingEval {\n /** The golden-brief kinds the step answers. */\n kinds: readonly AiEvalKind[]\n /** `answers` scores the step's answers; `plans` scores the plans those kinds' answers carry. */\n scores: 'answers' | 'plans'\n /** `authored` while the score is the hand-written references'; `recorded` once a live run records answers. */\n source: 'authored' | 'recorded'\n passRate: number\n /** The mean answer score; `null` for a plan score, which passes or fails. */\n meanScore: number | null\n}\n\n/**\n * THE ROUTING TABLE (AGL-2937): what each step kind asks of the model, in one\n * place, with the evidence beside it.\n *\n * A step runs on the smallest tier that holds its eval floor. A row moves to a\n * cheaper tier, or a lower effort, only when a recorded eval\n * (`npm run eval:ai-live`) holds the floor there; while a row's score is the\n * authored references', it says so, and the row stays where production has\n * run it. `runtime/ai-eval.spec.ts` recomputes every row's score from the\n * harness, so a row cannot claim a score its briefs do not hold.\n *\n * A ceiling is sized from the p95 output the staff signals page reports for\n * the kind once production has served it; until then `maxTokensBasis` names\n * what it is sized from. The same spec holds every reference answer under its\n * row's ceiling at three characters a token, a reading that errs high for a\n * tokenizer that splits finer than four.\n */\nexport const AI_ROUTING_TABLE: Readonly<Record<AiStepKind, AiRoutingRow>> = {\n 'assist.chat': {\n // A scoped, latency-sensitive chat turn: no thinking, the low effort rung.\n thinking: 'off',\n effort: 'low',\n // 2,048 since AGL-4075: a non-Latin script spends two to four times the\n // tokens per character, and 5 of 17 non-English answers stopped at 1,024\n // against 1 of 33 English ones. Output is billed only as it is written.\n maxTokens: 2048,\n maxTokensBasis:\n 'a docs-grounded answer of a few short paragraphs in any language; the edit rung raises it for its tool call',\n eval: { kinds: ['chat'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 0.9688 },\n },\n 'copy.element': {\n // One element's copy on the fast tier, whose model thinks unless told not\n // to (AGL-3781): the ceiling holds the copy alone, as it always has, and\n // the effort is the model's own.\n thinking: 'off',\n effort: null,\n maxTokens: 1024,\n maxTokensBasis: 'one element of copy, about the length of the text it replaces',\n eval: { kinds: ['element'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 0.9688 },\n },\n 'copy.section': {\n thinking: null,\n effort: null,\n maxTokens: 3000,\n maxTokensBasis: 'a section of four to twelve nodes written as JSON',\n eval: { kinds: ['section'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 0.9688 },\n },\n 'copy.blog': {\n thinking: null,\n effort: null,\n maxTokens: 2048,\n maxTokensBasis: 'one blog post body in markdown-lite',\n eval: { kinds: ['blog'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 0.9688 },\n cacheTtl: AI_SITE_JOB_CACHE_TTL,\n },\n 'generate.section': {\n thinking: null,\n effort: null,\n maxTokens: 3000,\n maxTokensBasis: 'a section of four to twelve nodes written as JSON',\n eval: { kinds: ['section'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 0.9688 },\n },\n 'job.layout': {\n // The plan was reasoned out and confirmed before the step, so it writes without thinking.\n // A site's header and footer in the layout language think adaptively, at\n // low since AGL-3781 (`AI_LAYOUT_LANGUAGE_THINKING`): the live round of\n // 2026-10-14 built 13 of 13 frames at low, none cut off.\n thinking: 'off',\n effort: null,\n maxTokens: 8000,\n maxTokensBasis:\n \"tighter than the doctrine's default for a layout, so one step and its re-ask fit the job beat's budget; the tree is held to its rule 17 budget either way\",\n eval: { kinds: ['layout'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 0.9833 },\n cacheTtl: AI_SITE_JOB_CACHE_TTL,\n },\n 'job.template': {\n // Medium since the golden A/B of 2026-10-14 (AGL-3781): both template\n // briefs passed at medium as at the default, with about a third fewer\n // output tokens.\n thinking: 'off',\n effort: 'medium',\n maxTokens: 8000,\n maxTokensBasis:\n \"tighter than the doctrine's default for a template, so one step and its re-ask fit the job beat's budget; the tree is held to its rule 17 budget either way\",\n eval: { kinds: ['template'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 0.9833 },\n cacheTtl: AI_SITE_JOB_CACHE_TTL,\n },\n 'job.component': {\n // A component's tree and the properties it declares, in one answer, from a\n // plan reasoned out and confirmed before the step (AGL-2908).\n thinking: 'off',\n effort: null,\n maxTokens: 8000,\n maxTokensBasis:\n \"the ceiling the layout and template steps keep, so one step and its re-ask fit the job beat's budget; the tree is held to its rule 17 budget either way\",\n eval: { kinds: ['component'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 0.9833 },\n cacheTtl: AI_SITE_JOB_CACHE_TTL,\n },\n 'job.crm': {\n // CRM by AI (AGL-2917): short answers through a strict tool, written from\n // the facts the CRM reports about the record the member opened, with no\n // thinking. The ceiling fits an inline door, so a member is answered in\n // the request that asked, and has no room for thinking: the fast tier's\n // model thinks unless told not to, so the row says off, and its effort\n // is the low one a short answer read off given facts needs (AGL-3781).\n thinking: 'off',\n effort: 'low',\n maxTokens: 700,\n maxTokensBasis:\n \"the largest answer a CRM tool accepts: sixty columns matched to fields by number, or an email draft at its length limits, at three characters a token with room, as ai-job-crm-step.spec.ts measures it\",\n eval: { kinds: ['crm'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 0.9938 },\n },\n 'job.experiment': {\n // Variants of one piece of copy, or a result explained against a verdict\n // code reached before the request was made (AGL-2914). The judgment is\n // already made either way, so it writes without thinking.\n thinking: 'off',\n effort: null,\n maxTokens: 1500,\n maxTokensBasis:\n \"four variants at the tool's own lengths — a name, a headline, a body and a rationale each — written as JSON, about 3,000 characters at three characters a token with room; an explanation is a headline, four sentences and a next step, well under it, as ai-job-experiment-step.spec.ts measures both\",\n eval: { kinds: ['experiment'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 1 },\n },\n 'job.form': {\n // A form's two halves in one answer: the field declaration and the design\n // that renders it, as JSON, with no extended thinking (AGL-2913). The step\n // asks this ceiling, which is the doctrine's own for a form, and registers\n // the time it takes (AGL-3035).\n thinking: 'off',\n effort: null,\n maxTokens: 6000,\n maxTokensBasis:\n \"the largest form the doctrine's output budget admits, a form's fields, its consent and routing declaration and the design that renders them written as JSON at the wordiest golden's characters a stored byte, under it with room; ai-job-form-step.spec.ts measures it\",\n eval: { kinds: ['form'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 0.9833 },\n cacheTtl: AI_SITE_JOB_CACHE_TTL,\n },\n 'job.insight': {\n // Readers chosen, then their tables phrased (AGL-2915): short sentences\n // over figures code computed, each held to the rows it cites, so no\n // extended thinking. The read call before the answer asks for at most\n // `AI_INSIGHT_READ_MAX_TOKENS` of the same allowance.\n thinking: 'off',\n effort: null,\n maxTokens: 1500,\n maxTokensBasis:\n 'five insights at their 280-character bound, each citing three tables of ten rows, and the gap sentence, written as JSON: about 2,700 characters, under 1,000 tokens at three characters a token, with room',\n eval: { kinds: ['insight'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 0.9688 },\n },\n 'job.page': {\n // One section of a page, written against a plan a member already\n // confirmed, so it writes without thinking (AGL-2907). The ceiling here\n // is the MOST a pass may ask for; each pass sizes its own down from it to\n // what fits the time the job beat can give the step, which on the\n // balanced tier is about half of this. A page in the layout language\n // thinks adaptively, at low since AGL-3781 (`AI_LAYOUT_LANGUAGE_THINKING`):\n // the live round of 2026-10-14 built 39 of 39 pages at low, 64 of 65\n // answers held their rules on the first ask, none cut off.\n thinking: 'off',\n effort: null,\n maxTokens: 2000,\n maxTokensBasis:\n \"a section of a page written as JSON, about forty elements at the step's measured 45 tokens an element; a pass lowers it to the worst case that fits AI_JOB_PAGE_STEP_MINIMUM_MS on the model it runs\",\n eval: { kinds: ['page'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 0.9833 },\n cacheTtl: AI_SITE_JOB_CACHE_TTL,\n },\n 'job.products': {\n // A product's copy from its name, text and photo, or a store's first\n // products, categories and discounts from a brief (AGL-2916): answers held\n // to the storefront claim rules, written with no extended thinking.\n thinking: 'off',\n effort: null,\n maxTokens: 8000,\n maxTokensBasis:\n \"the largest catalog the tool accepts, twelve proposed products at every bound written as JSON, which the balanced tier asks as much of as fits a beat; one product's copy asks 1,500 and categories with discounts 2,000, as ai-job-products-step.spec.ts measures them\",\n eval: {\n kinds: ['product', 'catalog', 'categories'],\n scores: 'answers',\n source: 'authored',\n passRate: 1,\n meanScore: 0.9922,\n },\n cacheTtl: AI_SITE_JOB_CACHE_TTL,\n },\n 'job.email': {\n // One email design from the email palette, written against the brief with\n // no extended thinking (AGL-2912). A campaign job runs the same\n // generation, so it carries the same ceiling.\n thinking: 'off',\n effort: null,\n maxTokens: 6000,\n maxTokensBasis:\n \"an email's node map written as JSON, with three subject lines and three preheaders beside it\",\n eval: { kinds: ['email'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 0.9688 },\n cacheTtl: AI_SITE_JOB_CACHE_TTL,\n },\n 'job.campaign': {\n // The campaign job generates its email through the email step, so it is\n // routed identically; the draft campaign it also writes asks nothing of\n // the model (AGL-2912).\n thinking: 'off',\n effort: null,\n maxTokens: 6000,\n maxTokensBasis:\n \"an email's node map written as JSON, with three subject lines and three preheaders beside it\",\n eval: { kinds: ['email'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 0.9688 },\n },\n 'job.workflow': {\n // An automation drafted from a description, or one explained (AGL-2919):\n // which trigger and which steps a description means is the judgment the\n // step sells, so it thinks before it answers.\n thinking: 'adaptive',\n effort: null,\n maxTokens: 4000,\n maxTokensBasis:\n 'the largest automation the tool accepts, 6,000 characters written out as JSON, at three characters a token with as much again to think in, as ai-job-workflow-step.spec.ts measures it',\n eval: { kinds: ['workflow'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 0.9688 },\n },\n 'job.logic': {\n // A site function or variable proposed, or a function explained\n // (AGL-3603): which conditions and arithmetic a description means is the\n // judgment the step sells, so it thinks before it answers.\n thinking: 'adaptive',\n effort: null,\n maxTokens: 3000,\n maxTokensBasis:\n 'the largest function the tool accepts — twelve parameters, twenty-four operations — is far past any description; a function of eight operations is about 2,400 characters of JSON, at three characters a token with as much again to think in, as ai-job-logic-step.spec.ts measures it',\n eval: { kinds: ['logic'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 1 },\n },\n 'job.edit': {\n // A change to a page or a layout the site has (AGL-3616): which elements\n // a request means, on an outline with no selection, is the judgment the\n // step sells, so it thinks before it answers.\n thinking: 'adaptive',\n effort: null,\n maxTokens: 6144,\n maxTokensBasis:\n \"the Assist edit rung's own ceiling for the proposal's tool call (ASSIST_EDIT_MAX_OUTPUT_TOKENS, 4,096: forty new elements written as name/value pairs), with half as much again to think in, as ai-job-edit-step.spec.ts holds it; the whole of it twice, as a re-ask may, still fits one beat on the balanced tier\",\n eval: { kinds: ['edit'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 1 },\n },\n 'job.seo': {\n // A listing written from the page's own text to fixed lengths, sized for\n // the answer alone: no thinking, which the fast tier's model does unless\n // told not to, at the low effort a short bounded answer needs (AGL-3781).\n // The audit's site proposal and fix batches run on the same row.\n thinking: 'off',\n effort: 'low',\n maxTokens: 1024,\n maxTokensBasis:\n \"the largest listing the tool accepts, at three characters a token with a quarter again, as ai-job-seo-step.spec.ts measures it; an audit's site proposal and fix batches keep ceilings of their own, measured the same way\",\n eval: { kinds: ['seo'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 1 },\n cacheTtl: AI_SITE_JOB_CACHE_TTL,\n },\n 'job.text': {\n // Low since the golden A/B of 2026-10-14 (AGL-3781): the tagline brief\n // passed at low, its copy scored higher than at the default.\n thinking: 'adaptive',\n effort: 'low',\n maxTokens: 1024,\n maxTokensBasis: 'a few paragraphs of copy; a brief asking for more gets a draft to extend',\n eval: { kinds: ['text'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 1 },\n },\n 'job.theme': {\n // Medium since the golden A/B of 2026-10-14 (AGL-3781): the theme brief\n // passed at medium with the default's scores and half its output tokens.\n // A site start's look runs its own setting (`ai-job-site-look.ts`).\n thinking: 'adaptive',\n effort: 'medium',\n maxTokens: 8000,\n maxTokensBasis:\n 'the largest answer the theme tool accepts, about 2,500 tokens of JSON as ai-job-theme-step.spec.ts measures it, with as much again to think in',\n eval: { kinds: ['theme'], scores: 'answers', source: 'authored', passRate: 1, meanScore: 0.9688 },\n cacheTtl: AI_SITE_JOB_CACHE_TTL,\n },\n 'job.plan': {\n // A site's plan is an outline written without thinking (AGL-3594), and a\n // paid site's asks medium (`AI_PAID_SITE_PLAN_EFFORT`, AGL-3781): the live\n // round of 2026-10-14 planned 11 of 11 paid site briefs at medium in 16\n // attempts to the default's 22, $0.129 to $0.164, with no refusal and no\n // rule broken. Every other plan, and a Free site's on the fast tier, asks\n // this row's effort — the model's default — until a round holds it lower.\n thinking: 'adaptive',\n effort: null,\n maxTokens: 8000,\n maxTokensBasis: 'a plan of up to twelve screens with sixteen sections each, with room to think',\n eval: {\n kinds: ['page', 'template', 'component', 'layout', 'form'],\n scores: 'plans',\n source: 'authored',\n passRate: 1,\n meanScore: null,\n },\n cacheTtl: AI_SITE_JOB_CACHE_TTL,\n },\n}\n\n/**\n * What a step kind's row asks of its model, as the fields a request carries:\n * only those the row names, so a row that names none sends none and the\n * model's own default stands.\n */\nexport function aiRouteSettings(kind: AiStepKind): { thinking?: AiThinking; effort?: AiEffort } {\n const row = AI_ROUTING_TABLE[kind]\n return {\n ...(row.thinking ? { thinking: row.thinking } : {}),\n ...(row.effort ? { effort: row.effort } : {}),\n }\n}\n\n/** The model id alone — what a door records on the meter and the job. */\nexport function aiModelForStep(kind: AiStepKind, settings?: AiPluginSettings): string {\n const route = resolveAiRoute(kind, settings)\n if (!route) {\n // Every door gates on the provider being configured before it asks\n // for a model, so this is a wiring fault rather than a customer path.\n throw new Error('no AI provider is registered')\n }\n return route.model\n}\n"],"names":["AI_SETTING_PLATFORM","AI_STEP_MODEL_SETTING","AI_MODEL_CATALOG","AI_STEP_TIERS","aiCatalogEntry","aiDefaultModelFor","aiProviderById","defaultAiProvider","AI_DEFAULT_MODEL_ENV","ASSIST_MODEL_ENV","aiServingModelId","modelId","current","hops","length","successor","chosen","settings","key","value","undefined","resolveAiProvider","wanted","named","resolveAiRoute","kind","servedBy","process","provider","serving","id","model","env","trim","AI_SITE_JOB_CACHE_TTL","AI_ROUTING_TABLE","thinking","effort","maxTokens","maxTokensBasis","eval","kinds","scores","source","passRate","meanScore","cacheTtl","aiRouteSettings","row","aiModelForStep","route","Error"],"mappings":"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"}
|
|
@@ -18,7 +18,7 @@ import type { AiStepKind } from '../providers/catalog';
|
|
|
18
18
|
import type { AiProvider } from '../providers/contract';
|
|
19
19
|
import { type AiPluginSettings } from '../providers/routing';
|
|
20
20
|
import { type AiDataset } from '../tools/ai-dataset-tool';
|
|
21
|
-
import { type AiValidatedGeneration } from './ai-doctrine';
|
|
21
|
+
import { type AiGenerationCheckResult, type AiValidatedGeneration } from './ai-doctrine';
|
|
22
22
|
import type { AiSystemBlock } from './ai-runtime';
|
|
23
23
|
/**
|
|
24
24
|
* A SITE'S DATASETS (AGL-3616): the typed fields and first records of one
|
|
@@ -43,6 +43,16 @@ export declare const AI_DATASET_STEP: AiStepKind;
|
|
|
43
43
|
export declare const AI_DATASET_MAX_TOKENS: number;
|
|
44
44
|
/** The generation kind a dataset is asked under, which the doctrine scopes. */
|
|
45
45
|
export declare const AI_DATASET_KIND = "dataset";
|
|
46
|
+
/**
|
|
47
|
+
* THE Q2 DECISION (AGL-4065): what a list the brief gives no entries for starts
|
|
48
|
+
* with. `samples` (decided 2026-10-28): complete-looking SAMPLE records the
|
|
49
|
+
* owner replaces, each marked a sample in Data, a dated list's dates set by
|
|
50
|
+
* the platform (`aiDatasetSampleDates`), so the site looks complete and no
|
|
51
|
+
* row is a placeholder. `empty`: the fields and no records, and the pages
|
|
52
|
+
* say the entries are on the way. The rules, the tool's answer and the
|
|
53
|
+
* pages' brief all follow this one switch.
|
|
54
|
+
*/
|
|
55
|
+
export declare const AI_DATASET_LIST_WITHOUT_ENTRIES: 'samples' | 'empty';
|
|
46
56
|
/** The rules for one dataset. Byte-identical on every request. */
|
|
47
57
|
export declare const AI_DATASET_INSTRUCTIONS: AiSystemBlock[];
|
|
48
58
|
export interface AiDatasetPromptInput {
|
|
@@ -57,9 +67,97 @@ export interface AiDatasetPromptInput {
|
|
|
57
67
|
shownIn: readonly string[];
|
|
58
68
|
/** Whether each record also gets a page of its own. */
|
|
59
69
|
recordPages: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* The sections of one page that each list part of it (AGL-4065), and the
|
|
72
|
+
* value of its planned field each lists: "Upcoming races" the records whose
|
|
73
|
+
* Status is "Upcoming", "Past results" those whose Status is "Past".
|
|
74
|
+
*/
|
|
75
|
+
split?: AiDatasetSplit;
|
|
76
|
+
}
|
|
77
|
+
/** A page's lists of one dataset, each the records of one value of one field (AGL-4065). */
|
|
78
|
+
export interface AiDatasetSplit {
|
|
79
|
+
/** The planned field, by name: "Status". */
|
|
80
|
+
field: string;
|
|
81
|
+
/** The sections, in page order. */
|
|
82
|
+
sections: string[];
|
|
83
|
+
/** The value each section lists, by position. */
|
|
84
|
+
values: string[];
|
|
60
85
|
}
|
|
86
|
+
/** What a dataset is told of the sections that each list part of it (AGL-4065). */
|
|
87
|
+
export declare function aiDatasetSplitLine(split: AiDatasetSplit): string;
|
|
61
88
|
/** The user turn: the site's words, the dataset, and where it is shown. */
|
|
62
89
|
export declare function aiDatasetPrompt(input: AiDatasetPromptInput): string;
|
|
90
|
+
/**
|
|
91
|
+
* The re-ask a dataset whose records are placeholders is answered with
|
|
92
|
+
* (AGL-4065): the 2026-10-25 Rut Runners Racing start's "Race events" came
|
|
93
|
+
* back as "Upcoming race 1 (details to be added)" to "Past race 5 (details to
|
|
94
|
+
* be added)", because the rules forbid inventing a date and the first field
|
|
95
|
+
* may not be empty.
|
|
96
|
+
*/
|
|
97
|
+
export declare const AI_DATASET_PLACEHOLDER_REASK: string;
|
|
98
|
+
/** The code a dataset's placeholder records are refused under. */
|
|
99
|
+
export declare const AI_DATASET_PLACEHOLDER_CODE = "placeholder-records";
|
|
100
|
+
/**
|
|
101
|
+
* The records of a designed dataset that are placeholders (AGL-4065), by
|
|
102
|
+
* position, which no site shows:
|
|
103
|
+
* - any value that marks a gap: "details to be added", "TBD", "coming soon";
|
|
104
|
+
* - a name that is a number after nothing but the list's own words and
|
|
105
|
+
* stand-in words: "Item 1", "Race 2", "Upcoming race 3", "New entry 4".
|
|
106
|
+
* A course's "Level 1" to "Level 3" and a series' "Series Round 1 at
|
|
107
|
+
* Lockhart" name things, and are kept. Pure.
|
|
108
|
+
*/
|
|
109
|
+
export declare function aiDatasetPlaceholderRecords(dataset: {
|
|
110
|
+
fields: ReadonlyArray<{
|
|
111
|
+
name: string;
|
|
112
|
+
}>;
|
|
113
|
+
records: ReadonlyArray<ReadonlyArray<string | undefined>>;
|
|
114
|
+
}, name?: string): number[];
|
|
115
|
+
/** Where a dataset is checked: its plan's fields, the words a claim may come from, and its name. */
|
|
116
|
+
export interface AiDatasetCheckContext {
|
|
117
|
+
planned: readonly string[];
|
|
118
|
+
merchantWords: string;
|
|
119
|
+
name: string;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* A dataset's answer held to the tool's check and to naming real things
|
|
123
|
+
* (AGL-4065): one with placeholder records is refused, with
|
|
124
|
+
* {@link AI_DATASET_PLACEHOLDER_REASK} for the one re-ask. Pure.
|
|
125
|
+
*/
|
|
126
|
+
export declare function checkAiDatasetRecords(answer: Record<string, unknown>, context: AiDatasetCheckContext): AiGenerationCheckResult<AiDataset>;
|
|
127
|
+
/**
|
|
128
|
+
* A dataset answered with placeholders after its one re-ask, kept with the
|
|
129
|
+
* records that name real things (AGL-4065) — however few, none included —
|
|
130
|
+
* where nothing else in it breaks a rule; `null` otherwise. Pure.
|
|
131
|
+
*/
|
|
132
|
+
export declare function aiDatasetWithoutPlaceholders(answer: Record<string, unknown> | null | undefined, context: AiDatasetCheckContext): AiDataset | null;
|
|
133
|
+
/**
|
|
134
|
+
* A designed dataset with any placeholder record taken out (AGL-4065), and
|
|
135
|
+
* where the list starts with no samples (`AI_DATASET_LIST_WITHOUT_ENTRIES`
|
|
136
|
+
* is `empty`), every sample too. Pure.
|
|
137
|
+
*/
|
|
138
|
+
export declare function aiDatasetRealRecords(dataset: AiDataset, name: string): AiDataset;
|
|
139
|
+
/** The days a sample's date steps by, and how far from the job's day the first falls: a season of weekends. */
|
|
140
|
+
export declare const AI_DATASET_SAMPLE_DATES: {
|
|
141
|
+
readonly stepDays: 21;
|
|
142
|
+
readonly leadDays: 14;
|
|
143
|
+
};
|
|
144
|
+
/**
|
|
145
|
+
* A dated list's SAMPLE records given real dates (AGL-4065): a sample the
|
|
146
|
+
* brief gave no date for falls on a weekend of a season from the job's day —
|
|
147
|
+
* the first Saturday at least two weeks out, then every three weeks — and a
|
|
148
|
+
* sample that is behind us (its split value or the list says past: "Past
|
|
149
|
+
* results") on the Saturdays as far back, the latest first, with no result
|
|
150
|
+
* left on one still to come. Written as a visitor reads a date, in the
|
|
151
|
+
* site's language. A record from the brief keeps its own date. Pure.
|
|
152
|
+
*/
|
|
153
|
+
export declare function aiDatasetSampleDates(dataset: AiDataset, options: {
|
|
154
|
+
now: Date;
|
|
155
|
+
language?: string | null;
|
|
156
|
+
split?: AiDatasetSplit | null;
|
|
157
|
+
pastOnly?: boolean;
|
|
158
|
+
}): AiDataset;
|
|
159
|
+
/** Whether every section that lists a dataset says its records are behind us ("Past results", "Race results"). */
|
|
160
|
+
export declare function aiDatasetListedAsPast(sections: readonly string[]): boolean;
|
|
63
161
|
export interface GenerateAiDatasetInput extends AiDatasetPromptInput {
|
|
64
162
|
/** What a claim or a price may be quoted from: the brief and the site's answers. */
|
|
65
163
|
merchantWords: string;
|