@xhulz/nina 0.32.0 → 0.33.0

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 (130) hide show
  1. package/README.md +5 -3
  2. package/package.json +1 -1
  3. package/releases/0.33.0/core/GAPS.md +125 -0
  4. package/releases/0.33.0/core/answered.json +29 -0
  5. package/releases/0.33.0/core/defaults/tree/.claude/agents/architect.md +1 -0
  6. package/releases/0.33.0/core/defaults/tree/.claude/agents/dba.md +3 -0
  7. package/releases/0.33.0/core/defaults/tree/.claude/agents/devops.md +1 -0
  8. package/releases/0.33.0/core/defaults/tree/.claude/agents/implementer.md +1 -0
  9. package/releases/0.33.0/core/defaults/tree/.claude/agents/integration-tester.md +3 -0
  10. package/releases/0.33.0/core/defaults/tree/.claude/agents/qa.md +2 -0
  11. package/releases/0.33.0/core/defaults/tree/.claude/agents/reviewer.md +3 -0
  12. package/releases/0.33.0/core/defaults/tree/.claude/agents/secops.md +3 -0
  13. package/releases/0.33.0/core/defaults/tree/.claude/agents/solidity-auditor.md +3 -0
  14. package/releases/0.33.0/core/defaults/tree/.claude/agents/solidity-dev.md +3 -0
  15. package/releases/0.33.0/core/tree/.claude/agents/architect.md +127 -0
  16. package/releases/0.33.0/core/tree/.claude/agents/dba.md +110 -0
  17. package/releases/0.33.0/core/tree/.claude/agents/devops.md +114 -0
  18. package/releases/0.33.0/core/tree/.claude/agents/implementer.md +156 -0
  19. package/releases/0.33.0/core/tree/.claude/agents/integration-tester.md +168 -0
  20. package/releases/0.33.0/core/tree/.claude/agents/planner.md +132 -0
  21. package/releases/0.33.0/core/tree/.claude/agents/qa.md +148 -0
  22. package/releases/0.33.0/core/tree/.claude/agents/reviewer.md +183 -0
  23. package/releases/0.33.0/core/tree/.claude/agents/secops.md +121 -0
  24. package/releases/0.33.0/core/tree/.claude/agents/solidity-auditor.md +126 -0
  25. package/releases/0.33.0/core/tree/.claude/agents/solidity-dev.md +123 -0
  26. package/releases/0.33.0/core/tree/.claude/agents-overview.md +64 -0
  27. package/releases/0.33.0/core/tree/.claude/frontend.md +10 -0
  28. package/releases/0.33.0/core/tree/.claude/graph.md +75 -0
  29. package/releases/0.33.0/core/tree/.claude/patterns.md +213 -0
  30. package/releases/0.33.0/core/tree/.claude/pills/README.md +146 -0
  31. package/releases/0.33.0/core/tree/.claude/pipeline.md +101 -0
  32. package/releases/0.33.0/core/tree/.claude/retrieval.md +103 -0
  33. package/releases/0.33.0/core/tree/.claude/router.md +198 -0
  34. package/releases/0.33.0/core/tree/.claude/templates/integration.md +45 -0
  35. package/releases/0.33.0/core/tree/CLAUDE.md +161 -0
  36. package/releases/0.33.0/core/tree/scripts/cost-watch.mjs +18 -0
  37. package/releases/0.33.0/core/tree/scripts/edit-guard.mjs +16 -0
  38. package/releases/0.33.0/core/tree/scripts/harness-check.mjs +71 -0
  39. package/releases/0.33.0/core/tree/scripts/loop-gate.mjs +19 -0
  40. package/releases/0.33.0/core/vocabulary.json +13 -0
  41. package/releases/0.33.0/surfaces/blockchain/tree/.claude/agents/architect.md +5 -0
  42. package/releases/0.33.0/surfaces/blockchain/tree/.claude/agents/devops.md +5 -0
  43. package/releases/0.33.0/surfaces/blockchain/tree/.claude/agents/reviewer.md +5 -0
  44. package/releases/0.33.0/surfaces/blockchain/tree/.claude/agents/solidity-auditor.md +18 -0
  45. package/releases/0.33.0/surfaces/blockchain/tree/.claude/agents/solidity-dev.md +16 -0
  46. package/releases/0.33.0/surfaces/blockchain/tree/.claude/agents-overview.md +6 -0
  47. package/releases/0.33.0/surfaces/blockchain/tree/.claude/graph.md +11 -0
  48. package/releases/0.33.0/surfaces/blockchain/tree/.claude/router.md +6 -0
  49. package/releases/0.33.0/surfaces/blockchain/tree/CLAUDE.md +12 -0
  50. package/releases/0.33.0/surfaces/db/tree/.claude/agents/architect.md +5 -0
  51. package/releases/0.33.0/surfaces/db/tree/.claude/agents/dba.md +50 -0
  52. package/releases/0.33.0/surfaces/db/tree/.claude/agents/devops.md +5 -0
  53. package/releases/0.33.0/surfaces/db/tree/.claude/agents/implementer.md +2 -0
  54. package/releases/0.33.0/surfaces/db/tree/.claude/agents/integration-tester.md +10 -0
  55. package/releases/0.33.0/surfaces/db/tree/.claude/agents/planner.md +2 -0
  56. package/releases/0.33.0/surfaces/db/tree/.claude/agents/reviewer.md +22 -0
  57. package/releases/0.33.0/surfaces/db/tree/.claude/agents/secops.md +2 -0
  58. package/releases/0.33.0/surfaces/db/tree/.claude/agents-overview.md +5 -0
  59. package/releases/0.33.0/surfaces/db/tree/.claude/graph.md +8 -0
  60. package/releases/0.33.0/surfaces/db/tree/.claude/patterns.md +19 -0
  61. package/releases/0.33.0/surfaces/db/tree/.claude/pipeline.md +13 -0
  62. package/releases/0.33.0/surfaces/db/tree/.claude/retrieval.md +5 -0
  63. package/releases/0.33.0/surfaces/db/tree/.claude/router.md +4 -0
  64. package/releases/0.33.0/surfaces/db/tree/CLAUDE.md +17 -0
  65. package/releases/0.33.0/surfaces/edge-cf/tree/.claude/agents/architect.md +14 -0
  66. package/releases/0.33.0/surfaces/edge-cf/tree/.claude/agents/dba.md +2 -0
  67. package/releases/0.33.0/surfaces/edge-cf/tree/.claude/agents/devops.md +32 -0
  68. package/releases/0.33.0/surfaces/edge-cf/tree/.claude/agents/implementer.md +14 -0
  69. package/releases/0.33.0/surfaces/edge-cf/tree/.claude/agents/integration-tester.md +11 -0
  70. package/releases/0.33.0/surfaces/edge-cf/tree/.claude/agents/qa.md +3 -0
  71. package/releases/0.33.0/surfaces/edge-cf/tree/.claude/agents/reviewer.md +8 -0
  72. package/releases/0.33.0/surfaces/edge-cf/tree/.claude/agents/secops.md +8 -0
  73. package/releases/0.33.0/surfaces/edge-cf/tree/.claude/patterns.md +17 -0
  74. package/releases/0.33.0/surfaces/edge-cf/tree/.claude/retrieval.md +8 -0
  75. package/releases/0.33.0/surfaces/edge-cf/tree/CLAUDE.md +27 -0
  76. package/releases/0.33.0/surfaces/frontend/tree/.claude/agents/architect.md +20 -0
  77. package/releases/0.33.0/surfaces/frontend/tree/.claude/agents/devops.md +28 -0
  78. package/releases/0.33.0/surfaces/frontend/tree/.claude/agents/implementer.md +25 -0
  79. package/releases/0.33.0/surfaces/frontend/tree/.claude/agents/qa.md +17 -0
  80. package/releases/0.33.0/surfaces/frontend/tree/.claude/agents/reviewer.md +56 -0
  81. package/releases/0.33.0/surfaces/frontend/tree/.claude/patterns.md +8 -0
  82. package/releases/0.33.0/surfaces/frontend/tree/.claude/retrieval.md +2 -0
  83. package/releases/0.33.0/surfaces/frontend/tree/.claude/router.md +14 -0
  84. package/releases/0.33.0/surfaces/frontend/tree/CLAUDE.md +5 -0
  85. package/releases/0.33.0/surfaces/integrations/tree/.claude/agents/architect.md +16 -0
  86. package/releases/0.33.0/surfaces/integrations/tree/.claude/agents/implementer.md +12 -0
  87. package/releases/0.33.0/surfaces/integrations/tree/.claude/agents/integration-tester.md +20 -0
  88. package/releases/0.33.0/surfaces/integrations/tree/.claude/agents/planner.md +2 -0
  89. package/releases/0.33.0/surfaces/integrations/tree/.claude/agents/reviewer.md +21 -0
  90. package/releases/0.33.0/surfaces/integrations/tree/.claude/agents/secops.md +2 -0
  91. package/releases/0.33.0/surfaces/integrations/tree/.claude/agents-overview.md +8 -0
  92. package/releases/0.33.0/surfaces/integrations/tree/.claude/graph.md +10 -0
  93. package/releases/0.33.0/surfaces/integrations/tree/.claude/patterns.md +38 -0
  94. package/releases/0.33.0/surfaces/integrations/tree/.claude/pipeline.md +16 -0
  95. package/releases/0.33.0/surfaces/integrations/tree/.claude/retrieval.md +16 -0
  96. package/releases/0.33.0/surfaces/integrations/tree/.claude/router.md +6 -0
  97. package/releases/0.33.0/surfaces/integrations/tree/CLAUDE.md +17 -0
  98. package/releases/0.33.0/surfaces/money/tree/.claude/agents/architect.md +12 -0
  99. package/releases/0.33.0/surfaces/money/tree/.claude/agents/dba.md +14 -0
  100. package/releases/0.33.0/surfaces/money/tree/.claude/agents/implementer.md +11 -0
  101. package/releases/0.33.0/surfaces/money/tree/.claude/agents/integration-tester.md +3 -0
  102. package/releases/0.33.0/surfaces/money/tree/.claude/agents/reviewer.md +23 -0
  103. package/releases/0.33.0/surfaces/money/tree/.claude/agents/secops.md +5 -0
  104. package/releases/0.33.0/surfaces/money/tree/.claude/agents/solidity-auditor.md +4 -0
  105. package/releases/0.33.0/surfaces/money/tree/.claude/agents/solidity-dev.md +4 -0
  106. package/releases/0.33.0/surfaces/money/tree/.claude/graph.md +2 -0
  107. package/releases/0.33.0/surfaces/money/tree/.claude/patterns.md +49 -0
  108. package/releases/0.33.0/surfaces/money/tree/.claude/pipeline.md +2 -0
  109. package/releases/0.33.0/surfaces/money/tree/.claude/retrieval.md +2 -0
  110. package/releases/0.33.0/surfaces/money/tree/CLAUDE.md +11 -0
  111. package/releases/0.33.0/surfaces/pii/tree/.claude/agents/dba.md +2 -0
  112. package/releases/0.33.0/surfaces/pii/tree/.claude/agents/devops.md +8 -0
  113. package/releases/0.33.0/surfaces/pii/tree/.claude/agents/reviewer.md +8 -0
  114. package/releases/0.33.0/surfaces/pii/tree/.claude/agents/secops.md +2 -0
  115. package/releases/0.33.0/surfaces/pii/tree/.claude/agents/solidity-auditor.md +5 -0
  116. package/releases/0.33.0/surfaces/pii/tree/CLAUDE.md +5 -0
  117. package/releases/0.33.0/surfaces/prisma/tree/.claude/agents/architect.md +2 -0
  118. package/releases/0.33.0/surfaces/prisma/tree/.claude/agents/dba.md +39 -0
  119. package/releases/0.33.0/surfaces/prisma/tree/.claude/agents/devops.md +11 -0
  120. package/releases/0.33.0/surfaces/prisma/tree/.claude/agents/implementer.md +2 -0
  121. package/releases/0.33.0/surfaces/prisma/tree/.claude/agents/integration-tester.md +2 -0
  122. package/releases/0.33.0/surfaces/prisma/tree/.claude/agents/reviewer.md +2 -0
  123. package/releases/0.33.0/surfaces/prisma/tree/.claude/patterns.md +15 -0
  124. package/releases/0.33.0/surfaces/prisma/tree/CLAUDE.md +5 -0
  125. package/src/commands/check.mjs +3 -1
  126. package/src/commands/runs.mjs +7 -2
  127. package/src/commands/stats.mjs +35 -0
  128. package/src/commands/upgrade.mjs +12 -3
  129. package/src/expected.mjs +4 -3
  130. package/src/transcripts.mjs +27 -1
@@ -223,6 +223,40 @@ function costReport(records) {
223
223
  console.log(bold(` ${'all stages'.padEnd(20)}${String([...byRole.values()].flat().length).padStart(6)}${''.padStart(10)}${money(total).padStart(10)}`));
224
224
  }
225
225
 
226
+ /**
227
+ * How each stage's rounds spent their context: turns, tool calls sent per turn, and the context a turn
228
+ * re-read when the round began and at its largest. Every turn is billed the whole context again, so the
229
+ * cost of a round is its turns times its context, and a round that starts where a long one ended pays for
230
+ * all of it on every turn. The first rounds of a run are set beside the later ones, which are resumes.
231
+ *
232
+ * @param {object[]} records - The rounds in the window.
233
+ */
234
+ function contextReport(records) {
235
+ const shaped = records.filter((r) => typeof r.turns === 'number' && r.turns > 0);
236
+ if (shaped.length === 0) return;
237
+ const median = (xs) => [...xs].sort((a, b) => a - b)[Math.floor(xs.length / 2)];
238
+ const k = (n) => (typeof n === 'number' ? `${Math.round(n / 1000)}k` : '—');
239
+ console.log(heading('context', 'every turn re-reads the whole context: a round costs its turns times its size, and a resumed one starts where it ended'));
240
+ console.log(dim(` ${'stage'.padEnd(20)}${'rounds'.padEnd(8)}${'n'.padStart(4)}${'turns'.padStart(7)}${'calls/turn'.padStart(12)}${'starts at'.padStart(11)}${'peak'.padStart(8)}`));
241
+ const byRole = new Map();
242
+ for (const r of shaped) byRole.set(r.role, [...(byRole.get(r.role) ?? []), r]);
243
+ for (const [role, rounds] of [...byRole].sort((a, b) => b[1].reduce((x, r) => x + r.turns, 0) - a[1].reduce((x, r) => x + r.turns, 0))) {
244
+ [
245
+ ['first', rounds.filter((r) => (r.round ?? 1) === 1)],
246
+ ['resumed', rounds.filter((r) => (r.round ?? 1) > 1)],
247
+ ].forEach(([label, group], i) => {
248
+ if (group.length === 0) return;
249
+ const turns = group.reduce((a, r) => a + r.turns, 0);
250
+ const calls = group.reduce((a, r) => a + (r.tool_calls ?? 0), 0);
251
+ const name = i === 0 || !rounds.some((r) => (r.round ?? 1) === 1) ? pink(role) + ' '.repeat(Math.max(20 - role.length, 1)) : ' '.repeat(20);
252
+ console.log(
253
+ ` ${name}${dim(label.padEnd(8))}${String(group.length).padStart(4)}${String(median(group.map((r) => r.turns))).padStart(7)}` +
254
+ `${(calls / turns).toFixed(2).padStart(12)}${k(median(group.map((r) => r.context_start ?? 0))).padStart(11)}${k(median(group.map((r) => r.context_peak ?? 0))).padStart(8)}`,
255
+ );
256
+ });
257
+ }
258
+ }
259
+
226
260
  /**
227
261
  * Whether a model a run used is the one a spec declares: an alias (`opus`) names a family — a whole
228
262
  * segment of the id, wherever it sits, so `claude-3-sonnet` is a sonnet — anything else names a model
@@ -722,6 +756,7 @@ export async function stats(argv, ctx) {
722
756
  }
723
757
 
724
758
  costReport(records.filter((r) => r.status !== 'denied'));
759
+ contextReport(records.filter((r) => r.status !== 'denied'));
725
760
  modelReport(records.filter((r) => r.status !== 'denied'));
726
761
  proportionReport(records.filter((r) => r.status !== 'denied'), stepLimitOf(ctx));
727
762
 
@@ -37,7 +37,8 @@ const BANNER = /^[\s█╗╔╝║═╚▄▀]*$|harness orchestration ·/;
37
37
  * fault — everything else it finds still rolls the move back.
38
38
  *
39
39
  * @param {string} target - The project.
40
- * @param {string[]} newSlots - Project slots this move introduces, as `<file> <slot id>`.
40
+ * @param {string[]} newSlots - Project slots this move introduces, as `<file> <slot id>`, and the
41
+ * vocabulary names it introduces, as `{{NAME}}`.
41
42
  * @returns {{label: string, args: string[]}[]}
42
43
  */
43
44
  function validationsFor(target, newSlots = []) {
@@ -242,6 +243,10 @@ export async function upgrade(argv, ctx) {
242
243
  // A slot the new release fills itself is not asked of the project: it composes as the release wrote it.
243
244
  const defaultedAfter = await defaultedSlots(onto.dir);
244
245
  const newSlots = [...slotsAfter].filter((s) => !slotsBefore.has(s) && !filled.has(s) && !defaultedAfter.has(s)).sort();
246
+ // A name only the new release uses is owed the way a new slot is: it cannot be filled with meaning
247
+ // before the core that asks for it is pinned, and rolling back for it left no order the move completes in.
248
+ const newVocab = needed.filter((v) => !vocabBefore.has(v));
249
+ const awaited = [...newSlots, ...newVocab.map((v) => `{{${v}}}`)];
245
250
  const stranded = [...filled].filter((s) => !slotsAfter.has(s)).sort();
246
251
 
247
252
  console.log(` ${profile.core} → ${to}\n`);
@@ -357,7 +362,7 @@ export async function upgrade(argv, ctx) {
357
362
  // What was already failing is not this upgrade's doing, and rolling back for it would blame
358
363
  // the move for a problem the project brought with it. Measured before anything is written,
359
364
  // so the only rollback is for something that WAS passing and now is not.
360
- const VALIDATIONS = validationsFor(target, newSlots);
365
+ const VALIDATIONS = validationsFor(target, awaited);
361
366
  const DETECTORS = "running the project's own detectors";
362
367
  const hasDetectors = await declares(target, 'harness:check');
363
368
 
@@ -416,7 +421,7 @@ export async function upgrade(argv, ctx) {
416
421
  // `harness:check` — an npm script whose detector list this command does not own and cannot
417
422
  // pass an argument to. The environment is what crosses that boundary, and setting it once
418
423
  // means a reporter added to the chain later is told without anyone remembering to tell it.
419
- if (newSlots.length > 0) process.env[EXPECT_ENV] = newSlots.join(',');
424
+ if (awaited.length > 0) process.env[EXPECT_ENV] = awaited.join(',');
420
425
 
421
426
  // Compose writes and never deletes. So the files the new version stops composing are removed right
422
427
  // after it composes — before anything is verified, so what is verified is what stays — unless they
@@ -516,6 +521,10 @@ export async function upgrade(argv, ctx) {
516
521
  console.log(`\n ${newSlots.length} project slot(s) still to fill — only this project can write them:`);
517
522
  for (const s of newSlots) console.log(` ${s}`);
518
523
  }
524
+ if (newVocab.length > 0) {
525
+ console.log(`\n ${newVocab.length} vocabulary name(s) ${to} asks for and this profile does not answer — fill each, or defer it with why:`);
526
+ console.log(` ${newVocab.join(', ')}`);
527
+ }
519
528
  console.log('');
520
529
  return 0;
521
530
  }
package/src/expected.mjs CHANGED
@@ -1,8 +1,9 @@
1
1
  /**
2
- * The project slots a caller has declared it already knows are open.
2
+ * The project slots, and vocabulary names, a caller has declared it already knows are open.
3
3
  *
4
4
  * `nina upgrade --apply` creates slots: a new core can introduce one, and it cannot be filled
5
- * before the core that introduces it is pinned. Every reporter that fails on an unfilled slot
5
+ * before the core that introduces it is pinned. A name only the new core uses is the same case,
6
+ * named as `{{NAME}}`: `check` rolled a move back for one the preview had just listed as needed. Every reporter that fails on an unfilled slot
6
7
  * therefore has to be told, or the move deadlocks — there is no order in which it completes.
7
8
  *
8
9
  * Two reporters say this same thing, and the first fix taught only one: `check` counts a slot
@@ -21,7 +22,7 @@ export const EXPECT_ENV = 'NINA_EXPECT_UNFILLED';
21
22
  /**
22
23
  * @param {string[]} argv - The command's arguments; `--expect-unfilled <a,b>` names slots.
23
24
  * @param {NodeJS.ProcessEnv} [env] - Where to read `NINA_EXPECT_UNFILLED` from.
24
- * @returns {Set<string>} Slot ids as `<relative path> <slot id>`.
25
+ * @returns {Set<string>} Slot ids as `<relative path> <slot id>`, names as `{{NAME}}`.
25
26
  */
26
27
  export function expectedUnfilled(argv = [], env = process.env) {
27
28
  const fromArgv = argv.includes('--expect-unfilled') ? argv[argv.indexOf('--expect-unfilled') + 1] ?? '' : '';
@@ -626,9 +626,13 @@ async function attachAgentDetail(projectDir, dispatches) {
626
626
  // 744 of one project's 826 in full on every turn, two seconds before the person saw an answer.
627
627
  const size = (await stat(join(dir, file)).catch(() => null))?.size ?? null;
628
628
  const settled = (r) => r.handed_back === false || String(r.verdict_source ?? '').startsWith('handback');
629
+ // `resumes` is written only by a reader from before rounds, which bills the whole run to its first
630
+ // record. A store both versions wrote to — one project on the old pin, a hook elsewhere on the new —
631
+ // had its runs marked read by the old one and skipped here, their first round billed twice.
629
632
  if (
630
633
  run.agent_read_bytes === size &&
631
- [...records, ...later].every((r) => settled(r) && 'round' in r && 'lessons_read' in r && 'tokens' in r && 'files_touched' in r && 'effort' in r)
634
+ !('resumes' in run) &&
635
+ [...records, ...later].every((r) => settled(r) && 'round' in r && 'lessons_read' in r && 'tokens' in r && 'files_touched' in r && 'effort' in r && 'turns' in r)
632
636
  ) {
633
637
  continue;
634
638
  }
@@ -656,6 +660,8 @@ async function attachAgentDetail(projectDir, dispatches) {
656
660
  usage: new Map(),
657
661
  /** The files the round wrote through the edit tools. Only their number is kept. */
658
662
  written: new Set(),
663
+ /** The tool calls the round made, by id: over its turns, how many calls each turn sent at once. */
664
+ calls: new Set(),
659
665
  });
660
666
  const rounds = [fresh()];
661
667
  const roundOf = roundsOf();
@@ -677,6 +683,7 @@ async function attachAgentDetail(projectDir, dispatches) {
677
683
  const round = rounds[n - 1];
678
684
  const hasSkill = line.includes('"Skill"');
679
685
  const hasHandback = line.includes('SubagentHandback');
686
+ for (const m of line.matchAll(/"type":"tool_use","id":"([^"]+)"/g)) round.calls.add(m[1]);
680
687
  if (/"name":"(Edit|Write|MultiEdit|NotebookEdit)"/.test(line)) {
681
688
  try {
682
689
  for (const block of JSON.parse(line)?.message?.content ?? []) {
@@ -755,9 +762,14 @@ async function attachAgentDetail(projectDir, dispatches) {
755
762
  targets = [dispatches.get(id)];
756
763
  }
757
764
  const spent = tokensOf(round.usage);
765
+ const shape = contextOf(round.usage);
758
766
  targets.forEach((record, j) => {
759
767
  // One transcript, one bill: were two records ever to share an agent, the second would count it again.
760
768
  record.tokens = j === 0 ? spent.tokens : null;
769
+ record.turns = shape.turns;
770
+ record.tool_calls = round.calls.size;
771
+ record.context_start = shape.start;
772
+ record.context_peak = shape.peak;
761
773
  record.usage_model = spent.model;
762
774
  record.effort = spent.effort;
763
775
  record.files_touched = round.written.size;
@@ -790,6 +802,20 @@ async function attachAgentDetail(projectDir, dispatches) {
790
802
  }
791
803
  }
792
804
 
805
+ /**
806
+ * How a round spent its context: how many turns it took, and how large the context each turn re-read was
807
+ * when the round began and at its largest. Every turn is billed the whole context again, so a round that
808
+ * begins where a long one ended — a run resumed for a fix — pays for everything before it on each turn.
809
+ *
810
+ * @param {Map<string, {usage: object}>} usage - Message id → its final usage, in the order the round sent them.
811
+ * @returns {{turns: number, start: number|null, peak: number|null}} Context sizes in tokens.
812
+ */
813
+ export function contextOf(usage) {
814
+ const n = (v) => (typeof v === 'number' ? v : 0);
815
+ const sizes = [...usage.values()].map(({ usage: u }) => n(u.input_tokens) + n(u.cache_read_input_tokens) + n(u.cache_creation_input_tokens));
816
+ return { turns: sizes.length, start: sizes[0] ?? null, peak: sizes.length > 0 ? Math.max(...sizes) : null };
817
+ }
818
+
793
819
  /**
794
820
  * What a run spent, from each of its messages' final usage: tokens by kind, and the model and effort
795
821
  * level that most of its messages ran at. Metadata — counts, a model id and a level, never a message.