From ef34a25ea3c029a205b27d84f583027e7b927a39 Mon Sep 17 00:00:00 2001 From: Drew Stone Date: Mon, 10 Aug 2026 23:48:31 -0600 Subject: [PATCH] fix(runtime): accept current cache usage receipts --- docs/04-runtime-contracts.md | 36 +++++---- docs/10-upstream-strategy.md | 4 +- package.json | 8 +- pnpm-lock.yaml | 74 ++++++++----------- pnpm-workspace.yaml | 10 +-- src/adapters/analysis/agent-eval-version.ts | 2 +- src/adapters/runtime/agent-runtime-version.ts | 2 +- src/domain/bounded-structured.ts | 68 +++++++++++++---- test/sanitize.test.ts | 64 +++++++++++++++- test/security.test.ts | 67 ++++++++++++++++- 10 files changed, 244 insertions(+), 91 deletions(-) diff --git a/docs/04-runtime-contracts.md b/docs/04-runtime-contracts.md index ddf1312..6191605 100644 --- a/docs/04-runtime-contracts.md +++ b/docs/04-runtime-contracts.md @@ -10,20 +10,20 @@ When a current package blocks a real Braid flow, Braid records the unavailable a ## Evidence baseline -The following published versions were queried from npm and their installed declarations were inspected directly on 2026-08-09. +The following published versions were queried from npm and their installed declarations were inspected directly on 2026-08-10. | Package | Installed version | Braid boundary | | --- | ---: | --- | | [`@tangle-network/agent-interface`](https://github.com/tangle-network/agent-sdk/tree/main/packages/agent-interface) | `0.46.1` | Canonical profile, capabilities, environment, stream, portable context, and interaction contracts | -| [`@tangle-network/agent-runtime`](https://github.com/tangle-network/agent-runtime) | `0.131.6` | Sole execution layer; exact executor, environment-provider, and terminal-monitor exports | -| [`@tangle-network/agent-eval`](https://github.com/tangle-network/agent-eval) | `0.144.10` | Run records, judges, trace analysts, comparisons, and feedback trajectories | -| `@tangle-network/agent-provider-cli-bridge` | `0.4.3` | CLI Bridge environment adapter with live streaming, replay, retry-safe turns, and explicit cancel | -| `@tangle-network/agent-provider-tangle` | `0.6.1` | Tangle environment adapter over the sandbox client | +| [`@tangle-network/agent-runtime`](https://github.com/tangle-network/agent-runtime) | `0.131.7` | Sole execution layer; exact executor, environment-provider, and terminal-monitor exports | +| [`@tangle-network/agent-eval`](https://github.com/tangle-network/agent-eval) | `0.144.11` | Run records, judges, trace analysts, comparisons, and feedback trajectories | +| `@tangle-network/agent-provider-cli-bridge` | `0.5.0` | CLI Bridge environment adapter with live streaming, replay, retry-safe turns, and explicit cancel | +| `@tangle-network/agent-provider-tangle` | `0.6.2` | Tangle environment adapter over the sandbox client | | `@tangle-network/sandbox` | `0.19.4` | Tangle cloud client used by the provider | -The installed runtime publishes `agent-eval >=0.144.8 <0.145.0`, `agent-interface >=0.46.1 <0.47.0`, and optional `sandbox >=0.19.4 <0.20.0` as peer ranges. +The installed runtime publishes `agent-eval >=0.144.10 <0.145.0`, `agent-interface >=0.46.1 <0.47.0`, and optional `sandbox >=0.19.4 <0.20.0` as peer ranges. -Braid exercises runtime `0.131.6` with interface `0.46.1`, eval `0.144.10`, CLI Bridge adapter `0.4.3`, Tangle adapter `0.6.1`, and sandbox `0.19.4`. +Braid exercises runtime `0.131.7` with interface `0.46.1`, eval `0.144.11`, CLI Bridge adapter `0.5.0`, Tangle adapter `0.6.2`, and sandbox `0.19.4`. The lockfile pins the registry integrity for every installed package. @@ -31,7 +31,7 @@ The published peer ranges accept the complete installed package set without a wo The workspace override resolves every transitive `agent-interface` dependency to `0.46.1`; the package tests prove that this version retains every public symbol Braid uses from the older dependency graph. -[Agent-runtime issue 746](https://github.com/tangle-network/agent-runtime/issues/746) is closed by the peer ranges retained in `0.131.6`. +[Agent-runtime issue 746](https://github.com/tangle-network/agent-runtime/issues/746) is closed by the peer ranges retained in `0.131.7`. Braid imports only the canonical root `agent-interface` entry point behind two local modules. @@ -132,11 +132,15 @@ A metadata-only profile change invalidates admission reuse and provider-session After restart, Braid keeps the reloaded exact profile only when its exact digest matches durable selection state. -Braid preserves Runtime's aggregate `{ input, output, tokensKnown?: false }` token record. +Braid preserves Runtime's aggregate token record, including its optional prompt-cache counters and explicit unknown markers. -Braid rejects all other fields under token-bearing record names. +A known prompt-cache split must contain all three counters and sum to total input tokens. -Runtime `0.131.6` currently buffers executor output until the executor settles. +A partial or inconsistent positive-input split must set `cacheBreakdownKnown: false`. + +Braid rejects unknown fields and inconsistent totals under token-bearing record names. + +Runtime `0.131.7` currently buffers executor output until the executor settles. Braid therefore receives terminal text, tool calls, usage, and result evidence but cannot render provider text deltas live through this path. @@ -162,7 +166,7 @@ The capability is a typed Runtime-executor tag, not a provider-specific callback When the tag is present, `/cancel` calls the public Runtime `Executor.teardown('infinity')` operation and waits for its result before committing control state. -The installed Runtime `0.131.6` bridge executor implements that operation by posting `POST /v1/runs/:id/cancel` and waiting for a terminal bridge snapshot. +The installed Runtime `0.131.7` bridge executor implements that operation by posting `POST /v1/runs/:id/cancel` and waiting for a terminal bridge snapshot. `destroyed: true` becomes an accepted cancellation, while `destroyed: false`, a thrown error, or a control deadline becomes unknown. @@ -184,7 +188,7 @@ The upstream Runtime change required to expand this support is recorded below wi Title: `Expose typed, signal-aware provider cancellation acknowledgement from Executor` -Runtime `0.131.6` exposes `Executor.teardown(grace): Promise<{ destroyed: boolean }>` in its published declaration bundle. +Runtime `0.131.7` exposes `Executor.teardown(grace): Promise<{ destroyed: boolean }>` in its published declaration bundle. The bridge executor's `teardown('infinity')` posts `POST /v1/runs/:id/cancel` and waits for a terminal bridge snapshot in `dist/supervisor-BI6Z-8Yi.js:6973-6977,7529-7570`. @@ -307,7 +311,7 @@ These behaviors may be suitable for isolated benchmark automation under an expli ## Existing CLI Bridge provider contract -The published `@tangle-network/agent-provider-cli-bridge@0.4.3` resolves the bridge model from a turn override, provider default, or profile harness and model. +The published `@tangle-network/agent-provider-cli-bridge@0.5.0` resolves the bridge model from a turn override, provider default, or profile harness and model. It sends stable `executionId` values as bridge run identifiers when they satisfy the bridge identifier rules. @@ -327,7 +331,7 @@ It exposes no generalized interaction response. ## Existing Tangle provider contract -The published `@tangle-network/agent-provider-tangle@0.6.1` wraps `@tangle-network/sandbox` as an `AgentEnvironmentProvider`. +The published `@tangle-network/agent-provider-tangle@0.6.2` wraps `@tangle-network/sandbox` as an `AgentEnvironmentProvider`. Its default document is an upper bound, not a claim about one client or deployment. @@ -398,7 +402,7 @@ Braid binds that function to the selected profile, connection, effective model, `agent-runtime` executes each canonical text-message request through `streamAgentTurn`; Braid returns normalized output, measured token usage, priced cost, terminal status, and finite redacted execution evidence to `agent-eval`. -The callback rejects multimodal and request-level thinking controls because runtime `0.131.6` does not expose those fields on this exact turn input. +The callback rejects multimodal and request-level thinking controls because runtime `0.131.7` does not expose those fields on this exact turn input. Reasoning remains an `AgentProfile` setting, and unsupported callback shapes fail before a provider call rather than being silently dropped. diff --git a/docs/10-upstream-strategy.md b/docs/10-upstream-strategy.md index 2017277..9bab5c6 100644 --- a/docs/10-upstream-strategy.md +++ b/docs/10-upstream-strategy.md @@ -37,7 +37,7 @@ The deciding question was how much source can be reused while preserving `AgentP | [OpenCode terminal packages](https://github.com/anomalyco/opencode/tree/dev/packages/tui) | `3a90639cb57619a21e59f544b3e8d23ffed56f48`; npm `1.18.16` | MIT | 50,341 lines across the TUI and run command | OpenTUI alternative and workflow reference | | [Codex terminal application](https://github.com/openai/codex/tree/main/codex-rs/tui) | repository `2cc9dbb`; TUI `9742cc8ed5def37a4575263733f70a01ca22047b`; CLI `0.147.0` | Apache-2.0 | 244,714 Rust lines | Composer, worker navigation, status, and snapshot-test reference | | [Hermes Agent terminal app](https://github.com/NousResearch/hermes-agent/tree/main/ui-tui) | `697f2896bf948731eb6fcb93caa7264478590843` | MIT | No size claim used | Client/runtime and workflow reference | -| [`agent-runtime` terminal monitor](https://github.com/tangle-network/agent-runtime/tree/main/src/tui) | `9c18cb48`; npm `0.131.6` | Project license | No refreshed size claim | Runtime-owned supervisor source, not app base | +| [`agent-runtime` terminal monitor](https://github.com/tangle-network/agent-runtime/tree/main/src/tui) | `9c18cb48`; npm `0.131.7` | Project license | No refreshed size claim | Runtime-owned supervisor source, not app base | The count commands selected the named TypeScript or Rust files from sparse clones and used `wc -l`. @@ -165,7 +165,7 @@ Braid uses Hermes as a workflow comparison and does not port its runtime protoco ## Runtime monitor -`agent-runtime` package version `0.131.6` exports a diagnostic terminal module and `agent-runtime-top` binary. +`agent-runtime` package version `0.131.7` exports a diagnostic terminal module and `agent-runtime-top` binary. The module understands runtime-owned supervisor files and shows worker state, spend, tokens, latency, logs, steering, shell action, and cancellation controls. diff --git a/package.json b/package.json index 72c670a..3f7cfac 100644 --- a/package.json +++ b/package.json @@ -93,11 +93,11 @@ "dependencies": { "@earendil-works/pi-tui": "0.84.1", "@napi-rs/keyring": "1.3.0", - "@tangle-network/agent-eval": "0.144.10", + "@tangle-network/agent-eval": "0.144.11", "@tangle-network/agent-interface": "0.46.1", - "@tangle-network/agent-provider-cli-bridge": "0.4.3", - "@tangle-network/agent-provider-tangle": "0.6.1", - "@tangle-network/agent-runtime": "0.131.6", + "@tangle-network/agent-provider-cli-bridge": "0.5.0", + "@tangle-network/agent-provider-tangle": "0.6.2", + "@tangle-network/agent-runtime": "0.131.7", "@tangle-network/sandbox": "0.19.4", "better-sqlite3-multiple-ciphers": "13.0.3", "chalk": "6.0.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0ecd2f1..4774d0c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,20 +20,20 @@ importers: specifier: 1.3.0 version: 1.3.0 '@tangle-network/agent-eval': - specifier: 0.144.10 - version: 0.144.10 + specifier: 0.144.11 + version: 0.144.11 '@tangle-network/agent-interface': specifier: 0.46.1 version: 0.46.1 '@tangle-network/agent-provider-cli-bridge': - specifier: 0.4.3 - version: 0.4.3 + specifier: 0.5.0 + version: 0.5.0 '@tangle-network/agent-provider-tangle': - specifier: 0.6.1 - version: 0.6.1(@tangle-network/sandbox@0.19.4) + specifier: 0.6.2 + version: 0.6.2(@tangle-network/sandbox@0.19.4) '@tangle-network/agent-runtime': - specifier: 0.131.6 - version: 0.131.6(@tangle-network/agent-eval@0.144.10)(@tangle-network/agent-interface@0.46.1)(@tangle-network/sandbox@0.19.4) + specifier: 0.131.7 + version: 0.131.7(@tangle-network/agent-eval@0.144.11)(@tangle-network/agent-interface@0.46.1)(@tangle-network/sandbox@0.19.4) '@tangle-network/sandbox': specifier: 0.19.4 version: 0.19.4 @@ -381,43 +381,41 @@ packages: '@tangle-network/agent-core@0.5.4': resolution: {integrity: sha512-k6gYv3BlagkfuWrGyTJH6mKUBgsLY6TXxizACqt0QF8a1/5uqy0UYc6R2Wo9nqQVJuaRxDnoiRf8YtsRVqA75g==} - '@tangle-network/agent-eval@0.144.10': - resolution: {integrity: sha512-mDUf902qRCDnEzQ7cyN9ytohyPtho/+Aumw8kMTjMcJo7XoUKeH4bEqpsTS0q4aLMQYgg5R+xH8FRCnN1wt8Jg==} - engines: {node: '>=20'} - hasBin: true - - '@tangle-network/agent-eval@0.144.8': - resolution: {integrity: sha512-5vB1bbLZ/KS1zUsUROFHIC+dqDqNzqYlzuYODI8MAvoUB/Jimr+6n7Sfn6Yi/08CElDanTy+FlQeF4vekU7Rew==} + '@tangle-network/agent-eval@0.144.11': + resolution: {integrity: sha512-rTZn6aRZzr8hZGGxG+iZDjifrQGO9Ghd4GvXdLHgPcwUJ3QHkRdFgp6ImTpjAW6ZVxrJlFv9wdN4OgDtbn5pCw==} engines: {node: '>=20'} hasBin: true '@tangle-network/agent-interface@0.46.1': resolution: {integrity: sha512-6a3GRkDxS+r6Bmlu8y6LQpiVE20oCYPzE2opb5o+AZeDzRW5KemyxreyYIprjgKfDrvBTMo0tgvC2JB51Sl84w==} - '@tangle-network/agent-knowledge@7.1.3': - resolution: {integrity: sha512-EldwPsx4Bn1xQGHVwZBDtCqBrPvSFx6KE7PGzUtYbhiFdSoouVFvESZy/IJ4N0EvfFgE4IrVqPI8jAaQbSZRow==} + '@tangle-network/agent-knowledge@7.2.0': + resolution: {integrity: sha512-DPpicRkEcRSd+5RBMmFg7uaoSQx4RpoTvrLo2Zm7m1NtDbQRKXgws/iaSPTWaOewUD5lI7OuJiXaBoLctGKn9g==} engines: {node: '>=20.19.0'} hasBin: true + peerDependencies: + '@tangle-network/agent-eval': '>=0.144.10 <0.145.0' + '@tangle-network/agent-interface': 0.46.1 '@tangle-network/agent-profile-materialize@0.13.1': resolution: {integrity: sha512-2g/F8ABiJ6gB8lifTKKIMGPBgYjCyPHQF93HF3g7c5d0D5Rh8cEZ9WGRvggYjOm4HPi3/4PViG6VKEmr5sXwIg==} peerDependencies: '@tangle-network/agent-interface': 0.46.1 - '@tangle-network/agent-provider-cli-bridge@0.4.3': - resolution: {integrity: sha512-drFPKQH2zTo2xQyHfDV1yUYMGmXO0F9UZHj4KBiVVMbk75AjI2ChUmHdw9+RKBMMcarbuEqxz7dBIJJmA8eizQ==} + '@tangle-network/agent-provider-cli-bridge@0.5.0': + resolution: {integrity: sha512-4SPa19e9J3nf42+fmm+GxXlLCU5lYk7oTtD4S1fg1HZuE6a2iEWgbcs3SYWNOidZGpzcvYNmK9KTICSYSt7GUQ==} - '@tangle-network/agent-provider-tangle@0.6.1': - resolution: {integrity: sha512-sYHACiWH7x5T+U/xYFTG5BONRGbe90jVAr5o6+Yg0vmyhZqSk57dDgzXtGnvRF/uhyJr0gis8anlp8w0GdY7Aw==} + '@tangle-network/agent-provider-tangle@0.6.2': + resolution: {integrity: sha512-pzhrV3aczuC20Gl+kKn0D6tJkMFWKLVdzxTHBxY20gEi0DkH2kRCRCAzpSY6QzxCdekzalQiNvgmRoQPDy6HVQ==} peerDependencies: '@tangle-network/sandbox': '>=0.17.0 <1.0.0' - '@tangle-network/agent-runtime@0.131.6': - resolution: {integrity: sha512-IhGS5z1aE6ez8ceJML6ot0gQIoR5rZVT0R3NP8WAp6v/J857CLAQ4Vrt30UqK9wL1+nsnW2UzeWRyNJrhuBVNA==} + '@tangle-network/agent-runtime@0.131.7': + resolution: {integrity: sha512-2Wg2jYh5UmzW9r5ZDkjVTEHvmrkD77qAJKhuwFSUUbBI3S3mJDa0c/dPU8GdnsOfdUFpyKXocVp+mNS+Sts1lA==} engines: {node: '>=22.13.0'} hasBin: true peerDependencies: - '@tangle-network/agent-eval': '>=0.144.8 <0.145.0' + '@tangle-network/agent-eval': '>=0.144.10 <0.145.0' '@tangle-network/agent-interface': 0.46.1 '@tangle-network/sandbox': '>=0.19.4 <0.20.0' peerDependenciesMeta: @@ -941,19 +939,7 @@ snapshots: '@tangle-network/agent-interface': 0.46.1 zod: 4.4.3 - '@tangle-network/agent-eval@0.144.10': - dependencies: - '@asteasolutions/zod-to-openapi': 9.1.0(zod@4.4.3) - '@hono/node-server': 2.1.0(hono@4.13.0) - '@tangle-network/agent-core': 0.5.4 - '@tangle-network/agent-interface': 0.46.1 - '@tangle-network/agent-trace-contract': 1.0.2 - hono: 4.13.0 - linear-sum-assignment: 1.0.9 - re2js: 2.8.6 - zod: 4.4.3 - - '@tangle-network/agent-eval@0.144.8': + '@tangle-network/agent-eval@0.144.11': dependencies: '@asteasolutions/zod-to-openapi': 9.1.0(zod@4.4.3) '@hono/node-server': 2.1.0(hono@4.13.0) @@ -971,9 +957,9 @@ snapshots: spdx-expression-parse: 5.0.0 zod: 4.4.3 - '@tangle-network/agent-knowledge@7.1.3': + '@tangle-network/agent-knowledge@7.2.0(@tangle-network/agent-eval@0.144.11)(@tangle-network/agent-interface@0.46.1)': dependencies: - '@tangle-network/agent-eval': 0.144.8 + '@tangle-network/agent-eval': 0.144.11 '@tangle-network/agent-interface': 0.46.1 proper-lockfile: 4.1.2 zod: 4.4.3 @@ -982,22 +968,22 @@ snapshots: dependencies: '@tangle-network/agent-interface': 0.46.1 - '@tangle-network/agent-provider-cli-bridge@0.4.3': + '@tangle-network/agent-provider-cli-bridge@0.5.0': dependencies: '@tangle-network/agent-interface': 0.46.1 undici: 7.29.0 - '@tangle-network/agent-provider-tangle@0.6.1(@tangle-network/sandbox@0.19.4)': + '@tangle-network/agent-provider-tangle@0.6.2(@tangle-network/sandbox@0.19.4)': dependencies: '@tangle-network/agent-interface': 0.46.1 '@tangle-network/sandbox': 0.19.4 - '@tangle-network/agent-runtime@0.131.6(@tangle-network/agent-eval@0.144.10)(@tangle-network/agent-interface@0.46.1)(@tangle-network/sandbox@0.19.4)': + '@tangle-network/agent-runtime@0.131.7(@tangle-network/agent-eval@0.144.11)(@tangle-network/agent-interface@0.46.1)(@tangle-network/sandbox@0.19.4)': dependencies: '@tangle-network/agent-core': 0.5.4 - '@tangle-network/agent-eval': 0.144.10 + '@tangle-network/agent-eval': 0.144.11 '@tangle-network/agent-interface': 0.46.1 - '@tangle-network/agent-knowledge': 7.1.3 + '@tangle-network/agent-knowledge': 7.2.0(@tangle-network/agent-eval@0.144.11)(@tangle-network/agent-interface@0.46.1) '@tangle-network/agent-profile-materialize': 0.13.1(@tangle-network/agent-interface@0.46.1) '@tangle-network/agent-trace-contract': 1.0.2 tar-stream: 3.2.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index d2e477d..b61dacb 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -8,13 +8,13 @@ allowBuilds: node-pty: true ignoreScripts: false minimumReleaseAgeExclude: - - '@tangle-network/agent-runtime@0.126.0 || 0.128.0 || 0.131.0 || 0.131.1 || 0.131.2 || 0.131.3 || 0.131.4 || 0.131.5 || 0.131.6' - - '@tangle-network/agent-eval@0.143.0 || 0.144.0 || 0.144.1 || 0.144.3 || 0.144.4 || 0.144.6 || 0.144.8 || 0.144.10' + - '@tangle-network/agent-runtime@0.126.0 || 0.128.0 || 0.131.0 || 0.131.1 || 0.131.2 || 0.131.3 || 0.131.4 || 0.131.5 || 0.131.6 || 0.131.7' + - '@tangle-network/agent-eval@0.143.0 || 0.144.0 || 0.144.1 || 0.144.3 || 0.144.4 || 0.144.6 || 0.144.8 || 0.144.10 || 0.144.11' - '@tangle-network/agent-interface@0.46.0 || 0.46.1' - - '@tangle-network/agent-knowledge@7.0.8 || 7.1.2 || 7.1.3' + - '@tangle-network/agent-knowledge@7.0.8 || 7.1.2 || 7.1.3 || 7.2.0' - '@tangle-network/agent-profile-materialize@0.10.2 || 0.13.1' - - '@tangle-network/agent-provider-cli-bridge@0.4.2 || 0.4.3' - - '@tangle-network/agent-provider-tangle@0.6.0 || 0.6.1' + - '@tangle-network/agent-provider-cli-bridge@0.4.2 || 0.4.3 || 0.5.0' + - '@tangle-network/agent-provider-tangle@0.6.0 || 0.6.1 || 0.6.2' - '@tangle-network/sandbox@0.17.3 || 0.18.0 || 0.19.2 || 0.19.3 || 0.19.4' - hono@4.13.0 - '@esbuild/aix-ppc64@0.28.2' diff --git a/src/adapters/analysis/agent-eval-version.ts b/src/adapters/analysis/agent-eval-version.ts index 920c9da..792a4f3 100644 --- a/src/adapters/analysis/agent-eval-version.ts +++ b/src/adapters/analysis/agent-eval-version.ts @@ -1 +1 @@ -export const AGENT_EVAL_VERSION = '0.144.10' +export const AGENT_EVAL_VERSION = '0.144.11' diff --git a/src/adapters/runtime/agent-runtime-version.ts b/src/adapters/runtime/agent-runtime-version.ts index 9a7c61f..95dc2a1 100644 --- a/src/adapters/runtime/agent-runtime-version.ts +++ b/src/adapters/runtime/agent-runtime-version.ts @@ -1 +1 @@ -export const AGENT_RUNTIME_VERSION = '0.131.6' +export const AGENT_RUNTIME_VERSION = '0.131.7' diff --git a/src/domain/bounded-structured.ts b/src/domain/bounded-structured.ts index 3e879af..058a370 100644 --- a/src/domain/bounded-structured.ts +++ b/src/domain/bounded-structured.ts @@ -46,17 +46,21 @@ export function isSafeBooleanTelemetryField(key: string, value: unknown): value } /** Accept Runtime's exact aggregate token counter without accepting arbitrary token-shaped data. */ -export function isSafeTokenUsageRecord( - key: string, - value: unknown, -): value is Readonly<{ input: number; output: number; tokensKnown?: false }> { +export function isSafeTokenUsageRecord(key: string, value: unknown): value is SafeTokenUsageRecord { return safeTokenUsageRecord(key, value) !== undefined } -function safeTokenUsageRecord( - key: string, - value: unknown, -): Readonly<{ input: number; output: number; tokensKnown?: false }> | undefined { +interface SafeTokenUsageRecord { + readonly input: number + readonly output: number + readonly tokensKnown?: false + readonly freshInput?: number + readonly cacheRead?: number + readonly cacheWrite?: number + readonly cacheBreakdownKnown?: false +} + +function safeTokenUsageRecord(key: string, value: unknown): SafeTokenUsageRecord | undefined { const normalized = key.replace(/[^a-z0-9]/giu, '').toLowerCase() if ((normalized !== 'tokens' && normalized !== 'tokenusage') || !isRecord(value)) { return undefined @@ -68,25 +72,63 @@ function safeTokenUsageRecord( let input: number | undefined let output: number | undefined let tokensKnown: false | undefined + let freshInput: number | undefined + let cacheRead: number | undefined + let cacheWrite: number | undefined + let cacheBreakdownKnown: false | undefined for (const field in value) { if (!Object.prototype.propertyIsEnumerable.call(value, field)) continue - if (field !== 'input' && field !== 'output' && field !== 'tokensKnown') return undefined + if ( + field !== 'input' && + field !== 'output' && + field !== 'tokensKnown' && + field !== 'freshInput' && + field !== 'cacheRead' && + field !== 'cacheWrite' && + field !== 'cacheBreakdownKnown' + ) + return undefined fields += 1 - if (fields > 3) return undefined + if (fields > 7) return undefined const fieldValue = value[field] if (field === 'tokensKnown') { if (fieldValue !== false) return undefined tokensKnown = false continue } + if (field === 'cacheBreakdownKnown') { + if (fieldValue !== false) return undefined + cacheBreakdownKnown = false + continue + } const count = fieldValue if (typeof count !== 'number' || !Number.isFinite(count) || count < 0) return undefined if (field === 'input') input = count - else output = count + else if (field === 'output') output = count + else if (field === 'freshInput') freshInput = count + else if (field === 'cacheRead') cacheRead = count + else cacheWrite = count } if (input === undefined || output === undefined) return undefined - if (fields !== (tokensKnown === false ? 3 : 2)) return undefined - return { input, output, ...(tokensKnown === false ? { tokensKnown } : {}) } + const cacheValues = [freshInput, cacheRead, cacheWrite] + const cacheFieldCount = cacheValues.filter((entry) => entry !== undefined).length + if (input > 0 && cacheFieldCount > 0 && cacheFieldCount < 3 && cacheBreakdownKnown !== false) + return undefined + if ( + cacheBreakdownKnown !== false && + cacheFieldCount === 3 && + (freshInput ?? 0) + (cacheRead ?? 0) + (cacheWrite ?? 0) !== input + ) + return undefined + return { + input, + output, + ...(tokensKnown === false ? { tokensKnown } : {}), + ...(freshInput === undefined ? {} : { freshInput }), + ...(cacheRead === undefined ? {} : { cacheRead }), + ...(cacheWrite === undefined ? {} : { cacheWrite }), + ...(cacheBreakdownKnown === false ? { cacheBreakdownKnown } : {}), + } } catch { return undefined } diff --git a/test/sanitize.test.ts b/test/sanitize.test.ts index cd0eeec..9da198b 100644 --- a/test/sanitize.test.ts +++ b/test/sanitize.test.ts @@ -83,9 +83,56 @@ test('plain structured redaction preserves only the exact aggregate token counte }) assert.deepEqual( redactStructuredValue({ - spend: { tokens: { input: 12, output: 7, tokensKnown: false } }, + spend: { + tokens: { + input: 12, + output: 7, + tokensKnown: false, + cacheBreakdownKnown: false, + }, + }, + }), + { + spend: { + tokens: { + input: 12, + output: 7, + tokensKnown: false, + cacheBreakdownKnown: false, + }, + }, + }, + ) + assert.deepEqual( + redactStructuredValue({ + tokens: { input: 12, output: 7, freshInput: 5, cacheRead: 4, cacheWrite: 3 }, }), - { spend: { tokens: { input: 12, output: 7, tokensKnown: false } } }, + { tokens: { input: 12, output: 7, freshInput: 5, cacheRead: 4, cacheWrite: 3 } }, + ) + assert.deepEqual(redactStructuredValue({ tokens: { input: 0, output: 1, cacheRead: 0 } }), { + tokens: { input: 0, output: 1, cacheRead: 0 }, + }) + assert.deepEqual( + redactStructuredValue({ + tokens: { + input: 12, + output: 7, + freshInput: 6, + cacheRead: 4, + cacheWrite: 3, + cacheBreakdownKnown: false, + }, + }), + { + tokens: { + input: 12, + output: 7, + freshInput: 6, + cacheRead: 4, + cacheWrite: 3, + cacheBreakdownKnown: false, + }, + }, ) assert.deepEqual(redactStructuredValue({ tokens: { input: -1, output: 7 } }), { tokens: '[redacted]', @@ -93,6 +140,19 @@ test('plain structured redaction preserves only the exact aggregate token counte assert.deepEqual(redactStructuredValue({ tokens: { input: 12, output: 7, tokensKnown: true } }), { tokens: '[redacted]', }) + assert.deepEqual( + redactStructuredValue({ tokens: { input: 12, output: 7, cacheBreakdownKnown: true } }), + { tokens: '[redacted]' }, + ) + assert.deepEqual(redactStructuredValue({ tokens: { input: 12, output: 7, freshInput: 11 } }), { + tokens: '[redacted]', + }) + assert.deepEqual( + redactStructuredValue({ + tokens: { input: 12, output: 7, freshInput: 6, cacheRead: 4, cacheWrite: 3 }, + }), + { tokens: '[redacted]' }, + ) assert.deepEqual( redactStructuredValue({ tokens: { input: 12, output: 7, credential: 'canary' } }), { tokens: '[redacted]' }, diff --git a/test/security.test.ts b/test/security.test.ts index b5b294b..7479b9e 100644 --- a/test/security.test.ts +++ b/test/security.test.ts @@ -816,7 +816,38 @@ test('secret-designated interaction values are rejected before journal persisten ) assert.doesNotThrow(() => assertPersistablePayload({ - spend: { tokens: { input: 12, output: 7, tokensKnown: false } }, + spend: { + tokens: { + input: 12, + output: 7, + tokensKnown: false, + cacheBreakdownKnown: false, + }, + }, + }), + ) + assert.doesNotThrow(() => + assertPersistablePayload({ + spend: { + tokens: { input: 12, output: 7, freshInput: 5, cacheRead: 4, cacheWrite: 3 }, + }, + }), + ) + assert.doesNotThrow(() => + assertPersistablePayload({ + tokens: { input: 0, output: 1, cacheRead: 0 }, + }), + ) + assert.doesNotThrow(() => + assertPersistablePayload({ + tokens: { + input: 12, + output: 7, + freshInput: 6, + cacheRead: 4, + cacheWrite: 3, + cacheBreakdownKnown: false, + }, }), ) assert.throws( @@ -826,6 +857,26 @@ test('secret-designated interaction values are rejected before journal persisten error.name === 'StorageError' && error.message.includes('Secret-bearing'), ) + assert.throws( + () => + assertPersistablePayload({ + tokens: { input: 12, output: 7, cacheBreakdownKnown: true }, + }), + (error: unknown) => + error instanceof Error && + error.name === 'StorageError' && + error.message.includes('Secret-bearing'), + ) + assert.throws( + () => + assertPersistablePayload({ + tokens: { input: 12, output: 7, freshInput: 6, cacheRead: 4, cacheWrite: 3 }, + }), + (error: unknown) => + error instanceof Error && + error.name === 'StorageError' && + error.message.includes('Secret-bearing'), + ) assert.throws( () => assertPersistablePayload({ tokens: { input: 12, output: 7, credential: 'canary' } }), (error: unknown) => @@ -876,7 +927,12 @@ test('runtime spend artifacts remain redacted and persistable when token totals metadata: { spend: { iterations: 1, - tokens: { input: 12, output: 7, tokensKnown: false }, + tokens: { + input: 12, + output: 7, + tokensKnown: false, + cacheBreakdownKnown: false, + }, tokensKnown: false, usdKnown: false, usd: 0, @@ -895,7 +951,12 @@ test('runtime spend artifacts remain redacted and persistable when token totals assert.deepEqual(event.metadata, { spend: { iterations: 1, - tokens: { input: 12, output: 7, tokensKnown: false }, + tokens: { + input: 12, + output: 7, + tokensKnown: false, + cacheBreakdownKnown: false, + }, tokensKnown: false, usdKnown: false, usd: 0,