{"version":3,"file":"deferral.d.ts","sourceRoot":"","sources":["../../../src/core/capabilities/deferral.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,uDAAuD;AACvD,MAAM,WAAW,WAAW;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,aAAa;IAC7B,sEAAsE;IACtE,cAAc,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,WAAW,CAAC;CACpB;AAED,MAAM,MAAM,eAAe,GACxB;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACpC;IAAE,IAAI,EAAE,kBAAkB,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC5C;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACpC;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,qBAAqB,EAAE,MAAM,CAAC;IAAC,gBAAgB,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,CAAA;CAAE,CAAC;AAO3G;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,aAAa,GAAG,eAAe,CA4BrE;AAQD;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,eAAe,EAAE,OAAO,GAAE;IAAE,aAAa,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,MAAM,CAsB1G","sourcesContent":["/**\n * Is deferring a tool's schema actually worth it?\n *\n * §6.3 said the threshold should be measurable rather than asserted, and until\n * now it was neither. The question turns out not to be \"how many tokens do the\n * schemas cost\" — `--print-token-surface` already answered that — but a\n * trade-off with a term nobody had priced:\n *\n *   Deferring withholds D tokens of schema from every request. But those tokens\n *   would have sat in the *cached* prefix, so what deferral saves per request is\n *   D at the cache-read rate, not at full price — an order of magnitude less\n *   than it looks. And the moment the model resolves a deferred tool, the\n *   harness adds it to `tools`, which renders at position 0 and invalidates the\n *   whole prefix P — so the next request pays P at the cache-*write* rate\n *   instead of the cache-read rate it would otherwise have paid.\n *\n * Break-even is therefore the number of requests before the first resolve:\n *\n *   N = P × (cacheWrite − cacheRead) / (D × cacheRead)\n *\n * The counterintuitive consequence is worth stating plainly: **P grows with the\n * conversation, D does not.** Deferral gets *worse* the longer a session runs,\n * which is the opposite of the intuition that motivated it.\n *\n * See docs/plugin-system-architecture.md §6.3 and §8.6 item 6.\n */\n\n/** Per-million-token prices, as carried on a model. */\nexport interface TokenPrices {\n\tinput: number;\n\tcacheRead: number;\n\tcacheWrite: number;\n}\n\nexport interface DeferralInput {\n\t/** Tokens of schema that would be withheld (the deferrable tools). */\n\tdeferredTokens: number;\n\t/**\n\t * Tokens in the cached prefix a resolve would invalidate: system prompt plus\n\t * every tool schema, and in a live session the conversation too. Callers that\n\t * only know the startup surface are supplying a floor — see `prefixIsFloor`.\n\t */\n\tprefixTokens: number;\n\tprices: TokenPrices;\n}\n\nexport type DeferralVerdict =\n\t| { kind: \"no-cache\"; reason: string }\n\t| { kind: \"nothing-deferred\"; reason: string }\n\t| { kind: \"unpriced\"; reason: string }\n\t| { kind: \"break-even\"; requestsBeforeResolve: number; savingPerRequest: number; resolvePenalty: number };\n\n/** Dollars for `tokens` at a per-million-token price. */\nfunction priceOf(tokens: number, perMillion: number): number {\n\treturn (tokens / 1_000_000) * perMillion;\n}\n\n/**\n * Price the deferral trade-off for one model.\n *\n * Deliberately returns a discriminated verdict rather than a bare number: three\n * of the four outcomes mean \"the break-even question does not apply here\", and\n * collapsing them into a number would invite reading 0 or Infinity as an answer.\n */\nexport function analyzeDeferral(input: DeferralInput): DeferralVerdict {\n\tconst { deferredTokens, prefixTokens, prices } = input;\n\n\tif (deferredTokens <= 0) {\n\t\treturn { kind: \"nothing-deferred\", reason: \"no deferrable tools in this configuration\" };\n\t}\n\tif (prices.input <= 0 && prices.cacheRead <= 0 && prices.cacheWrite <= 0) {\n\t\treturn { kind: \"unpriced\", reason: \"model carries no pricing, so the trade-off cannot be costed\" };\n\t}\n\t// A provider that does not price caching separately never had a cached prefix\n\t// to lose, so a resolve costs nothing extra and deferral is unambiguously\n\t// good — the saving is the full input rate on every request.\n\tif (prices.cacheRead <= 0 || prices.cacheWrite <= 0) {\n\t\treturn {\n\t\t\tkind: \"no-cache\",\n\t\t\treason: \"provider does not price prompt caching, so a resolve invalidates nothing — deferral is a pure saving\",\n\t\t};\n\t}\n\n\tconst savingPerRequest = priceOf(deferredTokens, prices.cacheRead);\n\tconst resolvePenalty = priceOf(prefixTokens, prices.cacheWrite - prices.cacheRead);\n\n\treturn {\n\t\tkind: \"break-even\",\n\t\trequestsBeforeResolve: resolvePenalty / savingPerRequest,\n\t\tsavingPerRequest,\n\t\tresolvePenalty,\n\t};\n}\n\nfunction usd(amount: number): string {\n\tif (amount === 0) return \"$0\";\n\tif (amount < 0.01) return `$${amount.toFixed(6)}`;\n\treturn `$${amount.toFixed(4)}`;\n}\n\n/**\n * Render the verdict for `--print-token-surface`.\n *\n * `prefixIsFloor` marks the common case where the caller measured the startup\n * surface only. The number is then a lower bound in the direction that matters:\n * the real prefix is larger, so the real break-even is higher and deferral looks\n * worse than printed, never better.\n */\nexport function formatDeferral(verdict: DeferralVerdict, options: { prefixIsFloor?: boolean } = {}): string {\n\tswitch (verdict.kind) {\n\t\tcase \"nothing-deferred\":\n\t\tcase \"unpriced\":\n\t\tcase \"no-cache\":\n\t\t\treturn `  deferral: ${verdict.reason}`;\n\t\tcase \"break-even\": {\n\t\t\tconst n = Math.ceil(verdict.requestsBeforeResolve);\n\t\t\tconst lines = [\n\t\t\t\t`  deferral saves ${usd(verdict.savingPerRequest)}/request (withheld schema at the cache-read rate)`,\n\t\t\t\t`  a resolve costs ${usd(verdict.resolvePenalty)} once (whole prefix re-written, not read)`,\n\t\t\t\t`  break-even: deferral pays off only if the model waits ${n}+ request(s) before resolving`,\n\t\t\t];\n\t\t\tif (options.prefixIsFloor) {\n\t\t\t\tlines.push(\n\t\t\t\t\t\"  (prefix measured at startup, so this is a floor — the conversation grows it,\",\n\t\t\t\t\t\"   and with it the resolve penalty, making deferral worse as a session runs on)\",\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn lines.join(\"\\n\");\n\t\t}\n\t}\n}\n"]}