@aglyn/plugins-ai 1.0.0-beta.256 → 1.0.0-beta.258
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +17 -17
- package/src/lib/components/assist-panel.component.js +21 -8
- package/src/lib/components/assist-panel.component.js.map +1 -1
- package/src/lib/insights/ai-figure-readers.js +15 -8
- package/src/lib/insights/ai-figure-readers.js.map +1 -1
- package/src/lib/jobs/ai-job-build-step.d.ts +9 -0
- package/src/lib/jobs/ai-job-build-step.js +19 -3
- package/src/lib/jobs/ai-job-build-step.js.map +1 -1
- package/src/lib/jobs/ai-job-form-step.js +3 -3
- package/src/lib/jobs/ai-job-form-step.js.map +1 -1
- package/src/lib/jobs/ai-job-insight-step.js +1 -1
- package/src/lib/jobs/ai-job-insight-step.js.map +1 -1
- package/src/lib/jobs/ai-job-page-language.d.ts +19 -0
- package/src/lib/jobs/ai-job-page-language.js +24 -4
- package/src/lib/jobs/ai-job-page-language.js.map +1 -1
- package/src/lib/jobs/ai-job-page-sections.js +12 -1
- package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
- package/src/lib/jobs/ai-job-page-step.d.ts +14 -0
- package/src/lib/jobs/ai-job-page-step.js +69 -20
- package/src/lib/jobs/ai-job-page-step.js.map +1 -1
- package/src/lib/jobs/ai-job-seo-step.js +3 -2
- package/src/lib/jobs/ai-job-seo-step.js.map +1 -1
- package/src/lib/jobs/ai-layout-stock-photos.d.ts +10 -5
- package/src/lib/jobs/ai-layout-stock-photos.js +89 -7
- package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
- package/src/lib/jobs/fixtures/ai-stock-photo-copper-kettle-recordings.d.ts +21 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-copper-kettle-recordings.js +126 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-copper-kettle-recordings.js.map +1 -0
- package/src/lib/layout-language/ai-layout-compiler.js +52 -52
- package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
- package/src/lib/layout-language/ai-layout-copy.d.ts +9 -5
- package/src/lib/layout-language/ai-layout-copy.js +22 -14
- package/src/lib/layout-language/ai-layout-copy.js.map +1 -1
- package/src/lib/layout-language/ai-layout-frame.js +4 -4
- package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
- package/src/lib/layout-language/ai-layout-pictures.d.ts +44 -15
- package/src/lib/layout-language/ai-layout-pictures.js +29 -8
- package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
- package/src/lib/model/ai-build-job.js +14 -0
- package/src/lib/model/ai-build-job.js.map +1 -1
- package/src/lib/model/ai-host-publish-context.d.ts +6 -0
- package/src/lib/model/ai-host-publish-context.js +15 -2
- package/src/lib/model/ai-host-publish-context.js.map +1 -1
- package/src/lib/model/assist-follow-up.d.ts +35 -0
- package/src/lib/model/assist-follow-up.js +44 -4
- package/src/lib/model/assist-follow-up.js.map +1 -1
- package/src/lib/providers/anthropic.js +17 -4
- package/src/lib/providers/anthropic.js.map +1 -1
- package/src/lib/providers/cache-write-hour.d.ts +37 -0
- package/src/lib/providers/cache-write-hour.js +46 -0
- package/src/lib/providers/cache-write-hour.js.map +1 -0
- package/src/lib/providers/contract.d.ts +16 -3
- package/src/lib/providers/contract.js.map +1 -1
- package/src/lib/providers/routing.d.ts +13 -0
- package/src/lib/providers/routing.js +27 -11
- package/src/lib/providers/routing.js.map +1 -1
- package/src/lib/runtime/ai-dev-env.d.ts +13 -3
- package/src/lib/runtime/ai-dev-env.js +17 -3
- package/src/lib/runtime/ai-dev-env.js.map +1 -1
- 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 +41 -1
- package/src/lib/runtime/ai-doctrine-validators.js +263 -2
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-doctrine.js +7 -1
- package/src/lib/runtime/ai-doctrine.js.map +1 -1
- package/src/lib/runtime/ai-eval-live.js +1 -1
- package/src/lib/runtime/ai-eval-live.js.map +1 -1
- package/src/lib/runtime/ai-eval.d.ts +1 -1
- package/src/lib/runtime/ai-eval.js +18 -1
- package/src/lib/runtime/ai-eval.js.map +1 -1
- package/src/lib/runtime/ai-palette.generated.js +162 -2
- package/src/lib/runtime/ai-palette.generated.js.map +1 -1
- package/src/lib/runtime/ai-placeholder-tokens.d.ts +70 -0
- package/src/lib/runtime/ai-placeholder-tokens.js +169 -0
- package/src/lib/runtime/ai-placeholder-tokens.js.map +1 -0
- package/src/lib/runtime/ai-repeated-items.js +5 -2
- package/src/lib/runtime/ai-repeated-items.js.map +1 -1
- package/src/lib/runtime/ai-runtime.js +2 -3
- package/src/lib/runtime/ai-runtime.js.map +1 -1
- package/src/lib/server/assist-chat.js +8 -3
- package/src/lib/server/assist-chat.js.map +1 -1
- package/src/lib/server/assist-view-context.d.ts +5 -1
- package/src/lib/server/assist-view-context.js +8 -3
- package/src/lib/server/assist-view-context.js.map +1 -1
- package/src/lib/usage/assist-usage.d.ts +2 -0
- package/src/lib/usage/assist-usage.js +8 -3
- package/src/lib/usage/assist-usage.js.map +1 -1
|
@@ -54,9 +54,9 @@
|
|
|
54
54
|
* live eval every ten to forty minutes while it iterates, which is past the
|
|
55
55
|
* default five-minute entry and inside an hour, so under the launcher the
|
|
56
56
|
* breakpoints ask for the hour: the second run reads the prefix the first one
|
|
57
|
-
* wrote instead of writing it again.
|
|
58
|
-
*
|
|
59
|
-
*
|
|
57
|
+
* wrote instead of writing it again. A deployed server never reads this:
|
|
58
|
+
* `undefined` there, where the lifetime is the route's own
|
|
59
|
+
* (`aiRequestCacheTtl`).
|
|
60
60
|
*/ export function aiDevCacheTtl(env = process.env) {
|
|
61
61
|
var _env_AI_DEV_CACHE_TTL_ENV;
|
|
62
62
|
if (aiIsDeployedRuntime(env)) return undefined;
|
|
@@ -65,6 +65,20 @@
|
|
|
65
65
|
if (raw === '5m') return undefined;
|
|
66
66
|
return aiUnderLiveRun(env) ? '1h' : undefined;
|
|
67
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* The lifetime a request goes out with: the one its door asked for, which a
|
|
70
|
+
* site job's routes set to the hour through their routing rows (AGL-3830),
|
|
71
|
+
* else this process's own. A deployed server sends what the door asked for
|
|
72
|
+
* and nothing else. In development `AGLYN_AI_CACHE_TTL=5m` turns every
|
|
73
|
+
* request back to five minutes, the route's hour included, for a one-off
|
|
74
|
+
* sweep that will not run again within the hour (docs/AI_CACHE_AUDIT_2026-10.md,
|
|
75
|
+
* lever 5).
|
|
76
|
+
*/ export function aiRequestCacheTtl(requested, env = process.env) {
|
|
77
|
+
var _env_AI_DEV_CACHE_TTL_ENV;
|
|
78
|
+
if (aiIsDeployedRuntime(env)) return requested;
|
|
79
|
+
if (String((_env_AI_DEV_CACHE_TTL_ENV = env[AI_DEV_CACHE_TTL_ENV]) != null ? _env_AI_DEV_CACHE_TTL_ENV : '').trim().toLowerCase() === '5m') return undefined;
|
|
80
|
+
return requested != null ? requested : aiDevCacheTtl(env);
|
|
81
|
+
}
|
|
68
82
|
/** A deployed server that was handed the variable says so once, and runs without it. */ let warnedDeployed = false;
|
|
69
83
|
export function aiWarnIfReplayAskedInDeployment(env = process.env) {
|
|
70
84
|
if (warnedDeployed || !aiIsDeployedRuntime(env)) return;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/runtime/ai-dev-env.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Which development layers this process runs (AGL-3660): the replay cache,\n * the hour-long prompt cache and batch mode, each read from the environment.\n * Kept apart from `ai-dev-replay.ts`, which reads and writes files, so the\n * runtime asks these questions on every request without loading a file\n * system module into a server that will never use one. Every answer is `off`\n * in a deployed server.\n */\n\nimport type { AiEffort } from '../providers/contract'\n\nexport type AiDevReplayMode = 'off' | 'replay' | 'refresh'\n\n/** The variable that turns the replay cache on, off, or to refresh. */\nexport const AI_REPLAY_ENV = 'AGLYN_AI_REPLAY'\n/** Where the answers are kept, when not under the repository's `.cache/ai-replay`. */\nexport const AI_REPLAY_DIR_ENV = 'AGLYN_AI_REPLAY_DIR'\n/** The prompt-cache lifetime a development run asks for: `1h` or `5m`. */\nexport const AI_DEV_CACHE_TTL_ENV = 'AGLYN_AI_CACHE_TTL'\n/** The mark the live-eval launcher leaves, which `jest.setup.js` also reads. */\nexport const AI_LIVE_LAUNCHER_ENV = 'AI_EVAL_LIVE_LAUNCHER'\n\nexport type AiDevEnv = Readonly<Record<string, string | undefined>>\n\n/** Whether this process is a deployed server, where no development layer may run. */\nexport function aiIsDeployedRuntime(env: AiDevEnv = process.env): boolean {\n return (\n env['NODE_ENV'] === 'production' ||\n env['VERCEL_ENV'] === 'production' ||\n env['VERCEL_ENV'] === 'preview' ||\n Boolean(env['K_SERVICE'])\n )\n}\n\n/** The variable every live spec is started with. */\nexport const AI_LIVE_RUN_ENV = 'AGLYN_LIVE_AI'\n\n/**\n * Whether this process is a development run that asked for the live\n * provider: through the launcher, or a live spec started with\n * `AGLYN_LIVE_AI=1` (AGL-3781). Every development layer is on by default in\n * such a run — a live spec started without the launcher's mark used to send\n * every request again, at the five-minute cache and full price — and each can\n * still be turned off by its own variable.\n */\nexport function aiUnderLiveRun(env: AiDevEnv = process.env): boolean {\n if (aiIsDeployedRuntime(env)) return false\n return Boolean(env[AI_LIVE_LAUNCHER_ENV]) || String(env[AI_LIVE_RUN_ENV] ?? '').trim() === '1'\n}\n\n/** The replay mode this process runs under; `off` in any deployed server. */\nexport function aiDevReplayMode(env: AiDevEnv = process.env): AiDevReplayMode {\n if (aiIsDeployedRuntime(env)) return 'off'\n const raw = String(env[AI_REPLAY_ENV] ?? '').trim().toLowerCase()\n if (raw === 'refresh') return 'refresh'\n if (raw === '1' || raw === 'on' || raw === 'true' || raw === 'replay') return 'replay'\n if (raw === '0' || raw === 'off' || raw === 'false') return 'off'\n return aiUnderLiveRun(env) ? 'replay' : 'off'\n}\n\n/**\n * The prompt-cache lifetime a development run asks for. An agent re-runs a\n * live eval every ten to forty minutes while it iterates, which is past the\n * default five-minute entry and inside an hour, so under the launcher the\n * breakpoints ask for the hour: the second run reads the prefix the first one\n * wrote instead of writing it again. Production keeps the default — its\n * steps run a minute apart on the beat, inside five minutes — and never reads\n * this: `undefined` there.\n */\nexport function aiDevCacheTtl(env: AiDevEnv = process.env): '1h' | undefined {\n if (aiIsDeployedRuntime(env)) return undefined\n const raw = String(env[AI_DEV_CACHE_TTL_ENV] ?? '').trim().toLowerCase()\n if (raw === '1h') return '1h'\n if (raw === '5m') return undefined\n return aiUnderLiveRun(env) ? '1h' : undefined\n}\n\n/** A deployed server that was handed the variable says so once, and runs without it. */\nlet warnedDeployed = false\nexport function aiWarnIfReplayAskedInDeployment(env: AiDevEnv = process.env): void {\n if (warnedDeployed || !aiIsDeployedRuntime(env)) return\n if (!env[AI_REPLAY_ENV] && !env[AI_DEV_CACHE_TTL_ENV]) return\n warnedDeployed = true\n console.error(\n `[ai] ${AI_REPLAY_ENV} / ${AI_DEV_CACHE_TTL_ENV} are development-only and are ignored in a deployed server`,\n )\n}\n\n/** The variable that turns batch mode on (`runtime/ai-live-batch.ts`). */\nexport const AI_LIVE_BATCH_ENV = 'AGLYN_LIVE_AI_BATCH'\n\n/**\n * Whether this process batches its live requests: what `AGLYN_LIVE_AI_BATCH`\n * says, and in a live run that says nothing, yes (AGL-3781). Nobody waits on\n * an eval's answers, and a batch is half price on every token, cache reads\n * and writes included; `AGLYN_LIVE_AI_BATCH=0` is for the quick single run an\n * iteration wants back in seconds.\n */\nexport function aiLiveBatchWanted(env: AiDevEnv = process.env): boolean {\n if (aiIsDeployedRuntime(env)) return false\n const raw = String(env[AI_LIVE_BATCH_ENV] ?? '').trim()\n if (/^(?:1|true|yes|on)$/i.test(raw)) return true\n if (/^(?:0|false|no|off)$/i.test(raw)) return false\n return aiUnderLiveRun(env)\n}\n\n/** The variable that bounds how long a live run waits on one batch (AGL-3781). */\nexport const AI_LIVE_BATCH_WAIT_ENV = 'AGLYN_LIVE_AI_BATCH_WAIT_MS'\n\n/** How long a batch is waited on when the run names no wait. */\nexport const AI_LIVE_BATCH_DEFAULT_WAIT_MS = 30 * 60_000\n\n/**\n * How long a live run waits on one batch (AGL-3781): what\n * `AGLYN_LIVE_AI_BATCH_WAIT_MS` says, or half an hour. Most batches end in\n * minutes. One that has not by then is canceled, which keeps every answer it\n * already has, and the requests it left unanswered are asked on their own at\n * full price — so a slow batch costs a round part of its discount, never the\n * round.\n */\nexport function aiLiveBatchWaitMs(env: AiDevEnv = process.env): number {\n const raw = String(env[AI_LIVE_BATCH_WAIT_ENV] ?? '').trim()\n const parsed = raw ? Number(raw) : Number.NaN\n return Number.isFinite(parsed) && parsed >= 0 ? parsed : AI_LIVE_BATCH_DEFAULT_WAIT_MS\n}\n\n/**\n * The most a batched live run may take: a run's requests go as a few batches\n * one after another — the answers, their re-asks, the grades — and each is\n * waited on for `aiLiveBatchWaitMs`, then given time to end its cancel and to\n * have what it left asked on its own. A spec that timed out would keep none\n * of the answers it already paid for, so the bound is the worst case.\n */\nexport const AI_LIVE_BATCH_TIMEOUT_MS = 5 * 60 * 60_000\n\n/** A live spec's timeout: `baseMs`, or the batched run's when this run batches. */\nexport function aiLiveTimeoutMs(baseMs: number, env: AiDevEnv = process.env): number {\n if (!aiLiveBatchWanted(env)) return baseMs\n // Four batches in a row, each waited on, canceled and finished by hand.\n const longWait = 4 * (aiLiveBatchWaitMs(env) + 20 * 60_000)\n return Math.max(baseMs, AI_LIVE_BATCH_TIMEOUT_MS, longWait)\n}\n\n/**\n * The variable an effort A/B names its arm with (AGL-3781): a step kind's\n * effort for this run, as `job.form:medium,job.text:low`, or `*:medium` for\n * every kind. A development run only, so the defaults stay where production\n * runs them until a recorded round shows a lower effort holds.\n */\nexport const AI_EFFORT_ARM_ENV = 'AGLYN_AI_EFFORT'\n\nconst AI_EFFORT_LEVELS: readonly AiEffort[] = ['low', 'medium', 'high']\n\n/**\n * Every step kind this development run's A/B arm names, with its effort, in\n * kind order — `*` for every kind it does not name. A pair whose effort is no\n * level is ignored; a kind named twice keeps its first effort, and `*` its\n * last. Empty when the run names no arm, and in any deployed server.\n */\nexport function aiDevEffortArm(env: AiDevEnv = process.env): Record<string, AiEffort> {\n if (aiIsDeployedRuntime(env)) return {}\n const raw = String(env[AI_EFFORT_ARM_ENV] ?? '').trim()\n const arm: Record<string, AiEffort> = {}\n for (const pair of raw ? raw.split(',') : []) {\n const [kind, level] = pair.split(':').map((part) => part.trim())\n if (!kind || !AI_EFFORT_LEVELS.includes(level as AiEffort)) continue\n if (kind === '*' || !(kind in arm)) arm[kind] = level as AiEffort\n }\n return Object.fromEntries(Object.entries(arm).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)))\n}\n\n/** The effort this development run's A/B arm asks `step` at, or `undefined` for the step's own. */\nexport function aiDevEffortFor(step: string | undefined, env: AiDevEnv = process.env): AiEffort | undefined {\n const arm = aiDevEffortArm(env)\n return (step !== undefined && step !== '*' ? arm[step] : undefined) ?? arm['*']\n}\n"],"names":["AI_REPLAY_ENV","AI_REPLAY_DIR_ENV","AI_DEV_CACHE_TTL_ENV","AI_LIVE_LAUNCHER_ENV","aiIsDeployedRuntime","env","process","Boolean","AI_LIVE_RUN_ENV","aiUnderLiveRun","String","trim","aiDevReplayMode","raw","toLowerCase","aiDevCacheTtl","undefined","warnedDeployed","aiWarnIfReplayAskedInDeployment","console","error","AI_LIVE_BATCH_ENV","aiLiveBatchWanted","test","AI_LIVE_BATCH_WAIT_ENV","AI_LIVE_BATCH_DEFAULT_WAIT_MS","aiLiveBatchWaitMs","parsed","Number","NaN","isFinite","AI_LIVE_BATCH_TIMEOUT_MS","aiLiveTimeoutMs","baseMs","longWait","Math","max","AI_EFFORT_ARM_ENV","AI_EFFORT_LEVELS","aiDevEffortArm","arm","pair","split","kind","level","map","part","includes","Object","fromEntries","entries","sort","a","b","aiDevEffortFor","step"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/runtime/ai-dev-env.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Which development layers this process runs (AGL-3660): the replay cache,\n * the hour-long prompt cache and batch mode, each read from the environment.\n * Kept apart from `ai-dev-replay.ts`, which reads and writes files, so the\n * runtime asks these questions on every request without loading a file\n * system module into a server that will never use one. Every answer is `off`\n * in a deployed server.\n */\n\nimport type { AiEffort } from '../providers/contract'\n\nexport type AiDevReplayMode = 'off' | 'replay' | 'refresh'\n\n/** The variable that turns the replay cache on, off, or to refresh. */\nexport const AI_REPLAY_ENV = 'AGLYN_AI_REPLAY'\n/** Where the answers are kept, when not under the repository's `.cache/ai-replay`. */\nexport const AI_REPLAY_DIR_ENV = 'AGLYN_AI_REPLAY_DIR'\n/** The prompt-cache lifetime a development run asks for: `1h` or `5m`. */\nexport const AI_DEV_CACHE_TTL_ENV = 'AGLYN_AI_CACHE_TTL'\n/** The mark the live-eval launcher leaves, which `jest.setup.js` also reads. */\nexport const AI_LIVE_LAUNCHER_ENV = 'AI_EVAL_LIVE_LAUNCHER'\n\nexport type AiDevEnv = Readonly<Record<string, string | undefined>>\n\n/** Whether this process is a deployed server, where no development layer may run. */\nexport function aiIsDeployedRuntime(env: AiDevEnv = process.env): boolean {\n return (\n env['NODE_ENV'] === 'production' ||\n env['VERCEL_ENV'] === 'production' ||\n env['VERCEL_ENV'] === 'preview' ||\n Boolean(env['K_SERVICE'])\n )\n}\n\n/** The variable every live spec is started with. */\nexport const AI_LIVE_RUN_ENV = 'AGLYN_LIVE_AI'\n\n/**\n * Whether this process is a development run that asked for the live\n * provider: through the launcher, or a live spec started with\n * `AGLYN_LIVE_AI=1` (AGL-3781). Every development layer is on by default in\n * such a run — a live spec started without the launcher's mark used to send\n * every request again, at the five-minute cache and full price — and each can\n * still be turned off by its own variable.\n */\nexport function aiUnderLiveRun(env: AiDevEnv = process.env): boolean {\n if (aiIsDeployedRuntime(env)) return false\n return Boolean(env[AI_LIVE_LAUNCHER_ENV]) || String(env[AI_LIVE_RUN_ENV] ?? '').trim() === '1'\n}\n\n/** The replay mode this process runs under; `off` in any deployed server. */\nexport function aiDevReplayMode(env: AiDevEnv = process.env): AiDevReplayMode {\n if (aiIsDeployedRuntime(env)) return 'off'\n const raw = String(env[AI_REPLAY_ENV] ?? '').trim().toLowerCase()\n if (raw === 'refresh') return 'refresh'\n if (raw === '1' || raw === 'on' || raw === 'true' || raw === 'replay') return 'replay'\n if (raw === '0' || raw === 'off' || raw === 'false') return 'off'\n return aiUnderLiveRun(env) ? 'replay' : 'off'\n}\n\n/**\n * The prompt-cache lifetime a development run asks for. An agent re-runs a\n * live eval every ten to forty minutes while it iterates, which is past the\n * default five-minute entry and inside an hour, so under the launcher the\n * breakpoints ask for the hour: the second run reads the prefix the first one\n * wrote instead of writing it again. A deployed server never reads this:\n * `undefined` there, where the lifetime is the route's own\n * (`aiRequestCacheTtl`).\n */\nexport function aiDevCacheTtl(env: AiDevEnv = process.env): '1h' | undefined {\n if (aiIsDeployedRuntime(env)) return undefined\n const raw = String(env[AI_DEV_CACHE_TTL_ENV] ?? '').trim().toLowerCase()\n if (raw === '1h') return '1h'\n if (raw === '5m') return undefined\n return aiUnderLiveRun(env) ? '1h' : undefined\n}\n\n/**\n * The lifetime a request goes out with: the one its door asked for, which a\n * site job's routes set to the hour through their routing rows (AGL-3830),\n * else this process's own. A deployed server sends what the door asked for\n * and nothing else. In development `AGLYN_AI_CACHE_TTL=5m` turns every\n * request back to five minutes, the route's hour included, for a one-off\n * sweep that will not run again within the hour (docs/AI_CACHE_AUDIT_2026-10.md,\n * lever 5).\n */\nexport function aiRequestCacheTtl(requested: '1h' | undefined, env: AiDevEnv = process.env): '1h' | undefined {\n if (aiIsDeployedRuntime(env)) return requested\n if (String(env[AI_DEV_CACHE_TTL_ENV] ?? '').trim().toLowerCase() === '5m') return undefined\n return requested ?? aiDevCacheTtl(env)\n}\n\n/** A deployed server that was handed the variable says so once, and runs without it. */\nlet warnedDeployed = false\nexport function aiWarnIfReplayAskedInDeployment(env: AiDevEnv = process.env): void {\n if (warnedDeployed || !aiIsDeployedRuntime(env)) return\n if (!env[AI_REPLAY_ENV] && !env[AI_DEV_CACHE_TTL_ENV]) return\n warnedDeployed = true\n console.error(\n `[ai] ${AI_REPLAY_ENV} / ${AI_DEV_CACHE_TTL_ENV} are development-only and are ignored in a deployed server`,\n )\n}\n\n/** The variable that turns batch mode on (`runtime/ai-live-batch.ts`). */\nexport const AI_LIVE_BATCH_ENV = 'AGLYN_LIVE_AI_BATCH'\n\n/**\n * Whether this process batches its live requests: what `AGLYN_LIVE_AI_BATCH`\n * says, and in a live run that says nothing, yes (AGL-3781). Nobody waits on\n * an eval's answers, and a batch is half price on every token, cache reads\n * and writes included; `AGLYN_LIVE_AI_BATCH=0` is for the quick single run an\n * iteration wants back in seconds.\n */\nexport function aiLiveBatchWanted(env: AiDevEnv = process.env): boolean {\n if (aiIsDeployedRuntime(env)) return false\n const raw = String(env[AI_LIVE_BATCH_ENV] ?? '').trim()\n if (/^(?:1|true|yes|on)$/i.test(raw)) return true\n if (/^(?:0|false|no|off)$/i.test(raw)) return false\n return aiUnderLiveRun(env)\n}\n\n/** The variable that bounds how long a live run waits on one batch (AGL-3781). */\nexport const AI_LIVE_BATCH_WAIT_ENV = 'AGLYN_LIVE_AI_BATCH_WAIT_MS'\n\n/** How long a batch is waited on when the run names no wait. */\nexport const AI_LIVE_BATCH_DEFAULT_WAIT_MS = 30 * 60_000\n\n/**\n * How long a live run waits on one batch (AGL-3781): what\n * `AGLYN_LIVE_AI_BATCH_WAIT_MS` says, or half an hour. Most batches end in\n * minutes. One that has not by then is canceled, which keeps every answer it\n * already has, and the requests it left unanswered are asked on their own at\n * full price — so a slow batch costs a round part of its discount, never the\n * round.\n */\nexport function aiLiveBatchWaitMs(env: AiDevEnv = process.env): number {\n const raw = String(env[AI_LIVE_BATCH_WAIT_ENV] ?? '').trim()\n const parsed = raw ? Number(raw) : Number.NaN\n return Number.isFinite(parsed) && parsed >= 0 ? parsed : AI_LIVE_BATCH_DEFAULT_WAIT_MS\n}\n\n/**\n * The most a batched live run may take: a run's requests go as a few batches\n * one after another — the answers, their re-asks, the grades — and each is\n * waited on for `aiLiveBatchWaitMs`, then given time to end its cancel and to\n * have what it left asked on its own. A spec that timed out would keep none\n * of the answers it already paid for, so the bound is the worst case.\n */\nexport const AI_LIVE_BATCH_TIMEOUT_MS = 5 * 60 * 60_000\n\n/** A live spec's timeout: `baseMs`, or the batched run's when this run batches. */\nexport function aiLiveTimeoutMs(baseMs: number, env: AiDevEnv = process.env): number {\n if (!aiLiveBatchWanted(env)) return baseMs\n // Four batches in a row, each waited on, canceled and finished by hand.\n const longWait = 4 * (aiLiveBatchWaitMs(env) + 20 * 60_000)\n return Math.max(baseMs, AI_LIVE_BATCH_TIMEOUT_MS, longWait)\n}\n\n/**\n * The variable an effort A/B names its arm with (AGL-3781): a step kind's\n * effort for this run, as `job.form:medium,job.text:low`, or `*:medium` for\n * every kind. A development run only, so the defaults stay where production\n * runs them until a recorded round shows a lower effort holds.\n */\nexport const AI_EFFORT_ARM_ENV = 'AGLYN_AI_EFFORT'\n\nconst AI_EFFORT_LEVELS: readonly AiEffort[] = ['low', 'medium', 'high']\n\n/**\n * Every step kind this development run's A/B arm names, with its effort, in\n * kind order — `*` for every kind it does not name. A pair whose effort is no\n * level is ignored; a kind named twice keeps its first effort, and `*` its\n * last. Empty when the run names no arm, and in any deployed server.\n */\nexport function aiDevEffortArm(env: AiDevEnv = process.env): Record<string, AiEffort> {\n if (aiIsDeployedRuntime(env)) return {}\n const raw = String(env[AI_EFFORT_ARM_ENV] ?? '').trim()\n const arm: Record<string, AiEffort> = {}\n for (const pair of raw ? raw.split(',') : []) {\n const [kind, level] = pair.split(':').map((part) => part.trim())\n if (!kind || !AI_EFFORT_LEVELS.includes(level as AiEffort)) continue\n if (kind === '*' || !(kind in arm)) arm[kind] = level as AiEffort\n }\n return Object.fromEntries(Object.entries(arm).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)))\n}\n\n/** The effort this development run's A/B arm asks `step` at, or `undefined` for the step's own. */\nexport function aiDevEffortFor(step: string | undefined, env: AiDevEnv = process.env): AiEffort | undefined {\n const arm = aiDevEffortArm(env)\n return (step !== undefined && step !== '*' ? arm[step] : undefined) ?? arm['*']\n}\n"],"names":["AI_REPLAY_ENV","AI_REPLAY_DIR_ENV","AI_DEV_CACHE_TTL_ENV","AI_LIVE_LAUNCHER_ENV","aiIsDeployedRuntime","env","process","Boolean","AI_LIVE_RUN_ENV","aiUnderLiveRun","String","trim","aiDevReplayMode","raw","toLowerCase","aiDevCacheTtl","undefined","aiRequestCacheTtl","requested","warnedDeployed","aiWarnIfReplayAskedInDeployment","console","error","AI_LIVE_BATCH_ENV","aiLiveBatchWanted","test","AI_LIVE_BATCH_WAIT_ENV","AI_LIVE_BATCH_DEFAULT_WAIT_MS","aiLiveBatchWaitMs","parsed","Number","NaN","isFinite","AI_LIVE_BATCH_TIMEOUT_MS","aiLiveTimeoutMs","baseMs","longWait","Math","max","AI_EFFORT_ARM_ENV","AI_EFFORT_LEVELS","aiDevEffortArm","arm","pair","split","kind","level","map","part","includes","Object","fromEntries","entries","sort","a","b","aiDevEffortFor","step"],"mappings":"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"}
|
|
@@ -20,7 +20,8 @@ import { _ as _wrap_async_generator } from "@swc/helpers/_/_wrap_async_generator
|
|
|
20
20
|
*/ import { createHash } from "node:crypto";
|
|
21
21
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
22
22
|
import { dirname, join } from "node:path";
|
|
23
|
-
import { estimateAiProviderCostUsd } from "../providers/catalog.js";
|
|
23
|
+
import { aiProviderRatesForModel, estimateAiProviderCostUsd } from "../providers/catalog.js";
|
|
24
|
+
import { aiCacheWriteHourSurchargeUsd } from "../providers/cache-write-hour.js";
|
|
24
25
|
import { aiLiveBatchStats } from "./ai-live-batch.js";
|
|
25
26
|
import { AI_REPLAY_DIR_ENV, aiDevReplayMode } from "./ai-dev-env.js";
|
|
26
27
|
/** The key format's version: bumped when what a key covers changes, so no old answer is read under a new meaning. */ const KEY_VERSION = 1;
|
|
@@ -162,7 +163,8 @@ function round(usd) {
|
|
|
162
163
|
return Math.round(usd * 1000000) / 1000000;
|
|
163
164
|
}
|
|
164
165
|
function usageCost(usage, model) {
|
|
165
|
-
|
|
166
|
+
// A live run writes hour-long entries, billed above the rate table's write rate (AGL-3830).
|
|
167
|
+
return round(estimateAiProviderCostUsd(usage, model) + aiCacheWriteHourSurchargeUsd(usage, aiProviderRatesForModel(model)));
|
|
166
168
|
}
|
|
167
169
|
/** Counts one request a live path sent, at what the provider billed for it; `discount` is a batch's half. */ export function aiDevCountLive(usage, model, discount = 1) {
|
|
168
170
|
stats.live += 1;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/runtime/ai-dev-replay.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { createHash } from 'node:crypto'\nimport { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'\nimport { dirname, join } from 'node:path'\nimport { estimateAiProviderCostUsd } from '../providers/catalog'\nimport type {\n AiProvider,\n AiProviderRequest,\n AiResult,\n AiStreamEvent,\n AiSystemBlock,\n AiTool,\n} from '../providers/contract'\nimport { aiLiveBatchStats } from './ai-live-batch'\nimport { AI_REPLAY_DIR_ENV, aiDevReplayMode, type AiDevEnv as Env, type AiDevReplayMode } from './ai-dev-env'\n\n/** The key format's version: bumped when what a key covers changes, so no old answer is read under a new meaning. */\nconst KEY_VERSION = 1\n\n/**\n * THE DEVELOPMENT REPLAY CACHE (AGL-3660).\n *\n * Almost every dollar the provider bills in a month is a development run: a\n * live eval, the layout screenshots fed by one, a local guided start. Most of\n * the requests in a second run of the same eval are byte-identical to the\n * first — the change under test was to a compiler, a check or one prompt, not\n * to all of them — and each identical request bought the same sample twice.\n *\n * So, outside production only, the runtime keys each request by a hash of\n * everything that decides the answer — the provider, the model, the system\n * blocks, the tools, the messages, the ceiling, thinking and effort — and keeps\n * the provider's answer under `.cache/ai-replay/` (gitignored). The next\n * identical request is answered from disk for nothing; a request whose prompt\n * changed by one byte has a new key and goes to the provider. A replayed answer\n * is a real answer the same model gave to the same bytes, so an eval that\n * passes on replays has still seen every CHANGED prompt answered live: the\n * replay never stands in for a prompt the provider has not answered.\n *\n * `AGLYN_AI_REPLAY`:\n * - `1` / `on`: replay what is recorded, record what is not.\n * - `refresh`: every request live, its answer recorded over the old one.\n * - `off` / `0`: nothing read, nothing written.\n * - unset: on under the live-eval launcher (`AI_EVAL_LIVE_LAUNCHER`), off\n * everywhere else, so a dev server or a spec that never asked for it\n * behaves exactly as before.\n *\n * It never runs in a deployed server. `NODE_ENV=production`, `VERCEL_ENV`\n * of `production` or `preview`, or a Cloud Run service (`K_SERVICE`) turn it\n * off whatever the variable says, and the first request says so in the log:\n * a recorded answer handed to a customer would be another customer's page.\n *\n * Every live eval's table carries `aiDevReplayStats()`, so a run that was\n * all replays reads as one: `live: 0` is not a live pass.\n */\n\n// ── The key ──────────────────────────────────────────────────────────────\n\n/** JSON with every object's keys in sorted order: the same value, the same bytes, whatever built it. */\nexport function aiCanonicalJson(value: unknown): string {\n return JSON.stringify(sortKeys(value))\n}\n\nfunction sortKeys(value: unknown): unknown {\n if (Array.isArray(value)) return value.map(sortKeys)\n if (value && typeof value === 'object') {\n return Object.fromEntries(\n Object.keys(value as Record<string, unknown>)\n .sort()\n .filter((key) => (value as Record<string, unknown>)[key] !== undefined)\n .map((key) => [key, sortKeys((value as Record<string, unknown>)[key])]),\n )\n }\n return value\n}\n\n/** What a request's answer depends on. The key, the signal and the cache lifetime are not part of it. */\nexport interface AiReplayRequest {\n model: string\n system: readonly AiSystemBlock[]\n messages: AiProviderRequest['messages']\n tools?: readonly AiTool[]\n maxTokens: number\n thinking?: AiProviderRequest['thinking']\n effort?: AiProviderRequest['effort']\n}\n\n/** The request's replay key: a SHA-256 of everything that decides the answer. */\nexport function aiDevReplayKey(providerId: string, stream: boolean, request: AiReplayRequest): string {\n return createHash('sha256')\n .update(\n aiCanonicalJson({\n v: KEY_VERSION,\n provider: providerId,\n stream,\n model: request.model,\n // A breakpoint changes no answer, so the key reads the text alone.\n system: request.system.map((block) => block.text),\n tools: request.tools ?? [],\n messages: request.messages,\n maxTokens: request.maxTokens,\n thinking: request.thinking ?? null,\n effort: request.effort ?? null,\n }),\n )\n .digest('hex')\n}\n\n/**\n * The prefix a provider caches: the model, the tools and every system block\n * through the last breakpoint. Two requests with the same prefix key read one\n * cache entry; `null` when the request marks no breakpoint.\n */\nexport function aiCachedPrefixKey(request: Pick<AiReplayRequest, 'model' | 'system' | 'tools'>): string | null {\n const last = request.system.map((block) => Boolean(block.cacheBreakpoint)).lastIndexOf(true)\n if (last < 0) return null\n return createHash('sha256')\n .update(\n aiCanonicalJson({\n model: request.model,\n tools: request.tools ?? [],\n system: request.system.slice(0, last + 1).map((block) => block.text),\n }),\n )\n .digest('hex')\n}\n\n// ── The store ────────────────────────────────────────────────────────────\n\n/** The repository root: the nearest directory up from here holding `nx.json`, else the working directory. */\nfunction repositoryRoot(start: string = process.cwd()): string {\n let dir = start\n for (;;) {\n if (existsSync(join(dir, 'nx.json'))) return dir\n const parent = dirname(dir)\n if (parent === dir) return start\n dir = parent\n }\n}\n\n/** Where recorded answers are kept. */\nexport function aiDevReplayDir(env: Env = process.env): string {\n return env[AI_REPLAY_DIR_ENV] || join(repositoryRoot(), '.cache', 'ai-replay')\n}\n\ntype Recording =\n | { v: number; key: string; model: string; recordedAt: string; kind: 'result'; result: AiResult }\n | { v: number; key: string; model: string; recordedAt: string; kind: 'stream'; events: AiStreamEvent[] }\n\nfunction pathFor(dir: string, key: string): string {\n return join(dir, key.slice(0, 2), `${key}.json`)\n}\n\nfunction readRecording(dir: string, key: string): Recording | null {\n const path = pathFor(dir, key)\n if (!existsSync(path)) return null\n try {\n const parsed = JSON.parse(readFileSync(path, 'utf8')) as Recording\n return parsed?.v === KEY_VERSION && parsed.key === key ? parsed : null\n } catch {\n return null\n }\n}\n\nfunction writeRecording(dir: string, recording: Recording): void {\n const path = pathFor(dir, recording.key)\n mkdirSync(dirname(path), { recursive: true })\n // Written aside and renamed, so a run killed mid-write leaves no half file to replay.\n const temp = `${path}.${process.pid}.tmp`\n writeFileSync(temp, JSON.stringify(recording))\n renameSync(temp, path)\n}\n\n// ── What a run did ───────────────────────────────────────────────────────\n\n/** How many of this process's requests were sent, and how many were answered from disk. */\nexport interface AiDevReplayStats {\n mode: AiDevReplayMode\n /** Requests sent to the provider. */\n live: number\n /** Requests answered from a recording. */\n replayed: number\n /** What the live requests cost at the provider's list rates. */\n liveProviderUsd: number\n /** What the replayed ones would have cost, had they been sent. */\n savedProviderUsd: number\n}\n\nconst stats: AiDevReplayStats = {\n mode: 'off',\n live: 0,\n replayed: 0,\n liveProviderUsd: 0,\n savedProviderUsd: 0,\n}\n\n/** This process's count so far: what a live eval prints beside its table. */\nexport function aiDevReplayStats(env: Env = process.env): AiDevReplayStats {\n return {\n ...stats,\n mode: aiDevReplayMode(env),\n liveProviderUsd: round(stats.liveProviderUsd),\n savedProviderUsd: round(stats.savedProviderUsd),\n }\n}\n\n/** Starts the count again; a spec that runs two rounds reports each. */\nexport function resetAiDevReplayStats(): void {\n stats.live = 0\n stats.replayed = 0\n stats.liveProviderUsd = 0\n stats.savedProviderUsd = 0\n}\n\nfunction round(usd: number): number {\n return Math.round(usd * 1_000_000) / 1_000_000\n}\n\nfunction usageCost(usage: AiResult['usage'], model: string): number {\n return estimateAiProviderCostUsd(usage, model)\n}\n\n/** Counts one request a live path sent, at what the provider billed for it; `discount` is a batch's half. */\nexport function aiDevCountLive(usage: AiResult['usage'], model: string, discount = 1): void {\n stats.live += 1\n stats.liveProviderUsd += usageCost(usage, model) * discount\n}\n\n// ── The seam ─────────────────────────────────────────────────────────────\n\nasync function* replayEvents(events: readonly AiStreamEvent[]): AsyncGenerator<AiStreamEvent> {\n for (const event of events) yield event\n}\n\n/** A live stream passed through as it arrives, and recorded once it is done. */\nasync function* recordEvents(\n source: AsyncIterable<AiStreamEvent>,\n onDone: (events: AiStreamEvent[]) => void,\n): AsyncGenerator<AiStreamEvent> {\n const events: AiStreamEvent[] = []\n let completed = false\n for await (const event of source) {\n events.push(event)\n if (event.type === 'done') completed = true\n yield event\n }\n // A stream cut short, or one that carried a provider error, is not an\n // answer worth keeping: the next run asks again.\n if (completed && !events.some((event) => event.type === 'error')) onDone(events)\n}\n\n/**\n * One request through the replay cache: answered from disk when recorded and\n * the mode replays, otherwise sent through `send` and recorded. `send` is the\n * live path — the provider, or the batch queue — and counts its own spend.\n */\nexport async function aiDevReplayComplete(\n provider: Pick<AiProvider, 'id'>,\n request: AiReplayRequest,\n mode: Exclude<AiDevReplayMode, 'off'>,\n send: () => Promise<AiResult>,\n env: Env = process.env,\n): Promise<AiResult> {\n const dir = aiDevReplayDir(env)\n const key = aiDevReplayKey(provider.id, false, request)\n const recorded = mode === 'replay' ? readRecording(dir, key) : null\n if (recorded?.kind === 'result') {\n stats.replayed += 1\n stats.savedProviderUsd += usageCost(recorded.result.usage, request.model)\n return recorded.result\n }\n const result = await send()\n writeRecording(dir, {\n v: KEY_VERSION,\n key,\n model: request.model,\n recordedAt: new Date().toISOString(),\n kind: 'result',\n result,\n })\n return result\n}\n\n/** The streaming half of {@link aiDevReplayComplete}. */\nexport async function aiDevReplayStream(\n provider: Pick<AiProvider, 'id'>,\n request: AiReplayRequest,\n mode: Exclude<AiDevReplayMode, 'off'>,\n send: () => Promise<AsyncIterable<AiStreamEvent>>,\n env: Env = process.env,\n): Promise<AsyncIterable<AiStreamEvent>> {\n const dir = aiDevReplayDir(env)\n const key = aiDevReplayKey(provider.id, true, request)\n const recorded = mode === 'replay' ? readRecording(dir, key) : null\n if (recorded?.kind === 'stream') {\n stats.replayed += 1\n const done = recorded.events.find((event) => event.type === 'done')\n if (done?.type === 'done') stats.savedProviderUsd += usageCost(done.usage, request.model)\n return replayEvents(recorded.events)\n }\n const live = await send()\n return recordEvents(live, (events) => {\n const done = events.find((event) => event.type === 'done')\n if (done?.type === 'done') aiDevCountLive(done.usage, request.model)\n writeRecording(dir, {\n v: KEY_VERSION,\n key,\n model: request.model,\n recordedAt: new Date().toISOString(),\n kind: 'stream',\n events,\n })\n })\n}\n\n// ── Warming a shared prefix before a fan-out ─────────────────────────────\n\n/**\n * A cache entry is readable only once the request that writes it has started\n * answering, so N requests sent at once over one prefix all pay to write it\n * and none reads it — which is exactly what a live eval's `Promise.all` over\n * its briefs did. In a development run the first request over a prefix goes\n * alone; the others wait for it and then read what it wrote. A prefix\n * answered within the cache's lifetime is not held again.\n *\n * Development only, like everything here: production's beat runs one step at\n * a time, and holding a customer's request behind another's would trade\n * their seconds for our cents.\n */\nconst warming = new Map<string, Promise<unknown>>()\nconst warmedAt = new Map<string, number>()\n\nexport async function aiDevWarmPrefixFirst<T>(\n prefixKey: string | null,\n ttlMs: number,\n send: () => Promise<T>,\n now: () => number = Date.now,\n): Promise<T> {\n if (!prefixKey) return send()\n const warmed = warmedAt.get(prefixKey)\n if (warmed !== undefined && now() - warmed < ttlMs) return send()\n const pending = warming.get(prefixKey)\n if (pending) {\n await pending.catch(() => undefined)\n return send()\n }\n const first = send()\n warming.set(prefixKey, first)\n try {\n const result = await first\n warmedAt.set(prefixKey, now())\n return result\n } finally {\n warming.delete(prefixKey)\n }\n}\n\n/** Forgets every warmed prefix; for specs. */\nexport function resetAiDevWarmPrefixes(): void {\n warming.clear()\n warmedAt.clear()\n}\n\n// ── What a live eval prints ──────────────────────────────────────────────\n\n/**\n * The line every live eval's table carries (AGL-3660): how many of its\n * requests went to the provider and how many were answered from disk, what\n * the live ones cost at list, and how many batches carried them. A table\n * whose `live` is 0 proved nothing new — every prompt in it had been answered\n * before — and is not a live pass; `AGLYN_AI_REPLAY=refresh` asks again.\n * `batchedRequests` are the ones a batch answered, at half price;\n * `batchFallbacks` are the ones a batch left unanswered past its wait and\n * that were asked on their own, at full price (AGL-3781). Both count in `live`.\n */\nexport function aiLiveRunLedger(env: Env = process.env): AiDevReplayStats & {\n batches: number\n batchedRequests: number\n batchFallbacks: number\n allReplayed: boolean\n} {\n const replay = aiDevReplayStats(env)\n const batch = aiLiveBatchStats()\n return {\n ...replay,\n batches: batch.batches,\n batchedRequests: batch.requests,\n batchFallbacks: batch.fallbacks,\n allReplayed: replay.live === 0 && replay.replayed > 0,\n }\n}\n"],"names":["createHash","existsSync","mkdirSync","readFileSync","renameSync","writeFileSync","dirname","join","estimateAiProviderCostUsd","aiLiveBatchStats","AI_REPLAY_DIR_ENV","aiDevReplayMode","KEY_VERSION","aiCanonicalJson","value","JSON","stringify","sortKeys","Array","isArray","map","Object","fromEntries","keys","sort","filter","key","undefined","aiDevReplayKey","providerId","stream","request","update","v","provider","model","system","block","text","tools","messages","maxTokens","thinking","effort","digest","aiCachedPrefixKey","last","Boolean","cacheBreakpoint","lastIndexOf","slice","repositoryRoot","start","process","cwd","dir","parent","aiDevReplayDir","env","pathFor","readRecording","path","parsed","parse","writeRecording","recording","recursive","temp","pid","stats","mode","live","replayed","liveProviderUsd","savedProviderUsd","aiDevReplayStats","round","resetAiDevReplayStats","usd","Math","usageCost","usage","aiDevCountLive","discount","replayEvents","events","event","recordEvents","source","onDone","completed","push","type","some","aiDevReplayComplete","send","id","recorded","kind","result","recordedAt","Date","toISOString","aiDevReplayStream","done","find","warming","Map","warmedAt","aiDevWarmPrefixFirst","prefixKey","ttlMs","now","warmed","get","pending","catch","first","set","delete","resetAiDevWarmPrefixes","clear","aiLiveRunLedger","replay","batch","batches","batchedRequests","requests","batchFallbacks","fallbacks","allReplayed"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/runtime/ai-dev-replay.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { createHash } from 'node:crypto'\nimport { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'\nimport { dirname, join } from 'node:path'\nimport { aiProviderRatesForModel, estimateAiProviderCostUsd } from '../providers/catalog'\nimport { aiCacheWriteHourSurchargeUsd } from '../providers/cache-write-hour'\nimport type {\n AiProvider,\n AiProviderRequest,\n AiResult,\n AiStreamEvent,\n AiSystemBlock,\n AiTool,\n} from '../providers/contract'\nimport { aiLiveBatchStats } from './ai-live-batch'\nimport { AI_REPLAY_DIR_ENV, aiDevReplayMode, type AiDevEnv as Env, type AiDevReplayMode } from './ai-dev-env'\n\n/** The key format's version: bumped when what a key covers changes, so no old answer is read under a new meaning. */\nconst KEY_VERSION = 1\n\n/**\n * THE DEVELOPMENT REPLAY CACHE (AGL-3660).\n *\n * Almost every dollar the provider bills in a month is a development run: a\n * live eval, the layout screenshots fed by one, a local guided start. Most of\n * the requests in a second run of the same eval are byte-identical to the\n * first — the change under test was to a compiler, a check or one prompt, not\n * to all of them — and each identical request bought the same sample twice.\n *\n * So, outside production only, the runtime keys each request by a hash of\n * everything that decides the answer — the provider, the model, the system\n * blocks, the tools, the messages, the ceiling, thinking and effort — and keeps\n * the provider's answer under `.cache/ai-replay/` (gitignored). The next\n * identical request is answered from disk for nothing; a request whose prompt\n * changed by one byte has a new key and goes to the provider. A replayed answer\n * is a real answer the same model gave to the same bytes, so an eval that\n * passes on replays has still seen every CHANGED prompt answered live: the\n * replay never stands in for a prompt the provider has not answered.\n *\n * `AGLYN_AI_REPLAY`:\n * - `1` / `on`: replay what is recorded, record what is not.\n * - `refresh`: every request live, its answer recorded over the old one.\n * - `off` / `0`: nothing read, nothing written.\n * - unset: on under the live-eval launcher (`AI_EVAL_LIVE_LAUNCHER`), off\n * everywhere else, so a dev server or a spec that never asked for it\n * behaves exactly as before.\n *\n * It never runs in a deployed server. `NODE_ENV=production`, `VERCEL_ENV`\n * of `production` or `preview`, or a Cloud Run service (`K_SERVICE`) turn it\n * off whatever the variable says, and the first request says so in the log:\n * a recorded answer handed to a customer would be another customer's page.\n *\n * Every live eval's table carries `aiDevReplayStats()`, so a run that was\n * all replays reads as one: `live: 0` is not a live pass.\n */\n\n// ── The key ──────────────────────────────────────────────────────────────\n\n/** JSON with every object's keys in sorted order: the same value, the same bytes, whatever built it. */\nexport function aiCanonicalJson(value: unknown): string {\n return JSON.stringify(sortKeys(value))\n}\n\nfunction sortKeys(value: unknown): unknown {\n if (Array.isArray(value)) return value.map(sortKeys)\n if (value && typeof value === 'object') {\n return Object.fromEntries(\n Object.keys(value as Record<string, unknown>)\n .sort()\n .filter((key) => (value as Record<string, unknown>)[key] !== undefined)\n .map((key) => [key, sortKeys((value as Record<string, unknown>)[key])]),\n )\n }\n return value\n}\n\n/** What a request's answer depends on. The key, the signal and the cache lifetime are not part of it. */\nexport interface AiReplayRequest {\n model: string\n system: readonly AiSystemBlock[]\n messages: AiProviderRequest['messages']\n tools?: readonly AiTool[]\n maxTokens: number\n thinking?: AiProviderRequest['thinking']\n effort?: AiProviderRequest['effort']\n}\n\n/** The request's replay key: a SHA-256 of everything that decides the answer. */\nexport function aiDevReplayKey(providerId: string, stream: boolean, request: AiReplayRequest): string {\n return createHash('sha256')\n .update(\n aiCanonicalJson({\n v: KEY_VERSION,\n provider: providerId,\n stream,\n model: request.model,\n // A breakpoint changes no answer, so the key reads the text alone.\n system: request.system.map((block) => block.text),\n tools: request.tools ?? [],\n messages: request.messages,\n maxTokens: request.maxTokens,\n thinking: request.thinking ?? null,\n effort: request.effort ?? null,\n }),\n )\n .digest('hex')\n}\n\n/**\n * The prefix a provider caches: the model, the tools and every system block\n * through the last breakpoint. Two requests with the same prefix key read one\n * cache entry; `null` when the request marks no breakpoint.\n */\nexport function aiCachedPrefixKey(request: Pick<AiReplayRequest, 'model' | 'system' | 'tools'>): string | null {\n const last = request.system.map((block) => Boolean(block.cacheBreakpoint)).lastIndexOf(true)\n if (last < 0) return null\n return createHash('sha256')\n .update(\n aiCanonicalJson({\n model: request.model,\n tools: request.tools ?? [],\n system: request.system.slice(0, last + 1).map((block) => block.text),\n }),\n )\n .digest('hex')\n}\n\n// ── The store ────────────────────────────────────────────────────────────\n\n/** The repository root: the nearest directory up from here holding `nx.json`, else the working directory. */\nfunction repositoryRoot(start: string = process.cwd()): string {\n let dir = start\n for (;;) {\n if (existsSync(join(dir, 'nx.json'))) return dir\n const parent = dirname(dir)\n if (parent === dir) return start\n dir = parent\n }\n}\n\n/** Where recorded answers are kept. */\nexport function aiDevReplayDir(env: Env = process.env): string {\n return env[AI_REPLAY_DIR_ENV] || join(repositoryRoot(), '.cache', 'ai-replay')\n}\n\ntype Recording =\n | { v: number; key: string; model: string; recordedAt: string; kind: 'result'; result: AiResult }\n | { v: number; key: string; model: string; recordedAt: string; kind: 'stream'; events: AiStreamEvent[] }\n\nfunction pathFor(dir: string, key: string): string {\n return join(dir, key.slice(0, 2), `${key}.json`)\n}\n\nfunction readRecording(dir: string, key: string): Recording | null {\n const path = pathFor(dir, key)\n if (!existsSync(path)) return null\n try {\n const parsed = JSON.parse(readFileSync(path, 'utf8')) as Recording\n return parsed?.v === KEY_VERSION && parsed.key === key ? parsed : null\n } catch {\n return null\n }\n}\n\nfunction writeRecording(dir: string, recording: Recording): void {\n const path = pathFor(dir, recording.key)\n mkdirSync(dirname(path), { recursive: true })\n // Written aside and renamed, so a run killed mid-write leaves no half file to replay.\n const temp = `${path}.${process.pid}.tmp`\n writeFileSync(temp, JSON.stringify(recording))\n renameSync(temp, path)\n}\n\n// ── What a run did ───────────────────────────────────────────────────────\n\n/** How many of this process's requests were sent, and how many were answered from disk. */\nexport interface AiDevReplayStats {\n mode: AiDevReplayMode\n /** Requests sent to the provider. */\n live: number\n /** Requests answered from a recording. */\n replayed: number\n /** What the live requests cost at the provider's list rates. */\n liveProviderUsd: number\n /** What the replayed ones would have cost, had they been sent. */\n savedProviderUsd: number\n}\n\nconst stats: AiDevReplayStats = {\n mode: 'off',\n live: 0,\n replayed: 0,\n liveProviderUsd: 0,\n savedProviderUsd: 0,\n}\n\n/** This process's count so far: what a live eval prints beside its table. */\nexport function aiDevReplayStats(env: Env = process.env): AiDevReplayStats {\n return {\n ...stats,\n mode: aiDevReplayMode(env),\n liveProviderUsd: round(stats.liveProviderUsd),\n savedProviderUsd: round(stats.savedProviderUsd),\n }\n}\n\n/** Starts the count again; a spec that runs two rounds reports each. */\nexport function resetAiDevReplayStats(): void {\n stats.live = 0\n stats.replayed = 0\n stats.liveProviderUsd = 0\n stats.savedProviderUsd = 0\n}\n\nfunction round(usd: number): number {\n return Math.round(usd * 1_000_000) / 1_000_000\n}\n\nfunction usageCost(usage: AiResult['usage'], model: string): number {\n // A live run writes hour-long entries, billed above the rate table's write rate (AGL-3830).\n return round(estimateAiProviderCostUsd(usage, model) + aiCacheWriteHourSurchargeUsd(usage, aiProviderRatesForModel(model)))\n}\n\n/** Counts one request a live path sent, at what the provider billed for it; `discount` is a batch's half. */\nexport function aiDevCountLive(usage: AiResult['usage'], model: string, discount = 1): void {\n stats.live += 1\n stats.liveProviderUsd += usageCost(usage, model) * discount\n}\n\n// ── The seam ─────────────────────────────────────────────────────────────\n\nasync function* replayEvents(events: readonly AiStreamEvent[]): AsyncGenerator<AiStreamEvent> {\n for (const event of events) yield event\n}\n\n/** A live stream passed through as it arrives, and recorded once it is done. */\nasync function* recordEvents(\n source: AsyncIterable<AiStreamEvent>,\n onDone: (events: AiStreamEvent[]) => void,\n): AsyncGenerator<AiStreamEvent> {\n const events: AiStreamEvent[] = []\n let completed = false\n for await (const event of source) {\n events.push(event)\n if (event.type === 'done') completed = true\n yield event\n }\n // A stream cut short, or one that carried a provider error, is not an\n // answer worth keeping: the next run asks again.\n if (completed && !events.some((event) => event.type === 'error')) onDone(events)\n}\n\n/**\n * One request through the replay cache: answered from disk when recorded and\n * the mode replays, otherwise sent through `send` and recorded. `send` is the\n * live path — the provider, or the batch queue — and counts its own spend.\n */\nexport async function aiDevReplayComplete(\n provider: Pick<AiProvider, 'id'>,\n request: AiReplayRequest,\n mode: Exclude<AiDevReplayMode, 'off'>,\n send: () => Promise<AiResult>,\n env: Env = process.env,\n): Promise<AiResult> {\n const dir = aiDevReplayDir(env)\n const key = aiDevReplayKey(provider.id, false, request)\n const recorded = mode === 'replay' ? readRecording(dir, key) : null\n if (recorded?.kind === 'result') {\n stats.replayed += 1\n stats.savedProviderUsd += usageCost(recorded.result.usage, request.model)\n return recorded.result\n }\n const result = await send()\n writeRecording(dir, {\n v: KEY_VERSION,\n key,\n model: request.model,\n recordedAt: new Date().toISOString(),\n kind: 'result',\n result,\n })\n return result\n}\n\n/** The streaming half of {@link aiDevReplayComplete}. */\nexport async function aiDevReplayStream(\n provider: Pick<AiProvider, 'id'>,\n request: AiReplayRequest,\n mode: Exclude<AiDevReplayMode, 'off'>,\n send: () => Promise<AsyncIterable<AiStreamEvent>>,\n env: Env = process.env,\n): Promise<AsyncIterable<AiStreamEvent>> {\n const dir = aiDevReplayDir(env)\n const key = aiDevReplayKey(provider.id, true, request)\n const recorded = mode === 'replay' ? readRecording(dir, key) : null\n if (recorded?.kind === 'stream') {\n stats.replayed += 1\n const done = recorded.events.find((event) => event.type === 'done')\n if (done?.type === 'done') stats.savedProviderUsd += usageCost(done.usage, request.model)\n return replayEvents(recorded.events)\n }\n const live = await send()\n return recordEvents(live, (events) => {\n const done = events.find((event) => event.type === 'done')\n if (done?.type === 'done') aiDevCountLive(done.usage, request.model)\n writeRecording(dir, {\n v: KEY_VERSION,\n key,\n model: request.model,\n recordedAt: new Date().toISOString(),\n kind: 'stream',\n events,\n })\n })\n}\n\n// ── Warming a shared prefix before a fan-out ─────────────────────────────\n\n/**\n * A cache entry is readable only once the request that writes it has started\n * answering, so N requests sent at once over one prefix all pay to write it\n * and none reads it — which is exactly what a live eval's `Promise.all` over\n * its briefs did. In a development run the first request over a prefix goes\n * alone; the others wait for it and then read what it wrote. A prefix\n * answered within the cache's lifetime is not held again.\n *\n * Development only, like everything here: production's beat runs one step at\n * a time, and holding a customer's request behind another's would trade\n * their seconds for our cents.\n */\nconst warming = new Map<string, Promise<unknown>>()\nconst warmedAt = new Map<string, number>()\n\nexport async function aiDevWarmPrefixFirst<T>(\n prefixKey: string | null,\n ttlMs: number,\n send: () => Promise<T>,\n now: () => number = Date.now,\n): Promise<T> {\n if (!prefixKey) return send()\n const warmed = warmedAt.get(prefixKey)\n if (warmed !== undefined && now() - warmed < ttlMs) return send()\n const pending = warming.get(prefixKey)\n if (pending) {\n await pending.catch(() => undefined)\n return send()\n }\n const first = send()\n warming.set(prefixKey, first)\n try {\n const result = await first\n warmedAt.set(prefixKey, now())\n return result\n } finally {\n warming.delete(prefixKey)\n }\n}\n\n/** Forgets every warmed prefix; for specs. */\nexport function resetAiDevWarmPrefixes(): void {\n warming.clear()\n warmedAt.clear()\n}\n\n// ── What a live eval prints ──────────────────────────────────────────────\n\n/**\n * The line every live eval's table carries (AGL-3660): how many of its\n * requests went to the provider and how many were answered from disk, what\n * the live ones cost at list, and how many batches carried them. A table\n * whose `live` is 0 proved nothing new — every prompt in it had been answered\n * before — and is not a live pass; `AGLYN_AI_REPLAY=refresh` asks again.\n * `batchedRequests` are the ones a batch answered, at half price;\n * `batchFallbacks` are the ones a batch left unanswered past its wait and\n * that were asked on their own, at full price (AGL-3781). Both count in `live`.\n */\nexport function aiLiveRunLedger(env: Env = process.env): AiDevReplayStats & {\n batches: number\n batchedRequests: number\n batchFallbacks: number\n allReplayed: boolean\n} {\n const replay = aiDevReplayStats(env)\n const batch = aiLiveBatchStats()\n return {\n ...replay,\n batches: batch.batches,\n batchedRequests: batch.requests,\n batchFallbacks: batch.fallbacks,\n allReplayed: replay.live === 0 && replay.replayed > 0,\n }\n}\n"],"names":["createHash","existsSync","mkdirSync","readFileSync","renameSync","writeFileSync","dirname","join","aiProviderRatesForModel","estimateAiProviderCostUsd","aiCacheWriteHourSurchargeUsd","aiLiveBatchStats","AI_REPLAY_DIR_ENV","aiDevReplayMode","KEY_VERSION","aiCanonicalJson","value","JSON","stringify","sortKeys","Array","isArray","map","Object","fromEntries","keys","sort","filter","key","undefined","aiDevReplayKey","providerId","stream","request","update","v","provider","model","system","block","text","tools","messages","maxTokens","thinking","effort","digest","aiCachedPrefixKey","last","Boolean","cacheBreakpoint","lastIndexOf","slice","repositoryRoot","start","process","cwd","dir","parent","aiDevReplayDir","env","pathFor","readRecording","path","parsed","parse","writeRecording","recording","recursive","temp","pid","stats","mode","live","replayed","liveProviderUsd","savedProviderUsd","aiDevReplayStats","round","resetAiDevReplayStats","usd","Math","usageCost","usage","aiDevCountLive","discount","replayEvents","events","event","recordEvents","source","onDone","completed","push","type","some","aiDevReplayComplete","send","id","recorded","kind","result","recordedAt","Date","toISOString","aiDevReplayStream","done","find","warming","Map","warmedAt","aiDevWarmPrefixFirst","prefixKey","ttlMs","now","warmed","get","pending","catch","first","set","delete","resetAiDevWarmPrefixes","clear","aiLiveRunLedger","replay","batch","batches","batchedRequests","requests","batchFallbacks","fallbacks","allReplayed"],"mappings":"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"}
|
|
@@ -594,8 +594,12 @@ export declare function aiTreeCopy(tree: AiDoctrineTree, context?: AiNodeTreeCon
|
|
|
594
594
|
* `null`. A line that closes itself, ends on a binding or a bracketed fact the
|
|
595
595
|
* member fills, or ends on any other word, is finished as far as a check can
|
|
596
596
|
* tell. Exported for the specs that hold the closed list.
|
|
597
|
+
*
|
|
598
|
+
* `language` is the site's language code (`es`, `pt-BR`): a line in one with
|
|
599
|
+
* a table above is held to that table's words (AGL-3828), and every other
|
|
600
|
+
* line, an unknown language's included, to English's.
|
|
597
601
|
*/
|
|
598
|
-
export declare function aiDanglingWord(line: string): string | null;
|
|
602
|
+
export declare function aiDanglingWord(line: string, language?: string | null): string | null;
|
|
599
603
|
/**
|
|
600
604
|
* Rule 14, for a line cut short (AGL-3072). Copy in the site's voice is
|
|
601
605
|
* finished copy: a heading, a subhead or a body text that ends on an article
|
|
@@ -640,6 +644,42 @@ export declare function detectHeavyDocument(tree: AiDoctrineTree, outputKind: Ai
|
|
|
640
644
|
* is not reported twice.
|
|
641
645
|
*/
|
|
642
646
|
export declare function detectEmptyItems(tree: AiDoctrineTree): AiDoctrineViolation[];
|
|
647
|
+
/**
|
|
648
|
+
* Rule 14, for a placeholder left in copy (AGL-3827): `{{1}}`, `{{name}}`,
|
|
649
|
+
* `[[Customer name]]`, `<Your business name>` (`ai-placeholder-tokens.ts`).
|
|
650
|
+
* A visitor reads it exactly as written. A live About page's two testimonials
|
|
651
|
+
* did, quote and name. A fact the brief never gave, in single square
|
|
652
|
+
* brackets, is the honest marker this rule asks for and is never one
|
|
653
|
+
* (AGL-3056); Lorem ipsum is `filler-copy`'s.
|
|
654
|
+
*/
|
|
655
|
+
export declare function detectPlaceholderTokens(tree: AiDoctrineTree, outputKind: AiOutputKind, context?: AiNodeTreeContext): AiDoctrineViolation[];
|
|
656
|
+
/** Rule 14 on copy that is not a tree, such as a plan's titles and search fields, named by path. */
|
|
657
|
+
export declare function detectPlaceholderCopy(samples: readonly AiCopySample[]): AiDoctrineViolation[];
|
|
658
|
+
/**
|
|
659
|
+
* An item of a list that shows only placeholders, taken out where its list
|
|
660
|
+
* keeps a real one (AGL-3827). Its copy says nothing a reader could keep, so
|
|
661
|
+
* taking it out loses no word, and the list still shows what it is for. This
|
|
662
|
+
* is the one answer the rule has without a model: anything else is the
|
|
663
|
+
* model's to write, through `placeholder-token`'s re-ask.
|
|
664
|
+
*
|
|
665
|
+
* An ITEM is the nearest node, from the copy up, that sits beside another
|
|
666
|
+
* of its own shape under one parent (`indexShapes`, with the components its
|
|
667
|
+
* instances place): a testimonial instance in a row of them, or the Grid item
|
|
668
|
+
* that holds it. It is taken out only when
|
|
669
|
+
*
|
|
670
|
+
* - every piece of its copy that holds a placeholder holds nothing else,
|
|
671
|
+
* such as a quote of `{{1}}` (a quote of `Thanks, {{name}}!` says
|
|
672
|
+
* something, and is the model's to finish);
|
|
673
|
+
* - an item of its shape beside it holds no placeholder, so the list keeps
|
|
674
|
+
* a real item: a list of only placeholders is not trimmed to nothing;
|
|
675
|
+
* - it is not a section, nor in one above the section it sits in: a page's
|
|
676
|
+
* sections are its plan's, and a section is never taken out here.
|
|
677
|
+
*
|
|
678
|
+
* Reads and returns the tree; a tree with nothing to settle comes back as the
|
|
679
|
+
* same object, so settling is idempotent, and a parent that is changed is
|
|
680
|
+
* copied, never edited in place.
|
|
681
|
+
*/
|
|
682
|
+
export declare function aiSettlePlaceholderItems<T extends AiDoctrineTree>(tree: T, outputKind: AiOutputKind, context?: AiNodeTreeContext): T;
|
|
643
683
|
export interface AiOutputScore {
|
|
644
684
|
nodes: number;
|
|
645
685
|
/** The stored node map, in the save's own bytes. */
|