@basein/runner 0.3.25 → 0.3.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +27 -26
  2. package/dist/auth/client.d.ts +4 -1
  3. package/dist/auth/client.js +83 -14
  4. package/dist/bin/bir-hooks.js +4 -2
  5. package/dist/bin/bir.js +153 -37
  6. package/dist/bin/investigate.d.ts +13 -0
  7. package/dist/bin/investigate.js +72 -2
  8. package/dist/bin/scenario-edit.d.ts +5 -0
  9. package/dist/bin/scenario-edit.js +17 -1
  10. package/dist/config/adapters/claude-code.d.ts +51 -7
  11. package/dist/config/adapters/claude-code.js +105 -18
  12. package/dist/config/generate.d.ts +61 -13
  13. package/dist/config/generate.js +108 -20
  14. package/dist/config/resolve.d.ts +5 -0
  15. package/dist/config/resolve.js +7 -0
  16. package/dist/control/correlation.js +0 -0
  17. package/dist/control/daemon.d.ts +28 -0
  18. package/dist/control/daemon.js +62 -0
  19. package/dist/control/paths.d.ts +19 -1
  20. package/dist/control/paths.js +20 -2
  21. package/dist/control/pin-guard.js +8 -2
  22. package/dist/control/recorder-main.js +17 -5
  23. package/dist/control/server.d.ts +75 -11
  24. package/dist/control/server.js +440 -77
  25. package/dist/control/transcript.d.ts +64 -3
  26. package/dist/control/transcript.js +318 -37
  27. package/dist/jsonrpc/framing.d.ts +18 -3
  28. package/dist/jsonrpc/framing.js +57 -4
  29. package/dist/proxy/intercept.d.ts +4 -0
  30. package/dist/proxy/intercept.js +4 -0
  31. package/dist/proxy/relay.d.ts +13 -0
  32. package/dist/proxy/relay.js +12 -1
  33. package/dist/proxy/session.d.ts +8 -1
  34. package/dist/proxy/session.js +67 -10
  35. package/dist/record/recorder.d.ts +6 -1
  36. package/dist/record/redact.d.ts +18 -4
  37. package/dist/record/redact.js +538 -79
  38. package/dist/record/remote-recorder.d.ts +26 -5
  39. package/dist/record/remote-recorder.js +174 -18
  40. package/dist/record/truncate.js +3 -1
  41. package/dist/replay/bash-channel.d.ts +7 -1
  42. package/dist/replay/bash-channel.js +9 -3
  43. package/dist/replay/builtins.d.ts +14 -0
  44. package/dist/replay/builtins.js +460 -95
  45. package/dist/replay/bundle.d.ts +15 -11
  46. package/dist/replay/bundle.js +167 -33
  47. package/dist/replay/controller.d.ts +92 -1
  48. package/dist/replay/controller.js +246 -47
  49. package/dist/replay/derive.js +46 -4
  50. package/dist/replay/handover.d.ts +13 -0
  51. package/dist/replay/handover.js +18 -0
  52. package/dist/replay/plan.d.ts +41 -0
  53. package/dist/replay/plan.js +171 -3
  54. package/dist/replay/pricing.d.ts +61 -11
  55. package/dist/replay/pricing.js +135 -42
  56. package/dist/replay/reads-only.d.ts +48 -0
  57. package/dist/replay/reads-only.js +161 -0
  58. package/dist/replay/safety.js +43 -5
  59. package/dist/upstream/http-client.d.ts +6 -3
  60. package/dist/upstream/http-client.js +54 -13
  61. package/dist/upstream/stdio-client.d.ts +7 -0
  62. package/dist/upstream/stdio-client.js +1 -1
  63. package/dist/util/atomic-write.d.ts +9 -1
  64. package/dist/util/atomic-write.js +9 -6
  65. package/dist/util/service-health.d.ts +7 -0
  66. package/dist/util/service-health.js +10 -3
  67. package/package.json +1 -1
  68. package/production.md +2 -2
  69. package/scripts/install.ps1 +14 -2
@@ -132,6 +132,47 @@ function clipReasoning(reasoning) {
132
132
  const first = (reasoning ?? "").trim().split(/\r?\n/, 1)[0]?.trim() ?? "";
133
133
  return first.length > MAX_REASONING_CHARS ? `${first.slice(0, MAX_REASONING_CHARS)}…` : first;
134
134
  }
135
+ // ── the recording's values in a directive's text (B2 is11.md) ────────────────
136
+ /**
137
+ * A recorded value as a text may name it: a string or a number at least this
138
+ * long — a one-character sample ("5", "a") is found in too many words — and
139
+ * no longer than a name or a title is.
140
+ */
141
+ const RECORDED_VALUE_MIN = 2;
142
+ const RECORDED_VALUE_MAX = 200;
143
+ /** One of this request's values, as a directive shows it: this long at most. */
144
+ const SHOWN_VALUE_MAX = 60;
145
+ /** This many of this request's values at most on a direct directive's values line… */
146
+ const SHOWN_VALUES = 4;
147
+ /** …and this long at most, all of them. */
148
+ const SHOWN_VALUES_CHARS = 200;
149
+ /** The recorded value `value` as a text may name it, or undefined. */
150
+ function recordedText(value) {
151
+ const text = typeof value === "number" && Number.isFinite(value)
152
+ ? String(value)
153
+ : typeof value === "string"
154
+ ? value.trim()
155
+ : undefined;
156
+ return text !== undefined && text.length >= RECORDED_VALUE_MIN && text.length <= RECORDED_VALUE_MAX ? text : undefined;
157
+ }
158
+ /** A value of this request's as plain text — never a mark — or undefined. */
159
+ function valueText(value) {
160
+ let text;
161
+ if (typeof value === "boolean")
162
+ text = String(value);
163
+ else if (typeof value === "number" && Number.isFinite(value))
164
+ text = String(value);
165
+ else if (typeof value === "string")
166
+ text = value.replace(/\s+/g, " ").trim();
167
+ if (!text || markedKey(text) !== undefined)
168
+ return undefined;
169
+ return text;
170
+ }
171
+ const clipValue = (text) => text.length > SHOWN_VALUE_MAX ? `${text.slice(0, SHOWN_VALUE_MAX)}…` : text;
172
+ const sameText = (a, b) => a.toLowerCase() === b.toLowerCase();
173
+ const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
174
+ /** A parameter's role, for a value a text must not name: `assignee_name` → `<assignee name>`. */
175
+ const roleOf = (key) => `<${key.replace(/[_-]+/g, " ").trim() || key}>`;
135
176
  /** A step's computed arguments, as a steer directive shows them: this long at most. */
136
177
  const MAX_DIRECTIVE_ARGS_CHARS = 240;
137
178
  /**
@@ -854,6 +895,120 @@ export class ScenarioReplayPlan {
854
895
  }
855
896
  return out;
856
897
  }
898
+ /**
899
+ * This request's value of `key`, as a directive may show it, or undefined
900
+ * (B2 is11.md): once the reading has settled, a plain value the reading
901
+ * took from the request — never a mark, and never a value a step works out
902
+ * (`from: "step"`): before the run nobody has that one, and the recorded one
903
+ * is another request's (issue 1212).
904
+ */
905
+ shownValueOf(key, p) {
906
+ if (!this.paramsSettled || p?.from === "step")
907
+ return undefined;
908
+ return valueText(this.derivedParams[key]);
909
+ }
910
+ /**
911
+ * A text written from the recording — the intent, a step's reasoning — put
912
+ * right for this request, for a directive to show (B2 is11.md).
913
+ *
914
+ * Both name the recording's values: "set Olga up", "moves each lead to
915
+ * Olga". Shown beside a request for Carlos, the name tells the model the
916
+ * plan is for someone else. A careful model then checks before it runs
917
+ * anything, and a direct plan's first call that was not `run_scenario`
918
+ * retired it — so the replay was lost on exactly the turn replay is for: the
919
+ * same task with a new value. The steps never read the text: they take
920
+ * Carlos from the request.
921
+ *
922
+ * Each recorded value of a parameter found in the text, as a whole word, is
923
+ * replaced by this request's own value where the plan has one it may show
924
+ * ({@link shownValueOf}), and else by the parameter's role (`<assignee
925
+ * name>`): a target the request did not name, a value a step works out, or
926
+ * any value while the reading has not settled. A value the same as the
927
+ * recorded one stays as it is. One pass, longest value first, so nothing put
928
+ * in is replaced again.
929
+ */
930
+ forThisRequest(text) {
931
+ return this.rewriter()(text);
932
+ }
933
+ /** {@link forThisRequest} as one function, built once for a directive's many texts. */
934
+ rewriter() {
935
+ const schema = this.scenario.paramsObject;
936
+ if (!schema || typeof schema !== "object")
937
+ return (text) => text;
938
+ /** Recorded value (lower case) → what goes in its place; null where it stays. */
939
+ const swaps = new Map();
940
+ for (const [key, p] of Object.entries(schema)) {
941
+ const recorded = recordedText(p?.sampleValue);
942
+ if (!recorded)
943
+ continue;
944
+ const now = this.shownValueOf(key, p);
945
+ const swap = now !== undefined && sameText(now, recorded) ? null : now !== undefined ? clipValue(now) : roleOf(key);
946
+ const k = recorded.toLowerCase();
947
+ // Two parameters recorded with one value: a change wins over "stays".
948
+ if (!swaps.has(k) || swaps.get(k) === null)
949
+ swaps.set(k, swap);
950
+ }
951
+ const found = [...swaps.entries()].filter(([, swap]) => swap !== null).map(([k]) => k);
952
+ if (found.length === 0)
953
+ return (text) => text;
954
+ found.sort((a, b) => b.length - a.length);
955
+ let pattern;
956
+ try {
957
+ pattern = new RegExp(`(?<![\\p{L}\\p{N}_])(?:${found.map(escapeRegExp).join("|")})(?![\\p{L}\\p{N}_])`, "giu");
958
+ }
959
+ catch {
960
+ return (text) => text;
961
+ }
962
+ return (text) => (text ? text.replace(pattern, (m) => swaps.get(m.toLowerCase()) ?? m) : text);
963
+ }
964
+ /**
965
+ * This request's own values, for the direct directive's line that says the
966
+ * steps take them (B2 is11.md): what the reading took from the request.
967
+ * The values the request names in words come first — a name, a title: what
968
+ * a reader holds the note against — those unlike the recording's first;
969
+ * then the values worked out from the day ("tomorrow" as a date). Targets
970
+ * only: never a setting, never a value a step works out, never a mark.
971
+ * Undefined until the reading has settled, or when it named none.
972
+ */
973
+ requestValues() {
974
+ if (!this.paramsSettled)
975
+ return undefined;
976
+ const schema = this.scenario.paramsObject;
977
+ if (!schema || typeof schema !== "object")
978
+ return undefined;
979
+ const items = [];
980
+ let order = 0;
981
+ for (const [key, p] of Object.entries(schema)) {
982
+ order += 1;
983
+ if (p?.kind === "setting")
984
+ continue;
985
+ const now = this.shownValueOf(key, p);
986
+ if (now === undefined)
987
+ continue;
988
+ const recorded = recordedText(p?.sampleValue);
989
+ items.push({
990
+ key,
991
+ text: clipValue(now),
992
+ // Named in the request's own words; an older service sends no `from`.
993
+ said: p?.from === undefined || p.from === "prompt",
994
+ differs: recorded !== undefined && !sameText(now, recorded),
995
+ order,
996
+ });
997
+ }
998
+ items.sort((a, b) => Number(b.said) - Number(a.said) || Number(b.differs) - Number(a.differs) || a.order - b.order);
999
+ const parts = [];
1000
+ let chars = 0;
1001
+ for (const item of items) {
1002
+ if (parts.length >= SHOWN_VALUES)
1003
+ break;
1004
+ const part = `${item.key} ${JSON.stringify(item.text)}`;
1005
+ if (parts.length > 0 && chars + part.length > SHOWN_VALUES_CHARS)
1006
+ break;
1007
+ parts.push(part);
1008
+ chars += part.length + 2;
1009
+ }
1010
+ return parts.length > 0 ? parts.join(", ") : undefined;
1011
+ }
857
1012
  /**
858
1013
  * The directive injected via `UserPromptSubmit`'s `additionalContext`.
859
1014
  *
@@ -880,9 +1035,14 @@ export class ScenarioReplayPlan {
880
1035
  // A sub-task plan (armed mid-task) never tells the agent to stop calling
881
1036
  // tools: the steps below are a part of the task it is already doing.
882
1037
  const subTask = opts?.subTask ?? this.subTask;
1038
+ // The intent and each step's reasoning were written from the recording,
1039
+ // so they name its values ("set Olga up"); shown here, they say this
1040
+ // request's — or the value's role — never another request's (B2 is11.md).
1041
+ const fix = this.rewriter();
883
1042
  // The intent is a paragraph the analyser wrote; it is context, not an
884
1043
  // instruction, so it is clipped rather than dumped whole into the prompt.
885
- const intent = this.intent.length > 240 ? `${this.intent.slice(0, 240)}…` : this.intent;
1044
+ const written = fix(this.intent);
1045
+ const intent = written.length > 240 ? `${written.slice(0, 240)}…` : written;
886
1046
  if (this.mode === "direct") {
887
1047
  // The steps are not listed. The model does not call them, and what the
888
1048
  // tool returns names every call and says whether the run was whole or
@@ -890,9 +1050,17 @@ export class ScenarioReplayPlan {
890
1050
  // cost the agent up to 2,000 characters in the first call of every turn
891
1051
  // a plan was armed for (docs/calculatedReplay.md, "How a bundle reads"),
892
1052
  // and the tool's result shows an operator watching the session what ran.
1053
+ //
1054
+ // One line says where the steps' values come from, with this request's
1055
+ // own: a model that met another request's value in the note used to
1056
+ // stop to check, and its check retired the plan (B2 is11.md).
1057
+ const values = this.requestValues();
893
1058
  return [
894
1059
  "[BaseInstRunner calculated replay] A known-good tool sequence was recorded",
895
- `for this request${intent ? ` (intent: ${intent})` : ""}.`,
1060
+ `for a request like this one${intent ? ` (intent: ${intent})` : ""}.`,
1061
+ values
1062
+ ? `Its steps take this request's values, not the recording's: ${values}.`
1063
+ : "Its steps take this request's values, not the recording's.",
896
1064
  "",
897
1065
  `Call ${directToolName} once, with no arguments, before any other tool. It runs the`,
898
1066
  "sequence and returns what each call did.",
@@ -924,7 +1092,7 @@ export class ScenarioReplayPlan {
924
1092
  return ` (only when ${t.text ?? "this request needs it"})`;
925
1093
  };
926
1094
  const listing = (withWhy, argsBefore) => planned.map((s, i) => {
927
- const why = withWhy ? clipReasoning(s.reasoning) : "";
1095
+ const why = withWhy ? clipReasoning(fix(s.reasoning ?? "")) : "";
928
1096
  const head = ` ${i + 1}. ${s.toolName}${onlyWhen(i)}${why ? ` — ${why}` : ""}`;
929
1097
  const shown = i < argsBefore ? args[i] : undefined;
930
1098
  return shown ? `${head}\n ${shown}` : head;
@@ -15,27 +15,77 @@
15
15
  * numbers are comparable. For that row, right now, they are not.
16
16
  *
17
17
  * Bump the version — on **every** side — whenever a row changes.
18
+ *
19
+ * WHAT A PRICE DEPENDS ON BESIDE THE MODEL (Anthropic's pricing page, read
20
+ * 2026-11-11). Until this version a turn was priced from four token counts per
21
+ * model, and three things the bill reads were not looked at:
22
+ *
23
+ * - WHICH CACHE WAS WRITTEN. A 5-minute cache write bills at 1.25x input, a
24
+ * 1-hour one at 2x. Claude Code writes the 1-hour cache: in the transcripts
25
+ * this was checked against, every cache write was a 1-hour one, and each was
26
+ * priced 37.5% too low.
27
+ * - THE REQUEST'S OWN PROMPT LENGTH, on Claude Haiku 5.5: a request whose prompt
28
+ * (input, cache reads and cache writes together) is over 100,000 tokens pays
29
+ * five times the price. Each request is priced on its own.
30
+ * - HOW THE REQUEST RAN: fast mode bills at twice the standard prices, and
31
+ * US-only inference at 1.1x.
32
+ *
33
+ * So a usage now says these of itself ({@link TokenUsage}), and whoever folds
34
+ * requests into totals keeps the requests that are priced differently apart
35
+ * ({@link requestRates}).
18
36
  */
19
37
  /** Identifies {@link MODEL_PRICING}. Must equal the service's constant. */
20
- export declare const PRICING_VERSION = "2026-09-24";
21
- /**
22
- * Tolerate provider prefixes (`anthropic/`, `us.anthropic.`), date snapshots and
23
- * unseen version bumps by falling back to the longest matching family, so a new
24
- * snapshot never silently prices at $0.
25
- */
26
- export declare function resolveModelPricing(model: string): {
38
+ export declare const PRICING_VERSION = "2026-11-11";
39
+ /** One rate card, in $/million tokens. */
40
+ export interface ModelRates {
27
41
  input: number;
28
42
  output: number;
29
43
  cacheRead: number;
44
+ /** A write to the 5-minute cache. */
30
45
  cacheWrite: number;
31
- } | undefined;
32
- export declare function hasModelPricing(model: string): boolean;
33
- export interface TokenUsage {
46
+ /** A write to the 1-hour cache. */
47
+ cacheWrite1h: number;
48
+ }
49
+ /**
50
+ * A model's rate card, or undefined when the table has no row for it. With
51
+ * `longPrompt`, the card of a request over the model's prompt-length
52
+ * threshold, for a model that has one.
53
+ */
54
+ export declare function resolveModelPricing(name: string, opts?: {
55
+ longPrompt?: boolean;
56
+ }): ModelRates | undefined;
57
+ export declare function hasModelPricing(name: string): boolean;
58
+ /** What of a request, beside its model and its token counts, sets its prices. */
59
+ export interface RateFlags {
60
+ /** Its prompt was over the model's long-prompt threshold (Haiku 5.5: 100,000 tokens). */
61
+ longPrompt?: boolean;
62
+ /** It ran in fast mode. */
63
+ fast?: boolean;
64
+ /** It ran on US-only inference. */
65
+ usOnly?: boolean;
66
+ }
67
+ export interface TokenUsage extends RateFlags {
34
68
  inputTokens: number;
35
69
  outputTokens: number;
36
70
  cacheReadTokens?: number;
71
+ /** Every cache write, the 1-hour ones among them. */
37
72
  cacheCreationTokens?: number;
73
+ /** Of {@link cacheCreationTokens}, the tokens written to the 1-hour cache. */
74
+ cacheCreation1hTokens?: number;
38
75
  }
76
+ /**
77
+ * The flags of ONE request, from its own `usage` as the API reports it: its
78
+ * prompt length (input, cache reads and cache writes together), its `speed`
79
+ * and its `inference_geo`. Only what is so is set, so two requests priced
80
+ * alike give equal flags — and {@link rateKey} the same key.
81
+ */
82
+ export declare function requestRates(name: string, request: {
83
+ promptTokens: number;
84
+ speed?: unknown;
85
+ inferenceGeo?: unknown;
86
+ }): RateFlags;
87
+ /** A short text that is equal for two sets of flags exactly when they price alike. "" for none. */
88
+ export declare function rateKey(flags: RateFlags): string;
39
89
  /** Cost in USD. An unpriced model yields 0 — the caller reports `measured: false`. */
40
- export declare function calculateCostUsd(model: string, usage: TokenUsage): number;
90
+ export declare function calculateCostUsd(name: string, usage: TokenUsage): number;
41
91
  //# sourceMappingURL=pricing.d.ts.map
@@ -15,70 +15,163 @@
15
15
  * numbers are comparable. For that row, right now, they are not.
16
16
  *
17
17
  * Bump the version — on **every** side — whenever a row changes.
18
+ *
19
+ * WHAT A PRICE DEPENDS ON BESIDE THE MODEL (Anthropic's pricing page, read
20
+ * 2026-11-11). Until this version a turn was priced from four token counts per
21
+ * model, and three things the bill reads were not looked at:
22
+ *
23
+ * - WHICH CACHE WAS WRITTEN. A 5-minute cache write bills at 1.25x input, a
24
+ * 1-hour one at 2x. Claude Code writes the 1-hour cache: in the transcripts
25
+ * this was checked against, every cache write was a 1-hour one, and each was
26
+ * priced 37.5% too low.
27
+ * - THE REQUEST'S OWN PROMPT LENGTH, on Claude Haiku 5.5: a request whose prompt
28
+ * (input, cache reads and cache writes together) is over 100,000 tokens pays
29
+ * five times the price. Each request is priced on its own.
30
+ * - HOW THE REQUEST RAN: fast mode bills at twice the standard prices, and
31
+ * US-only inference at 1.1x.
32
+ *
33
+ * So a usage now says these of itself ({@link TokenUsage}), and whoever folds
34
+ * requests into totals keeps the requests that are priced differently apart
35
+ * ({@link requestRates}).
18
36
  */
19
37
  /** Identifies {@link MODEL_PRICING}. Must equal the service's constant. */
20
- export const PRICING_VERSION = "2026-09-24";
38
+ export const PRICING_VERSION = "2026-11-11";
21
39
  /**
22
- * A 5-minute cache write bills at 1.25x input. Cache reads bill at 0.1x input,
23
- * except on models whose list price says otherwise (Opus 5.5, Fable 5.1), which
24
- * pass their read price in dollars.
40
+ * A rate card from its list prices. A 5-minute cache write bills at 1.25x
41
+ * input and a 1-hour one at 2x, on every model. A cache read bills at 0.1x
42
+ * input, except on the models whose list price says otherwise (0.05x on Opus
43
+ * 5.5 and Sonnet 5.5; 0.025x on Fable and Mythos 5.1 and 5.2), which pass
44
+ * their read price in dollars.
25
45
  */
26
46
  const priced = (input, output, cacheRead = input * 0.1) => ({
27
47
  input,
28
48
  output,
29
49
  cacheRead,
30
50
  cacheWrite: input * 1.25,
51
+ cacheWrite1h: input * 2,
31
52
  });
32
- const MODEL_PRICING = {
33
- "claude-haiku-4-5": priced(1.0, 5.0),
34
- "claude-haiku-4-5-20251001": priced(1.0, 5.0),
35
- "claude-sonnet-4-5": priced(3.0, 15.0),
36
- "claude-sonnet-4-6": priced(3.0, 15.0),
37
- "claude-sonnet-5": priced(2.0, 10.0),
38
- "claude-opus-4-5": priced(5.0, 25.0),
39
- "claude-opus-4-6": priced(5.0, 25.0),
40
- "claude-opus-4-7": priced(5.0, 25.0),
41
- "claude-opus-4-8": priced(5.0, 25.0),
42
- "claude-opus-5": priced(5.0, 25.0),
43
- "claude-opus-5-5": priced(4.0, 20.0, 0.2),
44
- "claude-fable-5": priced(10.0, 50.0),
45
- "claude-fable-5-1": priced(10.0, 50.0, 0.25),
46
- };
53
+ const model = (rates, longPrompt) => longPrompt ? { rates, longPrompt } : { rates };
54
+ /**
55
+ * A Map, not an object: a model's name comes from a transcript or from another
56
+ * host's payload, and `table["constructor"]` on an object is a function, which
57
+ * priced a turn at NaN.
58
+ */
59
+ const MODEL_PRICING = new Map([
60
+ ["claude-haiku-4-5", model(priced(1.0, 5.0))],
61
+ // Priced by prompt length: up to 100,000 tokens, and over.
62
+ ["claude-haiku-5-5", model(priced(0.1, 0.5), { overTokens: 100_000, rates: priced(0.5, 2.5) })],
63
+ ["claude-sonnet-4", model(priced(3.0, 15.0))],
64
+ ["claude-sonnet-4-0", model(priced(3.0, 15.0))],
65
+ ["claude-sonnet-4-5", model(priced(3.0, 15.0))],
66
+ ["claude-sonnet-4-6", model(priced(3.0, 15.0))],
67
+ ["claude-sonnet-5", model(priced(2.0, 10.0))],
68
+ ["claude-sonnet-5-5", model(priced(2.0, 10.0, 0.1))],
69
+ ["claude-opus-4", model(priced(15.0, 75.0))],
70
+ ["claude-opus-4-0", model(priced(15.0, 75.0))],
71
+ ["claude-opus-4-1", model(priced(15.0, 75.0))],
72
+ ["claude-opus-4-5", model(priced(5.0, 25.0))],
73
+ ["claude-opus-4-6", model(priced(5.0, 25.0))],
74
+ ["claude-opus-4-7", model(priced(5.0, 25.0))],
75
+ ["claude-opus-4-8", model(priced(5.0, 25.0))],
76
+ ["claude-opus-5", model(priced(5.0, 25.0))],
77
+ ["claude-opus-5-5", model(priced(4.0, 20.0, 0.2))],
78
+ ["claude-fable-5", model(priced(10.0, 50.0))],
79
+ ["claude-fable-5-1", model(priced(10.0, 50.0, 0.25))],
80
+ ["claude-fable-5-2", model(priced(8.0, 40.0, 0.2))],
81
+ ["claude-mythos-5", model(priced(10.0, 50.0))],
82
+ ["claude-mythos-5-1", model(priced(10.0, 50.0, 0.25))],
83
+ ["claude-mythos-5-2", model(priced(8.0, 40.0, 0.2))],
84
+ ]);
85
+ /** Fast mode bills every kind of token at twice the standard price. */
86
+ const FAST_FACTOR = 2;
87
+ /** US-only inference (`inference_geo: "us"`) bills every kind of token at 1.1x. */
88
+ const US_ONLY_FACTOR = 1.1;
89
+ /**
90
+ * What may follow a model's name and still be that model: a dated snapshot
91
+ * (`-20251001`, `@20251001`), a cloud's version tag (`-v1:0`), `-latest`.
92
+ */
93
+ const SNAPSHOT_SUFFIX = /^(?:[-@]\d{8})?(?:-v\d+(?::\d+)?)?(?:-latest)?$/;
47
94
  /**
48
- * Tolerate provider prefixes (`anthropic/`, `us.anthropic.`), date snapshots and
49
- * unseen version bumps by falling back to the longest matching family, so a new
50
- * snapshot never silently prices at $0.
95
+ * The table's row for a name: the name itself, or the name with a provider
96
+ * prefix (`anthropic/`, `us.anthropic.`), a host's tag (`[1m]`) or a snapshot
97
+ * suffix taken off.
98
+ *
99
+ * NEVER A NEWER VERSION PRICED AS AN OLDER ONE. This used to take the longest
100
+ * row that prefixed the name, "so a new snapshot never silently prices at $0".
101
+ * A new *version* is not a snapshot: `claude-opus-5-5` was priced as Opus 5
102
+ * until its row came, and `claude-fable-5-2` as Fable 5 — a quarter too dear
103
+ * on input and output, five times on cache reads — and each such turn was
104
+ * reported as measured. A version with no row has no price: the turn is
105
+ * reported unmeasured, and says which model to add (`run.unmeasured`).
51
106
  */
52
- export function resolveModelPricing(model) {
53
- const exact = MODEL_PRICING[model];
107
+ function pricedModel(name) {
108
+ const exact = MODEL_PRICING.get(name);
54
109
  if (exact)
55
110
  return exact;
56
- // Strip a provider prefix: "us.anthropic.claude-haiku-4-5-v1:0" → "claude-haiku-4-5-v1:0".
57
- const idx = model.indexOf("claude-");
58
- const bare = idx >= 0 ? model.slice(idx) : model;
59
- if (MODEL_PRICING[bare])
60
- return MODEL_PRICING[bare];
61
- // Longest declared family that prefixes the name — so a dated snapshot of a
62
- // known model prices as that model rather than as nothing.
111
+ const at = name.indexOf("claude-");
112
+ const bare = (at >= 0 ? name.slice(at) : name).replace(/\[[^\]]*\]$/, "");
113
+ const known = MODEL_PRICING.get(bare);
114
+ if (known)
115
+ return known;
63
116
  let best;
64
- for (const key of Object.keys(MODEL_PRICING)) {
65
- if (bare.startsWith(key) && (!best || key.length > best.length))
117
+ for (const key of MODEL_PRICING.keys()) {
118
+ if (!bare.startsWith(key) || !SNAPSHOT_SUFFIX.test(bare.slice(key.length)))
119
+ continue;
120
+ if (!best || key.length > best.length)
66
121
  best = key;
67
122
  }
68
- return best ? MODEL_PRICING[best] : undefined;
123
+ return best ? MODEL_PRICING.get(best) : undefined;
124
+ }
125
+ /**
126
+ * A model's rate card, or undefined when the table has no row for it. With
127
+ * `longPrompt`, the card of a request over the model's prompt-length
128
+ * threshold, for a model that has one.
129
+ */
130
+ export function resolveModelPricing(name, opts = {}) {
131
+ const row = pricedModel(name);
132
+ if (!row)
133
+ return undefined;
134
+ return opts.longPrompt && row.longPrompt ? row.longPrompt.rates : row.rates;
135
+ }
136
+ export function hasModelPricing(name) {
137
+ return pricedModel(name) !== undefined;
138
+ }
139
+ /**
140
+ * The flags of ONE request, from its own `usage` as the API reports it: its
141
+ * prompt length (input, cache reads and cache writes together), its `speed`
142
+ * and its `inference_geo`. Only what is so is set, so two requests priced
143
+ * alike give equal flags — and {@link rateKey} the same key.
144
+ */
145
+ export function requestRates(name, request) {
146
+ const flags = {};
147
+ const over = pricedModel(name)?.longPrompt?.overTokens;
148
+ if (over !== undefined && request.promptTokens > over)
149
+ flags.longPrompt = true;
150
+ if (request.speed === "fast")
151
+ flags.fast = true;
152
+ if (request.inferenceGeo === "us")
153
+ flags.usOnly = true;
154
+ return flags;
69
155
  }
70
- export function hasModelPricing(model) {
71
- return resolveModelPricing(model) !== undefined;
156
+ /** A short text that is equal for two sets of flags exactly when they price alike. "" for none. */
157
+ export function rateKey(flags) {
158
+ return `${flags.longPrompt ? "|long" : ""}${flags.fast ? "|fast" : ""}${flags.usOnly ? "|us" : ""}`;
72
159
  }
73
160
  /** Cost in USD. An unpriced model yields 0 — the caller reports `measured: false`. */
74
- export function calculateCostUsd(model, usage) {
75
- const p = resolveModelPricing(model);
161
+ export function calculateCostUsd(name, usage) {
162
+ const p = resolveModelPricing(name, usage);
76
163
  if (!p)
77
164
  return 0;
165
+ const writes = Math.max(0, usage.cacheCreationTokens ?? 0);
166
+ const writes1h = Math.min(writes, Math.max(0, usage.cacheCreation1hTokens ?? 0));
167
+ const factor = (usage.fast ? FAST_FACTOR : 1) * (usage.usOnly ? US_ONLY_FACTOR : 1);
78
168
  const perToken = 1e-6;
79
- return (usage.inputTokens * p.input * perToken +
80
- usage.outputTokens * p.output * perToken +
81
- (usage.cacheReadTokens ?? 0) * p.cacheRead * perToken +
82
- (usage.cacheCreationTokens ?? 0) * p.cacheWrite * perToken);
169
+ return (factor *
170
+ perToken *
171
+ (usage.inputTokens * p.input +
172
+ usage.outputTokens * p.output +
173
+ (usage.cacheReadTokens ?? 0) * p.cacheRead +
174
+ (writes - writes1h) * p.cacheWrite +
175
+ writes1h * p.cacheWrite1h));
83
176
  }
84
177
  //# sourceMappingURL=pricing.js.map
@@ -0,0 +1,48 @@
1
+ /**
2
+ * reads-only — whether one call of the agent's only looks, and changes nothing.
3
+ *
4
+ * WHY THE RUNNER ASKS (B2 is11.md, 2026-11-11). A direct plan is announced
5
+ * with "call run_scenario before any other tool", and a first call that is
6
+ * not `run_scenario` used to retire it on the spot (control/server.ts). The
7
+ * rule is there for one danger: a model that has no `run_scenario` makes its
8
+ * own calls, and a divergence would then run every step, writes included,
9
+ * inside a hook nobody approves. But the call that retired the plan in is11
10
+ * was the agent reading its own notes file: it had seen another person's name
11
+ * in the note (Olga, the recording's; the request said Carlos) and checked
12
+ * first. The plan would have done the task right — its steps take the person
13
+ * from the request — and the turn cost a full agent run.
14
+ *
15
+ * So a call that only looks no longer retires a direct plan: it runs as the
16
+ * agent wrote it, and the plan waits, still announced, for `run_scenario`.
17
+ * The first call that may change something still retires it, running nothing
18
+ * (and nothing ever runs in a hook in its place): a plan run after the agent
19
+ * changed something itself could do the same work twice.
20
+ *
21
+ * WHEN IN DOUBT, IT IS A CHANGE. A call judged "only looks" that does change
22
+ * something leaves a plan armed that could then repeat that change; a change
23
+ * judged a change only ends a replay early, as every call did before. So:
24
+ *
25
+ * - Claude Code's built-ins by name: the reading ones only look, every other
26
+ * built-in (a sub-agent, an edit, an unknown new tool) is a change;
27
+ * - `Bash` by its command ({@link commandReadsOnly}), and `PowerShell` never;
28
+ * - an MCP tool by what its own server says of it (`annotations`), else by
29
+ * its name: a reading word and no writing word.
30
+ *
31
+ * The same rule as the service's `readsOnly` (correctionCheck.ts, AC13), kept
32
+ * in step by hand, stricter where a wrong "only looks" costs more here: a
33
+ * command substitution, a process substitution or an `awk` `system(` makes a
34
+ * command a change.
35
+ */
36
+ import type { ToolAnnotations } from "../record/recorder.js";
37
+ /**
38
+ * Whether this call only looks. `toolInput` is the hook's `tool_input` (only a
39
+ * `Bash` call's `command` is read from it); `hints` is what the tool's own
40
+ * server listed for it, when it listed anything.
41
+ */
42
+ export declare function readsOnlyCall(toolName: string, toolInput?: unknown, hints?: ToolAnnotations): boolean;
43
+ /**
44
+ * Whether a shell command only looks: every part of it is a looking command,
45
+ * nothing is written to a file, and nothing runs code it does not show.
46
+ */
47
+ export declare function commandReadsOnly(command: string): boolean;
48
+ //# sourceMappingURL=reads-only.d.ts.map