pi-advisor-flow 0.3.6 → 0.4.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.
package/src/tools.ts CHANGED
@@ -24,7 +24,6 @@ import {
24
24
  advisorGitContextMaxCharsRef,
25
25
  advisorGitContextRef,
26
26
  advisorLoopThresholdRef,
27
- advisorMaxCallsPerSessionRef,
28
27
  advisorOutcomeLoggingRef,
29
28
  advisorPlanGateRef,
30
29
  advisorRedactSecretsRef,
@@ -35,6 +34,7 @@ import {
35
34
  advisorUntrackedContentRef,
36
35
  contextMaxCharsRef,
37
36
  executorRef,
37
+ getAdvisorMaxCallsPerSession,
38
38
  isSimpleMode,
39
39
  loadConfig,
40
40
  } from "./config.js";
@@ -68,6 +68,7 @@ import {
68
68
  import {
69
69
  buildScoutManifest,
70
70
  reconstructScoutConversation,
71
+ SCOUT_MANIFEST_MAX_BYTES,
71
72
  } from "./scout-context.js";
72
73
  import {
73
74
  AdvisorSessionState,
@@ -75,8 +76,13 @@ import {
75
76
  type GateDecision,
76
77
  type GateTrigger,
77
78
  } from "./session-state.js";
78
- import type { BenchmarkTelemetry } from "./telemetry.js";
79
79
  import { readTrackedFiles, readUntrackedFiles } from "./untracked.js";
80
+ import {
81
+ advisorUsageCost,
82
+ advisorUsageForPi,
83
+ formatAdvisorUsage,
84
+ snapshotAdvisorUsage,
85
+ } from "./usage.js";
80
86
 
81
87
  export type {
82
88
  AdvisorInvocationRecord,
@@ -302,6 +308,7 @@ export interface AdvisorGateFailure {
302
308
  markdown?: string;
303
309
  message: string;
304
310
  ok: false;
311
+ usage?: unknown;
305
312
  }
306
313
  export interface AdvisorConsultationResult {
307
314
  adviceId: string;
@@ -327,14 +334,6 @@ export interface AdvisorGateResult {
327
334
  }
328
335
  export type AdvisorGateOutcome = AdvisorGateResult | AdvisorGateFailure;
329
336
 
330
- export const advisorUsageCost = (usage: unknown): number | undefined => {
331
- const value = usage as
332
- | { cost?: { total?: unknown }; totalCost?: unknown }
333
- | undefined;
334
- const cost = value?.cost?.total ?? value?.totalCost;
335
- return typeof cost === "number" ? cost : undefined;
336
- };
337
-
338
337
  const DECISION_LINE = /^Decision\s*:\s*(proceed|revise|blocked)\s*$/i;
339
338
  const CODE_FENCE = /^(?:```|~~~)/;
340
339
  const LINE_BREAK = /\r?\n/;
@@ -431,8 +430,7 @@ export const curateAdvisorConversation = async (
431
430
  enabled = advisorScoutEnabledRef,
432
431
  runScout: typeof runAdvisorScout = runAdvisorScout,
433
432
  currentInvocationId?: string,
434
- maxChars?: number,
435
- telemetry?: BenchmarkTelemetry
433
+ maxChars?: number
436
434
  ): Promise<{
437
435
  conversation: string;
438
436
  scout?: Exclude<ScoutOutcome, { cancelled: true }>;
@@ -445,7 +443,8 @@ export const curateAdvisorConversation = async (
445
443
  }
446
444
  const built = buildScoutManifest(ctx, {
447
445
  currentInvocationId,
448
- maxBytes: maxChars,
446
+ maxConversationChars: maxChars,
447
+ maxManifestBytes: SCOUT_MANIFEST_MAX_BYTES,
449
448
  });
450
449
  if (!built.ok) {
451
450
  const scout: Exclude<ScoutOutcome, { cancelled: true }> = {
@@ -470,8 +469,7 @@ export const curateAdvisorConversation = async (
470
469
  signal,
471
470
  onScout,
472
471
  undefined,
473
- undefined,
474
- telemetry
472
+ undefined
475
473
  );
476
474
  if (!outcome.ok && outcome.cancelled) {
477
475
  throw signal?.reason instanceof Error
@@ -504,8 +502,7 @@ const collectAdvisorResponse = async (
504
502
  includeUntracked?: string[],
505
503
  includeTracked?: string[],
506
504
  onScout?: (event: ScoutLifecycleEvent) => void,
507
- currentInvocationId?: string,
508
- telemetry?: BenchmarkTelemetry
505
+ currentInvocationId?: string
509
506
  ) => {
510
507
  loadConfig(ctx);
511
508
  const resolved = await resolveConfiguredModel(ctx, advisorRef, "Advisor");
@@ -551,8 +548,7 @@ const collectAdvisorResponse = async (
551
548
  advisorScoutEnabledRef,
552
549
  runAdvisorScout,
553
550
  currentInvocationId,
554
- conversationBudget,
555
- telemetry
551
+ conversationBudget
556
552
  );
557
553
  const { conversation, scout } = curated;
558
554
  const preferences = await readProjectPreferences(
@@ -643,43 +639,22 @@ export const consultAdvisor = async (
643
639
  includeUntracked?: string[],
644
640
  includeTracked?: string[],
645
641
  onScout?: (event: ScoutLifecycleEvent) => void,
646
- currentInvocationId?: string,
647
- telemetry?: BenchmarkTelemetry
642
+ currentInvocationId?: string
648
643
  ): Promise<AdvisorConsultationResult> => {
649
- telemetry?.advisorStart({ model: advisorRef, question, trigger });
650
- try {
651
- const result = await collectAdvisorResponse(
652
- ctx,
653
- ADVISOR_SYSTEM,
654
- question,
655
- signal,
656
- onChunk,
657
- gitContext,
658
- draft,
659
- includeUntracked,
660
- includeTracked,
661
- onScout,
662
- currentInvocationId,
663
- telemetry
664
- );
665
- telemetry?.advisorEnd({
666
- model: result.model,
667
- outcome: "completed",
668
- question,
669
- response: result.markdown,
670
- trigger,
671
- usage: result.usage,
672
- });
673
- return { ...result, adviceId: randomUUID(), trigger };
674
- } catch (error) {
675
- telemetry?.advisorError({
676
- category: signal?.aborted ? "cancelled" : "provider-error",
677
- model: advisorRef,
678
- question,
679
- trigger,
680
- });
681
- throw error;
682
- }
644
+ const result = await collectAdvisorResponse(
645
+ ctx,
646
+ ADVISOR_SYSTEM,
647
+ question,
648
+ signal,
649
+ onChunk,
650
+ gitContext,
651
+ draft,
652
+ includeUntracked,
653
+ includeTracked,
654
+ onScout,
655
+ currentInvocationId
656
+ );
657
+ return { ...result, adviceId: randomUUID(), trigger };
683
658
  };
684
659
 
685
660
  export const runAdvisorGate = async (
@@ -689,10 +664,8 @@ export const runAdvisorGate = async (
689
664
  signal?: AbortSignal,
690
665
  onChunk?: (thinking: string, text: string) => void,
691
666
  onScout?: (event: ScoutLifecycleEvent) => void,
692
- currentInvocationId?: string,
693
- telemetry?: BenchmarkTelemetry
667
+ currentInvocationId?: string
694
668
  ): Promise<AdvisorGateOutcome> => {
695
- telemetry?.advisorStart({ model: advisorRef, question, trigger });
696
669
  try {
697
670
  const result = await collectAdvisorResponse(
698
671
  ctx,
@@ -705,20 +678,11 @@ export const runAdvisorGate = async (
705
678
  undefined,
706
679
  undefined,
707
680
  onScout,
708
- currentInvocationId,
709
- telemetry
681
+ currentInvocationId
710
682
  );
711
683
  const parsed = parseAutomaticDecision(result.markdown);
712
- telemetry?.advisorEnd({
713
- model: result.model,
714
- outcome: parsed.ok ? `decision:${parsed.decision}` : parsed.category,
715
- question,
716
- response: result.markdown,
717
- trigger,
718
- usage: result.usage,
719
- });
720
684
  if (!parsed.ok) {
721
- return parsed;
685
+ return { ...parsed, usage: result.usage };
722
686
  }
723
687
  return {
724
688
  ...parsed,
@@ -728,12 +692,6 @@ export const runAdvisorGate = async (
728
692
  usage: result.usage,
729
693
  };
730
694
  } catch (error) {
731
- telemetry?.advisorError({
732
- category: signal?.aborted ? "cancelled" : "provider-error",
733
- model: advisorRef,
734
- question,
735
- trigger,
736
- });
737
695
  if (signal?.aborted) {
738
696
  throw error;
739
697
  }
@@ -749,6 +707,15 @@ export const runAdvisorGate = async (
749
707
  }
750
708
  };
751
709
 
710
+ const updateAdvisorUsageStatus = (
711
+ ctx: ExtensionContext,
712
+ session: AdvisorSessionState
713
+ ) => {
714
+ if (ctx.hasUI) {
715
+ ctx.ui.setStatus("advisor-usage", session.usageStatus());
716
+ }
717
+ };
718
+
752
719
  const notifyLocalFailure = (
753
720
  ctx: ExtensionContext,
754
721
  message: string,
@@ -814,7 +781,7 @@ const reserveAdvisorCall = (
814
781
  if (event.toolName !== "ask_advisor" || isSimpleMode()) {
815
782
  return;
816
783
  }
817
- if (!session.canConsult(advisorMaxCallsPerSessionRef)) {
784
+ if (!session.canConsult(getAdvisorMaxCallsPerSession())) {
818
785
  const message = "Advisor call budget exhausted for this session.";
819
786
  if (ctx.hasUI) {
820
787
  ctx.ui.notify(message, "warning");
@@ -840,12 +807,20 @@ const sendAutomaticGateCall = (pi: ExtensionAPI, event: ToolCallEvent) => {
840
807
  );
841
808
  };
842
809
 
843
- const sendAutomaticGateFailure = (pi: ExtensionAPI, markdown: string) => {
810
+ const sendAutomaticGateFailure = (
811
+ pi: ExtensionAPI,
812
+ markdown: string,
813
+ usage?: unknown
814
+ ) => {
815
+ const normalizedUsage = snapshotAdvisorUsage(usage);
844
816
  pi.sendMessage(
845
817
  {
846
818
  content: markdown,
847
819
  customType: "advisor-loop-result",
848
- details: { text: markdown },
820
+ details: {
821
+ text: markdown,
822
+ ...(normalizedUsage ? { usage: normalizedUsage } : {}),
823
+ },
849
824
  display: true,
850
825
  },
851
826
  { deliverAs: "steer" }
@@ -864,6 +839,9 @@ const sendAutomaticGateResult = (
864
839
  advisor: result.model,
865
840
  decision: result.decision,
866
841
  text: result.markdown,
842
+ ...(snapshotAdvisorUsage(result.usage)
843
+ ? { usage: snapshotAdvisorUsage(result.usage) }
844
+ : {}),
867
845
  },
868
846
  display: true,
869
847
  },
@@ -877,8 +855,7 @@ const handleAutomaticGate = async (
877
855
  ctx: ExtensionContext,
878
856
  session: AdvisorSessionState,
879
857
  runGate: typeof runAdvisorGate,
880
- scoutStatus: ScoutStatusManager,
881
- telemetry?: BenchmarkTelemetry
858
+ scoutStatus: ScoutStatusManager
882
859
  ): Promise<ToolCallEventResult | undefined> => {
883
860
  if (
884
861
  isSimpleMode() ||
@@ -893,7 +870,7 @@ const handleAutomaticGate = async (
893
870
  return;
894
871
  }
895
872
  const reason = `Advisor loop gate: normalized signature for ${event.toolName} repeated ${advisorLoopThresholdRef} times without a materially different tool action.`;
896
- if (!session.canConsult(advisorMaxCallsPerSessionRef)) {
873
+ if (!session.canConsult(getAdvisorMaxCallsPerSession())) {
897
874
  const failure = failureEffect(
898
875
  "budget-exhausted",
899
876
  "Advisor gate call budget is exhausted.",
@@ -931,8 +908,7 @@ const handleAutomaticGate = async (
931
908
  ensureGateCall();
932
909
  }
933
910
  },
934
- event.toolCallId,
935
- telemetry
911
+ event.toolCallId
936
912
  );
937
913
  ensureGateCall();
938
914
  if (!result.ok) {
@@ -942,7 +918,9 @@ const handleAutomaticGate = async (
942
918
  kind: "gate",
943
919
  model: advisorRef,
944
920
  trigger: "repeated-tool-call",
921
+ usage: result.usage,
945
922
  });
923
+ updateAdvisorUsageStatus(ctx, session);
946
924
  const failure = failureEffect(
947
925
  result.category,
948
926
  result.message,
@@ -951,7 +929,8 @@ const handleAutomaticGate = async (
951
929
  );
952
930
  sendAutomaticGateFailure(
953
931
  pi,
954
- `**Advisor gate failure (${result.category}):** ${result.message}`
932
+ `**Advisor gate failure (${result.category}):** ${result.message}`,
933
+ result.usage
955
934
  );
956
935
  return failure.block
957
936
  ? { block: true, reason: `${reason}\n${failure.reason}` }
@@ -966,6 +945,7 @@ const handleAutomaticGate = async (
966
945
  trigger: result.trigger,
967
946
  usage: result.usage,
968
947
  });
948
+ updateAdvisorUsageStatus(ctx, session);
969
949
  sendAutomaticGateResult(pi, result);
970
950
  if (result.decision === "proceed") {
971
951
  session.resetRepetition();
@@ -1011,6 +991,7 @@ interface AdvisorToolDetails {
1011
991
  thinking?: string;
1012
992
  trackedBytes?: number;
1013
993
  untrackedBytes?: number;
994
+ usage?: unknown;
1014
995
  }
1015
996
  interface AdvisorRenderState {
1016
997
  phase?: string;
@@ -1059,7 +1040,7 @@ export const scoutDetailsFromEvent = (
1059
1040
  selectedLabels: outcome.selectedLabels,
1060
1041
  status: "curated",
1061
1042
  synthesis: outcome.selection.synthesis,
1062
- usage: outcome.metrics.usage,
1043
+ usage: snapshotAdvisorUsage(outcome.metrics.usage),
1063
1044
  }
1064
1045
  : {
1065
1046
  availableCount: outcome.metrics.availableCount,
@@ -1069,7 +1050,7 @@ export const scoutDetailsFromEvent = (
1069
1050
  omittedBeforeScout: outcome.metrics.omittedBeforeScout,
1070
1051
  selectedCount: 0,
1071
1052
  status: "fallback",
1072
- usage: outcome.metrics.usage,
1053
+ usage: snapshotAdvisorUsage(outcome.metrics.usage),
1073
1054
  };
1074
1055
  };
1075
1056
 
@@ -1173,6 +1154,10 @@ export const renderScoutDetails = (
1173
1154
  ` ${scout.model}${scout.selectedCount === undefined ? "" : ` · ${scout.selectedCount} kept / ${Math.max(0, (scout.availableCount ?? 0) - scout.selectedCount)} omitted`}${scout.latencyMs === undefined ? "" : ` · ${(scout.latencyMs / 1000).toFixed(1)}s`}`
1174
1155
  ),
1175
1156
  ];
1157
+ const usage = formatAdvisorUsage(scout.usage);
1158
+ if (usage) {
1159
+ lines.push(theme.fg("dim", ` Usage: ${usage}`));
1160
+ }
1176
1161
  if (scout.fallbackReason) {
1177
1162
  lines.push(theme.fg("warning", ` ${scout.fallbackReason}`));
1178
1163
  }
@@ -1293,6 +1278,10 @@ const renderFinalAdvisorResult = (
1293
1278
  if (details?.advisor) {
1294
1279
  lines.push(theme.fg("dim", ` ${details.advisor}`));
1295
1280
  }
1281
+ const usage = formatAdvisorUsage(details?.usage);
1282
+ if (usage) {
1283
+ lines.push(theme.fg("dim", ` Usage: ${usage}`));
1284
+ }
1296
1285
  const attachments = [
1297
1286
  details?.draftBytes
1298
1287
  ? `Draft attached · ${details.draftBytes} B`
@@ -1357,7 +1346,6 @@ export const registerAdvisorTool = (
1357
1346
  dependencies: {
1358
1347
  runGate?: typeof runAdvisorGate;
1359
1348
  statusManager?: ScoutStatusManager;
1360
- telemetry?: BenchmarkTelemetry;
1361
1349
  } = {}
1362
1350
  ) => {
1363
1351
  const reservedCalls = new Set<string>();
@@ -1385,7 +1373,12 @@ export const registerAdvisorTool = (
1385
1373
  "advisor-loop-result",
1386
1374
  (message, { expanded }, theme) => {
1387
1375
  const details = message.details as
1388
- | { decision?: GateDecision; text?: string; advisor?: string }
1376
+ | {
1377
+ advisor?: string;
1378
+ decision?: GateDecision;
1379
+ text?: string;
1380
+ usage?: unknown;
1381
+ }
1389
1382
  | undefined;
1390
1383
  const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
1391
1384
  box.addChild(
@@ -1401,6 +1394,10 @@ export const registerAdvisorTool = (
1401
1394
  if (details?.advisor) {
1402
1395
  box.addChild(new Text(theme.fg("dim", ` ${details.advisor}`), 0, 0));
1403
1396
  }
1397
+ const usage = formatAdvisorUsage(details?.usage);
1398
+ if (usage) {
1399
+ box.addChild(new Text(theme.fg("dim", ` Usage: ${usage}`), 0, 0));
1400
+ }
1404
1401
  if (details?.text) {
1405
1402
  box.addChild(
1406
1403
  new Markdown(
@@ -1428,10 +1425,13 @@ export const registerAdvisorTool = (
1428
1425
  }
1429
1426
  );
1430
1427
 
1431
- pi.on("session_start", () => {
1428
+ pi.on("session_start", (_event, ctx) => {
1432
1429
  session.resetTask();
1433
1430
  reservedCalls.clear();
1434
1431
  herdrAdvisorBlock.clear();
1432
+ if (ctx?.hasUI) {
1433
+ ctx.ui.setStatus("advisor-usage", undefined);
1434
+ }
1435
1435
  });
1436
1436
 
1437
1437
  pi.on("before_agent_start", (_event, ctx) => {
@@ -1442,7 +1442,7 @@ export const registerAdvisorTool = (
1442
1442
  const guidelines = advisorInvocationGuidelines();
1443
1443
  const budget = isSimpleMode()
1444
1444
  ? undefined
1445
- : session.remainingCalls(advisorMaxCallsPerSessionRef);
1445
+ : session.remainingCalls(getAdvisorMaxCallsPerSession());
1446
1446
  if (budget !== undefined) {
1447
1447
  guidelines.push(
1448
1448
  `Advisor calls remaining this session: ${budget}.\nReserve calls for material decisions, repeated failures, or final review.`
@@ -1476,8 +1476,7 @@ export const registerAdvisorTool = (
1476
1476
  ctx,
1477
1477
  session,
1478
1478
  dependencies.runGate ?? runAdvisorGate,
1479
- scoutStatus,
1480
- dependencies.telemetry
1479
+ scoutStatus
1481
1480
  );
1482
1481
  });
1483
1482
 
@@ -1488,7 +1487,7 @@ export const registerAdvisorTool = (
1488
1487
  if (isSimpleMode() || session.blocked || !advisorSessionSummaryRef) {
1489
1488
  return;
1490
1489
  }
1491
- const summary = session.summary(advisorMaxCallsPerSessionRef);
1490
+ const summary = session.summary(getAdvisorMaxCallsPerSession());
1492
1491
  if (summary && ctx.hasUI) {
1493
1492
  ctx.ui.notify(summary, "info");
1494
1493
  }
@@ -1498,11 +1497,15 @@ export const registerAdvisorTool = (
1498
1497
  reservedCalls.clear();
1499
1498
  scoutStatus.clear(ctx);
1500
1499
  herdrAdvisorBlock.clear();
1500
+ if (ctx?.hasUI) {
1501
+ ctx.ui.setStatus("advisor-usage", undefined);
1502
+ }
1501
1503
  });
1502
1504
 
1503
1505
  pi.registerTool({
1504
1506
  description:
1505
1507
  "Consult the on-demand Advisor model for strategic guidance. Call with an empty object for a contextual review; attach an optional draft for concrete plan or completion review. If the Advisor explicitly names a missing file, you may make a sequential follow-up call with includeTrackedFiles when enabled and relevant.",
1508
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: usage and consent paths remain explicit at the tool boundary.
1506
1509
  async execute(_id, params, signal, onUpdate, ctx) {
1507
1510
  reservedCalls.delete(_id);
1508
1511
  if (
@@ -1514,7 +1517,7 @@ export const registerAdvisorTool = (
1514
1517
  );
1515
1518
  }
1516
1519
  if (!isSimpleMode()) {
1517
- if (!session.canConsult(advisorMaxCallsPerSessionRef)) {
1520
+ if (!session.canConsult(getAdvisorMaxCallsPerSession())) {
1518
1521
  throw new Error("Advisor call budget exhausted for this session.");
1519
1522
  }
1520
1523
  session.consumeCall();
@@ -1554,8 +1557,7 @@ export const registerAdvisorTool = (
1554
1557
  },
1555
1558
  });
1556
1559
  },
1557
- _id,
1558
- dependencies.telemetry
1560
+ _id
1559
1561
  );
1560
1562
  session.issueAdvice(
1561
1563
  result.adviceId,
@@ -1571,6 +1573,11 @@ export const registerAdvisorTool = (
1571
1573
  trigger: "executor-requested",
1572
1574
  usage: result.usage,
1573
1575
  });
1576
+ const usage = snapshotAdvisorUsage(result.usage);
1577
+ const piUsage = advisorUsageForPi(result.usage);
1578
+ if (ctx.hasUI) {
1579
+ ctx.ui.setStatus("advisor-usage", session.usageStatus());
1580
+ }
1574
1581
  return {
1575
1582
  content: [
1576
1583
  {
@@ -1589,7 +1596,9 @@ export const registerAdvisorTool = (
1589
1596
  thinking: result.thinkingText,
1590
1597
  trackedBytes: result.trackedBytes,
1591
1598
  untrackedBytes: result.untrackedBytes,
1599
+ ...(usage ? { usage } : {}),
1592
1600
  },
1601
+ ...(piUsage ? { usage: piUsage } : {}),
1593
1602
  };
1594
1603
  } catch (error) {
1595
1604
  const message = error instanceof Error ? error.message : String(error);
@@ -1600,6 +1609,9 @@ export const registerAdvisorTool = (
1600
1609
  model: advisorRef,
1601
1610
  trigger: "executor-requested",
1602
1611
  });
1612
+ if (ctx.hasUI) {
1613
+ ctx.ui.setStatus("advisor-usage", session.usageStatus());
1614
+ }
1603
1615
  notifyLocalFailure(ctx, message);
1604
1616
  notifyHerdrAdvisorFailure("Advisor consultation failed", message);
1605
1617
  throw error;
package/src/usage.ts ADDED
@@ -0,0 +1,199 @@
1
+ import type { Usage } from "@earendil-works/pi-ai/compat";
2
+
3
+ /** Normalized usage returned by an Advisor or Scout provider response. */
4
+ export interface AdvisorUsageSnapshot {
5
+ cacheRead?: number;
6
+ cacheWrite?: number;
7
+ cost?: number;
8
+ input?: number;
9
+ output?: number;
10
+ totalTokens?: number;
11
+ }
12
+
13
+ /** Session-local totals for direct Advisor model requests. */
14
+ export interface AdvisorUsageTotals {
15
+ cacheRead?: number;
16
+ cacheWrite?: number;
17
+ calls: number;
18
+ cost?: number;
19
+ costCalls: number;
20
+ input?: number;
21
+ knownCalls: number;
22
+ output?: number;
23
+ totalTokens?: number;
24
+ }
25
+
26
+ const finite = (value: unknown) =>
27
+ typeof value === "number" && Number.isFinite(value) && value >= 0
28
+ ? value
29
+ : undefined;
30
+
31
+ const add = (left: number | undefined, right: number | undefined) =>
32
+ left === undefined || right === undefined ? (left ?? right) : left + right;
33
+
34
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
35
+ Boolean(value) && typeof value === "object" && !Array.isArray(value);
36
+
37
+ const costFields = ["input", "output", "cacheRead", "cacheWrite", "total"];
38
+
39
+ /** Extracts provider-agnostic usage fields without trusting provider metadata. */
40
+ export const snapshotAdvisorUsage = (
41
+ usage: unknown
42
+ ): AdvisorUsageSnapshot | undefined => {
43
+ if (!isRecord(usage)) {
44
+ return undefined;
45
+ }
46
+ const cost = isRecord(usage.cost) ? usage.cost : undefined;
47
+ const snapshot = {
48
+ cacheRead: finite(usage.cacheRead),
49
+ cacheWrite: finite(usage.cacheWrite),
50
+ cost: finite(cost?.total) ?? finite(usage.totalCost) ?? finite(usage.cost),
51
+ input: finite(usage.input),
52
+ output: finite(usage.output),
53
+ totalTokens: finite(usage.totalTokens),
54
+ } satisfies AdvisorUsageSnapshot;
55
+ const hasCostField = costFields.some(
56
+ (field) => finite(cost?.[field]) !== undefined
57
+ );
58
+ return Object.values(snapshot).some((value) => value !== undefined) ||
59
+ hasCostField
60
+ ? snapshot
61
+ : undefined;
62
+ };
63
+
64
+ /** Returns the reported provider cost, when the response includes one. */
65
+ export const advisorUsageCost = (usage: unknown): number | undefined =>
66
+ snapshotAdvisorUsage(usage)?.cost;
67
+
68
+ /**
69
+ * Converts supported provider usage to Pi's complete nested-tool usage shape.
70
+ * Missing fields become zero only at this Pi API boundary; absent usage remains
71
+ * undefined so an unavailable request is never presented as a zero-cost call.
72
+ */
73
+ export const advisorUsageForPi = (usage: unknown): Usage | undefined => {
74
+ const snapshot = snapshotAdvisorUsage(usage);
75
+ if (!(snapshot && isRecord(usage))) {
76
+ return undefined;
77
+ }
78
+ const cost = isRecord(usage.cost) ? usage.cost : undefined;
79
+ const input = snapshot.input ?? 0;
80
+ const output = snapshot.output ?? 0;
81
+ const cacheRead = snapshot.cacheRead ?? 0;
82
+ const cacheWrite = snapshot.cacheWrite ?? 0;
83
+ const cacheWrite1h = finite(usage.cacheWrite1h);
84
+ const reasoning = finite(usage.reasoning);
85
+ return {
86
+ cacheRead,
87
+ cacheWrite,
88
+ ...(cacheWrite1h === undefined ? {} : { cacheWrite1h }),
89
+ ...(reasoning === undefined ? {} : { reasoning }),
90
+ cost: {
91
+ cacheRead: finite(cost?.cacheRead) ?? 0,
92
+ cacheWrite: finite(cost?.cacheWrite) ?? 0,
93
+ input: finite(cost?.input) ?? 0,
94
+ output: finite(cost?.output) ?? 0,
95
+ total: snapshot.cost ?? 0,
96
+ },
97
+ input,
98
+ output,
99
+ totalTokens:
100
+ snapshot.totalTokens ?? input + output + cacheRead + cacheWrite,
101
+ };
102
+ };
103
+
104
+ /** Creates empty totals without treating absent usage as zero usage. */
105
+ export const emptyAdvisorUsageTotals = (): AdvisorUsageTotals => ({
106
+ calls: 0,
107
+ costCalls: 0,
108
+ knownCalls: 0,
109
+ });
110
+
111
+ /** Adds one direct Advisor response to session-local usage totals. */
112
+ export const addAdvisorUsage = (totals: AdvisorUsageTotals, usage: unknown) => {
113
+ totals.calls += 1;
114
+ const snapshot = snapshotAdvisorUsage(usage);
115
+ if (!snapshot) {
116
+ return;
117
+ }
118
+ totals.knownCalls += 1;
119
+ totals.cacheRead = add(totals.cacheRead, snapshot.cacheRead);
120
+ totals.cacheWrite = add(totals.cacheWrite, snapshot.cacheWrite);
121
+ totals.input = add(totals.input, snapshot.input);
122
+ totals.output = add(totals.output, snapshot.output);
123
+ totals.totalTokens = add(totals.totalTokens, snapshot.totalTokens);
124
+ if (snapshot.cost !== undefined) {
125
+ totals.cost = add(totals.cost, snapshot.cost);
126
+ totals.costCalls += 1;
127
+ }
128
+ };
129
+
130
+ const formatTokens = (value: number) => {
131
+ if (value < 1000) {
132
+ return String(value);
133
+ }
134
+ if (value < 10_000) {
135
+ return `${(value / 1000).toFixed(1)}k`;
136
+ }
137
+ if (value < 1_000_000) {
138
+ return `${Math.round(value / 1000)}k`;
139
+ }
140
+ if (value < 10_000_000) {
141
+ return `${(value / 1_000_000).toFixed(1)}M`;
142
+ }
143
+ return `${Math.round(value / 1_000_000)}M`;
144
+ };
145
+
146
+ const formatCost = (value: number) => `$${value.toFixed(4)}`;
147
+
148
+ const formatUsageFields = (usage: AdvisorUsageSnapshot): string | undefined => {
149
+ const tokens = [
150
+ usage.input === undefined ? undefined : `↑${formatTokens(usage.input)}`,
151
+ usage.output === undefined ? undefined : `↓${formatTokens(usage.output)}`,
152
+ usage.cacheRead === undefined
153
+ ? undefined
154
+ : `cr:${formatTokens(usage.cacheRead)}`,
155
+ usage.cacheWrite === undefined
156
+ ? undefined
157
+ : `cw:${formatTokens(usage.cacheWrite)}`,
158
+ ].filter((value): value is string => value !== undefined);
159
+ if (tokens.length === 0 && usage.totalTokens !== undefined) {
160
+ tokens.push(`tokens:${formatTokens(usage.totalTokens)}`);
161
+ }
162
+ if (usage.cost !== undefined) {
163
+ tokens.push(formatCost(usage.cost));
164
+ }
165
+ return tokens.join(" · ") || undefined;
166
+ };
167
+
168
+ /** Formats one response for an Advisor or Scout result renderer. */
169
+ export const formatAdvisorUsage = (usage: unknown): string | undefined => {
170
+ const snapshot = snapshotAdvisorUsage(usage);
171
+ return snapshot ? formatUsageFields(snapshot) : undefined;
172
+ };
173
+
174
+ /** Formats cumulative direct Advisor usage for a footer or session summary. */
175
+ export const formatAdvisorUsageTotals = (
176
+ totals: AdvisorUsageTotals
177
+ ): string => {
178
+ const usage = formatUsageFields(totals);
179
+ const missing = totals.calls - totals.knownCalls;
180
+ const parts = [
181
+ usage,
182
+ missing > 0 ? `${missing} without usage data` : undefined,
183
+ ];
184
+ return (
185
+ parts.filter((value): value is string => value !== undefined).join(" · ") ||
186
+ "unavailable"
187
+ );
188
+ };
189
+
190
+ /** Formats the current direct Advisor usage status for the Pi footer. */
191
+ export const formatAdvisorUsageStatus = (
192
+ totals: AdvisorUsageTotals
193
+ ): string | undefined => {
194
+ if (totals.calls === 0) {
195
+ return undefined;
196
+ }
197
+ const label = `Advisor: ${totals.calls} call${totals.calls === 1 ? "" : "s"}`;
198
+ return `${label} · ${formatAdvisorUsageTotals(totals)}`;
199
+ };