pi-advisor-flow 0.3.6 → 0.5.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,8 @@ import {
35
34
  advisorUntrackedContentRef,
36
35
  contextMaxCharsRef,
37
36
  executorRef,
37
+ getAdvisorMaxCallsPerSession,
38
+ getAdvisorSettings,
38
39
  isSimpleMode,
39
40
  loadConfig,
40
41
  } from "./config.js";
@@ -57,7 +58,12 @@ import {
57
58
  herdrAdvisorBlock,
58
59
  notifyHerdrAdvisorFailure,
59
60
  } from "./herdr.js";
60
- import { collectTextStream, resolveConfiguredModel } from "./model-stream.js";
61
+ import {
62
+ ADVISOR_STREAM_UPDATE_INTERVAL_MS,
63
+ collectTextStream,
64
+ createCoalescedUpdate,
65
+ resolveConfiguredModel,
66
+ } from "./model-stream.js";
61
67
  import { ADOPTIONS, appendOutcome, VALIDATIONS } from "./outcomes.js";
62
68
  import { readProjectPreferences } from "./preferences.js";
63
69
  import {
@@ -68,6 +74,7 @@ import {
68
74
  import {
69
75
  buildScoutManifest,
70
76
  reconstructScoutConversation,
77
+ SCOUT_MANIFEST_MAX_BYTES,
71
78
  } from "./scout-context.js";
72
79
  import {
73
80
  AdvisorSessionState,
@@ -75,8 +82,13 @@ import {
75
82
  type GateDecision,
76
83
  type GateTrigger,
77
84
  } from "./session-state.js";
78
- import type { BenchmarkTelemetry } from "./telemetry.js";
79
85
  import { readTrackedFiles, readUntrackedFiles } from "./untracked.js";
86
+ import {
87
+ advisorUsageCost,
88
+ advisorUsageForPi,
89
+ formatAdvisorUsage,
90
+ snapshotAdvisorUsage,
91
+ } from "./usage.js";
80
92
 
81
93
  export type {
82
94
  AdvisorInvocationRecord,
@@ -302,6 +314,7 @@ export interface AdvisorGateFailure {
302
314
  markdown?: string;
303
315
  message: string;
304
316
  ok: false;
317
+ usage?: unknown;
305
318
  }
306
319
  export interface AdvisorConsultationResult {
307
320
  adviceId: string;
@@ -327,14 +340,6 @@ export interface AdvisorGateResult {
327
340
  }
328
341
  export type AdvisorGateOutcome = AdvisorGateResult | AdvisorGateFailure;
329
342
 
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
343
  const DECISION_LINE = /^Decision\s*:\s*(proceed|revise|blocked)\s*$/i;
339
344
  const CODE_FENCE = /^(?:```|~~~)/;
340
345
  const LINE_BREAK = /\r?\n/;
@@ -431,8 +436,7 @@ export const curateAdvisorConversation = async (
431
436
  enabled = advisorScoutEnabledRef,
432
437
  runScout: typeof runAdvisorScout = runAdvisorScout,
433
438
  currentInvocationId?: string,
434
- maxChars?: number,
435
- telemetry?: BenchmarkTelemetry
439
+ maxChars?: number
436
440
  ): Promise<{
437
441
  conversation: string;
438
442
  scout?: Exclude<ScoutOutcome, { cancelled: true }>;
@@ -445,7 +449,8 @@ export const curateAdvisorConversation = async (
445
449
  }
446
450
  const built = buildScoutManifest(ctx, {
447
451
  currentInvocationId,
448
- maxBytes: maxChars,
452
+ maxConversationChars: maxChars,
453
+ maxManifestBytes: SCOUT_MANIFEST_MAX_BYTES,
449
454
  });
450
455
  if (!built.ok) {
451
456
  const scout: Exclude<ScoutOutcome, { cancelled: true }> = {
@@ -470,8 +475,7 @@ export const curateAdvisorConversation = async (
470
475
  signal,
471
476
  onScout,
472
477
  undefined,
473
- undefined,
474
- telemetry
478
+ undefined
475
479
  );
476
480
  if (!outcome.ok && outcome.cancelled) {
477
481
  throw signal?.reason instanceof Error
@@ -504,8 +508,7 @@ const collectAdvisorResponse = async (
504
508
  includeUntracked?: string[],
505
509
  includeTracked?: string[],
506
510
  onScout?: (event: ScoutLifecycleEvent) => void,
507
- currentInvocationId?: string,
508
- telemetry?: BenchmarkTelemetry
511
+ currentInvocationId?: string
509
512
  ) => {
510
513
  loadConfig(ctx);
511
514
  const resolved = await resolveConfiguredModel(ctx, advisorRef, "Advisor");
@@ -551,8 +554,7 @@ const collectAdvisorResponse = async (
551
554
  advisorScoutEnabledRef,
552
555
  runAdvisorScout,
553
556
  currentInvocationId,
554
- conversationBudget,
555
- telemetry
557
+ conversationBudget
556
558
  );
557
559
  const { conversation, scout } = curated;
558
560
  const preferences = await readProjectPreferences(
@@ -643,43 +645,22 @@ export const consultAdvisor = async (
643
645
  includeUntracked?: string[],
644
646
  includeTracked?: string[],
645
647
  onScout?: (event: ScoutLifecycleEvent) => void,
646
- currentInvocationId?: string,
647
- telemetry?: BenchmarkTelemetry
648
+ currentInvocationId?: string
648
649
  ): 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
- }
650
+ const result = await collectAdvisorResponse(
651
+ ctx,
652
+ ADVISOR_SYSTEM,
653
+ question,
654
+ signal,
655
+ onChunk,
656
+ gitContext,
657
+ draft,
658
+ includeUntracked,
659
+ includeTracked,
660
+ onScout,
661
+ currentInvocationId
662
+ );
663
+ return { ...result, adviceId: randomUUID(), trigger };
683
664
  };
684
665
 
685
666
  export const runAdvisorGate = async (
@@ -689,10 +670,8 @@ export const runAdvisorGate = async (
689
670
  signal?: AbortSignal,
690
671
  onChunk?: (thinking: string, text: string) => void,
691
672
  onScout?: (event: ScoutLifecycleEvent) => void,
692
- currentInvocationId?: string,
693
- telemetry?: BenchmarkTelemetry
673
+ currentInvocationId?: string
694
674
  ): Promise<AdvisorGateOutcome> => {
695
- telemetry?.advisorStart({ model: advisorRef, question, trigger });
696
675
  try {
697
676
  const result = await collectAdvisorResponse(
698
677
  ctx,
@@ -705,20 +684,11 @@ export const runAdvisorGate = async (
705
684
  undefined,
706
685
  undefined,
707
686
  onScout,
708
- currentInvocationId,
709
- telemetry
687
+ currentInvocationId
710
688
  );
711
689
  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
690
  if (!parsed.ok) {
721
- return parsed;
691
+ return { ...parsed, usage: result.usage };
722
692
  }
723
693
  return {
724
694
  ...parsed,
@@ -728,12 +698,6 @@ export const runAdvisorGate = async (
728
698
  usage: result.usage,
729
699
  };
730
700
  } catch (error) {
731
- telemetry?.advisorError({
732
- category: signal?.aborted ? "cancelled" : "provider-error",
733
- model: advisorRef,
734
- question,
735
- trigger,
736
- });
737
701
  if (signal?.aborted) {
738
702
  throw error;
739
703
  }
@@ -749,6 +713,18 @@ export const runAdvisorGate = async (
749
713
  }
750
714
  };
751
715
 
716
+ const updateAdvisorUsageStatus = (
717
+ ctx: ExtensionContext,
718
+ session: AdvisorSessionState
719
+ ) => {
720
+ if (ctx.hasUI) {
721
+ ctx.ui.setStatus(
722
+ "advisor-usage",
723
+ getAdvisorSettings().showUsageFooter ? session.usageStatus() : undefined
724
+ );
725
+ }
726
+ };
727
+
752
728
  const notifyLocalFailure = (
753
729
  ctx: ExtensionContext,
754
730
  message: string,
@@ -773,12 +749,15 @@ export const gateFailureEffectForMode = (
773
749
  : ("session-blocked" as const);
774
750
  };
775
751
 
776
- const gateDecisionEffect = (decision: GateDecision) => {
752
+ const gateDecisionEffect = (
753
+ decision: GateDecision,
754
+ failureMode: "block-session" | "block-tool" | "warn-and-continue"
755
+ ) => {
777
756
  if (decision === "proceed") {
778
757
  return "continued" as const;
779
758
  }
780
759
  return decision === "blocked"
781
- ? ("session-blocked" as const)
760
+ ? gateFailureEffectForMode(failureMode)
782
761
  : ("tool-blocked" as const);
783
762
  };
784
763
 
@@ -786,15 +765,42 @@ const failureEffect = (
786
765
  category: GateFailureCategory,
787
766
  message: string,
788
767
  ctx: ExtensionContext,
789
- session: AdvisorSessionState
768
+ session: AdvisorSessionState,
769
+ failureMode: "block-session" | "block-tool" | "warn-and-continue"
790
770
  ) => {
791
771
  const reason = `Advisor gate ${category}: ${message}`;
792
- notifyLocalFailure(ctx, message, advisorFailureModeRef === "block-session");
772
+ notifyLocalFailure(ctx, message, failureMode === "block-session");
793
773
  notifyHerdrAdvisorFailure("Advisor gate failure", reason);
794
- if (advisorFailureModeRef === "warn-and-continue") {
774
+ if (failureMode === "warn-and-continue") {
775
+ return { block: false, effect: "continued" as const, reason };
776
+ }
777
+ if (failureMode === "block-tool") {
778
+ return { block: true, effect: "tool-blocked" as const, reason };
779
+ }
780
+ session.block(reason);
781
+ herdrAdvisorBlock.set(reason);
782
+ if (advisorBlockOnBlockedRef) {
783
+ ctx.abort();
784
+ }
785
+ return { block: true, effect: "session-blocked" as const, reason };
786
+ };
787
+
788
+ const blockedDecisionEffect = (
789
+ reason: string,
790
+ ctx: ExtensionContext,
791
+ session: AdvisorSessionState,
792
+ failureMode: "block-session" | "block-tool" | "warn-and-continue"
793
+ ) => {
794
+ if (failureMode === "warn-and-continue") {
795
+ if (ctx.hasUI) {
796
+ ctx.ui.notify(
797
+ "Advisor gate returned blocked; continuing by configuration.",
798
+ "warning"
799
+ );
800
+ }
795
801
  return { block: false, effect: "continued" as const, reason };
796
802
  }
797
- if (advisorFailureModeRef === "block-tool") {
803
+ if (failureMode === "block-tool") {
798
804
  return { block: true, effect: "tool-blocked" as const, reason };
799
805
  }
800
806
  session.block(reason);
@@ -814,7 +820,7 @@ const reserveAdvisorCall = (
814
820
  if (event.toolName !== "ask_advisor" || isSimpleMode()) {
815
821
  return;
816
822
  }
817
- if (!session.canConsult(advisorMaxCallsPerSessionRef)) {
823
+ if (!session.canConsult(getAdvisorMaxCallsPerSession())) {
818
824
  const message = "Advisor call budget exhausted for this session.";
819
825
  if (ctx.hasUI) {
820
826
  ctx.ui.notify(message, "warning");
@@ -840,12 +846,20 @@ const sendAutomaticGateCall = (pi: ExtensionAPI, event: ToolCallEvent) => {
840
846
  );
841
847
  };
842
848
 
843
- const sendAutomaticGateFailure = (pi: ExtensionAPI, markdown: string) => {
849
+ const sendAutomaticGateFailure = (
850
+ pi: ExtensionAPI,
851
+ markdown: string,
852
+ usage?: unknown
853
+ ) => {
854
+ const normalizedUsage = snapshotAdvisorUsage(usage);
844
855
  pi.sendMessage(
845
856
  {
846
857
  content: markdown,
847
858
  customType: "advisor-loop-result",
848
- details: { text: markdown },
859
+ details: {
860
+ text: markdown,
861
+ ...(normalizedUsage ? { usage: normalizedUsage } : {}),
862
+ },
849
863
  display: true,
850
864
  },
851
865
  { deliverAs: "steer" }
@@ -864,6 +878,9 @@ const sendAutomaticGateResult = (
864
878
  advisor: result.model,
865
879
  decision: result.decision,
866
880
  text: result.markdown,
881
+ ...(snapshotAdvisorUsage(result.usage)
882
+ ? { usage: snapshotAdvisorUsage(result.usage) }
883
+ : {}),
867
884
  },
868
885
  display: true,
869
886
  },
@@ -877,8 +894,7 @@ const handleAutomaticGate = async (
877
894
  ctx: ExtensionContext,
878
895
  session: AdvisorSessionState,
879
896
  runGate: typeof runAdvisorGate,
880
- scoutStatus: ScoutStatusManager,
881
- telemetry?: BenchmarkTelemetry
897
+ scoutStatus: ScoutStatusManager
882
898
  ): Promise<ToolCallEventResult | undefined> => {
883
899
  if (
884
900
  isSimpleMode() ||
@@ -893,12 +909,14 @@ const handleAutomaticGate = async (
893
909
  return;
894
910
  }
895
911
  const reason = `Advisor loop gate: normalized signature for ${event.toolName} repeated ${advisorLoopThresholdRef} times without a materially different tool action.`;
896
- if (!session.canConsult(advisorMaxCallsPerSessionRef)) {
912
+ const failureMode = advisorFailureModeRef;
913
+ if (!session.canConsult(getAdvisorMaxCallsPerSession())) {
897
914
  const failure = failureEffect(
898
915
  "budget-exhausted",
899
916
  "Advisor gate call budget is exhausted.",
900
917
  ctx,
901
- session
918
+ session,
919
+ failureMode
902
920
  );
903
921
  return failure.block ? { block: true, reason: failure.reason } : undefined;
904
922
  }
@@ -931,27 +949,30 @@ const handleAutomaticGate = async (
931
949
  ensureGateCall();
932
950
  }
933
951
  },
934
- event.toolCallId,
935
- telemetry
952
+ event.toolCallId
936
953
  );
937
954
  ensureGateCall();
938
955
  if (!result.ok) {
939
956
  session.recordInvocation({
940
- executionEffect: gateFailureEffectForMode(advisorFailureModeRef),
957
+ executionEffect: gateFailureEffectForMode(failureMode),
941
958
  failure: result.category,
942
959
  kind: "gate",
943
960
  model: advisorRef,
944
961
  trigger: "repeated-tool-call",
962
+ usage: result.usage,
945
963
  });
964
+ updateAdvisorUsageStatus(ctx, session);
946
965
  const failure = failureEffect(
947
966
  result.category,
948
967
  result.message,
949
968
  ctx,
950
- session
969
+ session,
970
+ failureMode
951
971
  );
952
972
  sendAutomaticGateFailure(
953
973
  pi,
954
- `**Advisor gate failure (${result.category}):** ${result.message}`
974
+ `**Advisor gate failure (${result.category}):** ${result.message}`,
975
+ result.usage
955
976
  );
956
977
  return failure.block
957
978
  ? { block: true, reason: `${reason}\n${failure.reason}` }
@@ -960,12 +981,13 @@ const handleAutomaticGate = async (
960
981
  session.recordInvocation({
961
982
  cost: advisorUsageCost(result.usage),
962
983
  decision: result.decision,
963
- executionEffect: gateDecisionEffect(result.decision),
984
+ executionEffect: gateDecisionEffect(result.decision, failureMode),
964
985
  kind: "gate",
965
986
  model: result.model,
966
987
  trigger: result.trigger,
967
988
  usage: result.usage,
968
989
  });
990
+ updateAdvisorUsageStatus(ctx, session);
969
991
  sendAutomaticGateResult(pi, result);
970
992
  if (result.decision === "proceed") {
971
993
  session.resetRepetition();
@@ -973,11 +995,13 @@ const handleAutomaticGate = async (
973
995
  }
974
996
  const gateReason = `Advisor loop review: ${result.markdown}`;
975
997
  if (result.decision === "blocked") {
976
- session.block(gateReason);
977
- herdrAdvisorBlock.set(gateReason);
978
- if (advisorBlockOnBlockedRef) {
979
- ctx.abort();
980
- }
998
+ const effect = blockedDecisionEffect(
999
+ gateReason,
1000
+ ctx,
1001
+ session,
1002
+ failureMode
1003
+ );
1004
+ return effect.block ? { block: true, reason: effect.reason } : undefined;
981
1005
  }
982
1006
  return { block: true, reason: gateReason };
983
1007
  } finally {
@@ -1011,6 +1035,7 @@ interface AdvisorToolDetails {
1011
1035
  thinking?: string;
1012
1036
  trackedBytes?: number;
1013
1037
  untrackedBytes?: number;
1038
+ usage?: unknown;
1014
1039
  }
1015
1040
  interface AdvisorRenderState {
1016
1041
  phase?: string;
@@ -1059,7 +1084,7 @@ export const scoutDetailsFromEvent = (
1059
1084
  selectedLabels: outcome.selectedLabels,
1060
1085
  status: "curated",
1061
1086
  synthesis: outcome.selection.synthesis,
1062
- usage: outcome.metrics.usage,
1087
+ usage: snapshotAdvisorUsage(outcome.metrics.usage),
1063
1088
  }
1064
1089
  : {
1065
1090
  availableCount: outcome.metrics.availableCount,
@@ -1069,7 +1094,7 @@ export const scoutDetailsFromEvent = (
1069
1094
  omittedBeforeScout: outcome.metrics.omittedBeforeScout,
1070
1095
  selectedCount: 0,
1071
1096
  status: "fallback",
1072
- usage: outcome.metrics.usage,
1097
+ usage: snapshotAdvisorUsage(outcome.metrics.usage),
1073
1098
  };
1074
1099
  };
1075
1100
 
@@ -1093,6 +1118,11 @@ export class ScoutStatusManager {
1093
1118
  readonly #active = new Set<symbol>();
1094
1119
  readonly #known = new Set<symbol>();
1095
1120
  readonly #retired = new Set<symbol>();
1121
+ private readonly showStatus: boolean;
1122
+
1123
+ constructor(showStatus = true) {
1124
+ this.showStatus = showStatus;
1125
+ }
1096
1126
 
1097
1127
  register(token: symbol) {
1098
1128
  if (!this.#retired.has(token)) {
@@ -1107,7 +1137,9 @@ export class ScoutStatusManager {
1107
1137
  this.#known.add(token);
1108
1138
  if (event.type === "call" || event.type === "chunk") {
1109
1139
  this.#active.add(token);
1110
- ctx.ui.setStatus("advisor-scout", "Scout curating…");
1140
+ if (this.showStatus) {
1141
+ ctx.ui.setStatus("advisor-scout", "Scout curating…");
1142
+ }
1111
1143
  return;
1112
1144
  }
1113
1145
  this.release(ctx, token);
@@ -1117,7 +1149,7 @@ export class ScoutStatusManager {
1117
1149
  this.#active.delete(token);
1118
1150
  this.#known.delete(token);
1119
1151
  this.#retired.add(token);
1120
- if (!ctx.hasUI) {
1152
+ if (!(ctx.hasUI && this.showStatus)) {
1121
1153
  return;
1122
1154
  }
1123
1155
  ctx.ui.setStatus(
@@ -1132,7 +1164,7 @@ export class ScoutStatusManager {
1132
1164
  }
1133
1165
  this.#known.clear();
1134
1166
  this.#active.clear();
1135
- if (ctx.hasUI) {
1167
+ if (ctx.hasUI && this.showStatus) {
1136
1168
  ctx.ui.setStatus("advisor-scout", undefined);
1137
1169
  }
1138
1170
  }
@@ -1173,6 +1205,12 @@ export const renderScoutDetails = (
1173
1205
  ` ${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
1206
  ),
1175
1207
  ];
1208
+ if (getAdvisorSettings().showUsageDetails) {
1209
+ const usage = formatAdvisorUsage(scout.usage);
1210
+ if (usage) {
1211
+ lines.push(theme.fg("dim", ` Usage: ${usage}`));
1212
+ }
1213
+ }
1176
1214
  if (scout.fallbackReason) {
1177
1215
  lines.push(theme.fg("warning", ` ${scout.fallbackReason}`));
1178
1216
  }
@@ -1293,6 +1331,12 @@ const renderFinalAdvisorResult = (
1293
1331
  if (details?.advisor) {
1294
1332
  lines.push(theme.fg("dim", ` ${details.advisor}`));
1295
1333
  }
1334
+ if (getAdvisorSettings().showUsageDetails) {
1335
+ const usage = formatAdvisorUsage(details?.usage);
1336
+ if (usage) {
1337
+ lines.push(theme.fg("dim", ` Usage: ${usage}`));
1338
+ }
1339
+ }
1296
1340
  const attachments = [
1297
1341
  details?.draftBytes
1298
1342
  ? `Draft attached · ${details.draftBytes} B`
@@ -1355,11 +1399,14 @@ export const registerAdvisorTool = (
1355
1399
  pi: ExtensionAPI,
1356
1400
  session: AdvisorSessionState = advisorSessionState,
1357
1401
  dependencies: {
1402
+ appendOutcome?: typeof appendOutcome;
1403
+ consult?: typeof consultAdvisor;
1358
1404
  runGate?: typeof runAdvisorGate;
1359
1405
  statusManager?: ScoutStatusManager;
1360
- telemetry?: BenchmarkTelemetry;
1361
1406
  } = {}
1362
1407
  ) => {
1408
+ const appendAdvisorOutcome = dependencies.appendOutcome ?? appendOutcome;
1409
+ const requestAdvisor = dependencies.consult ?? consultAdvisor;
1363
1410
  const reservedCalls = new Set<string>();
1364
1411
  const scoutStatus = dependencies.statusManager ?? new ScoutStatusManager();
1365
1412
 
@@ -1385,7 +1432,12 @@ export const registerAdvisorTool = (
1385
1432
  "advisor-loop-result",
1386
1433
  (message, { expanded }, theme) => {
1387
1434
  const details = message.details as
1388
- | { decision?: GateDecision; text?: string; advisor?: string }
1435
+ | {
1436
+ advisor?: string;
1437
+ decision?: GateDecision;
1438
+ text?: string;
1439
+ usage?: unknown;
1440
+ }
1389
1441
  | undefined;
1390
1442
  const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
1391
1443
  box.addChild(
@@ -1401,6 +1453,12 @@ export const registerAdvisorTool = (
1401
1453
  if (details?.advisor) {
1402
1454
  box.addChild(new Text(theme.fg("dim", ` ${details.advisor}`), 0, 0));
1403
1455
  }
1456
+ if (getAdvisorSettings().showUsageDetails) {
1457
+ const usage = formatAdvisorUsage(details?.usage);
1458
+ if (usage) {
1459
+ box.addChild(new Text(theme.fg("dim", ` Usage: ${usage}`), 0, 0));
1460
+ }
1461
+ }
1404
1462
  if (details?.text) {
1405
1463
  box.addChild(
1406
1464
  new Markdown(
@@ -1428,10 +1486,13 @@ export const registerAdvisorTool = (
1428
1486
  }
1429
1487
  );
1430
1488
 
1431
- pi.on("session_start", () => {
1489
+ pi.on("session_start", (_event, ctx) => {
1432
1490
  session.resetTask();
1433
1491
  reservedCalls.clear();
1434
1492
  herdrAdvisorBlock.clear();
1493
+ if (ctx?.hasUI) {
1494
+ ctx.ui.setStatus("advisor-usage", undefined);
1495
+ }
1435
1496
  });
1436
1497
 
1437
1498
  pi.on("before_agent_start", (_event, ctx) => {
@@ -1442,7 +1503,7 @@ export const registerAdvisorTool = (
1442
1503
  const guidelines = advisorInvocationGuidelines();
1443
1504
  const budget = isSimpleMode()
1444
1505
  ? undefined
1445
- : session.remainingCalls(advisorMaxCallsPerSessionRef);
1506
+ : session.remainingCalls(getAdvisorMaxCallsPerSession());
1446
1507
  if (budget !== undefined) {
1447
1508
  guidelines.push(
1448
1509
  `Advisor calls remaining this session: ${budget}.\nReserve calls for material decisions, repeated failures, or final review.`
@@ -1476,8 +1537,7 @@ export const registerAdvisorTool = (
1476
1537
  ctx,
1477
1538
  session,
1478
1539
  dependencies.runGate ?? runAdvisorGate,
1479
- scoutStatus,
1480
- dependencies.telemetry
1540
+ scoutStatus
1481
1541
  );
1482
1542
  });
1483
1543
 
@@ -1488,7 +1548,7 @@ export const registerAdvisorTool = (
1488
1548
  if (isSimpleMode() || session.blocked || !advisorSessionSummaryRef) {
1489
1549
  return;
1490
1550
  }
1491
- const summary = session.summary(advisorMaxCallsPerSessionRef);
1551
+ const summary = session.summary(getAdvisorMaxCallsPerSession());
1492
1552
  if (summary && ctx.hasUI) {
1493
1553
  ctx.ui.notify(summary, "info");
1494
1554
  }
@@ -1498,6 +1558,9 @@ export const registerAdvisorTool = (
1498
1558
  reservedCalls.clear();
1499
1559
  scoutStatus.clear(ctx);
1500
1560
  herdrAdvisorBlock.clear();
1561
+ if (ctx?.hasUI) {
1562
+ ctx.ui.setStatus("advisor-usage", undefined);
1563
+ }
1501
1564
  });
1502
1565
 
1503
1566
  pi.registerTool({
@@ -1514,20 +1577,31 @@ export const registerAdvisorTool = (
1514
1577
  );
1515
1578
  }
1516
1579
  if (!isSimpleMode()) {
1517
- if (!session.canConsult(advisorMaxCallsPerSessionRef)) {
1580
+ if (!session.canConsult(getAdvisorMaxCallsPerSession())) {
1518
1581
  throw new Error("Advisor call budget exhausted for this session.");
1519
1582
  }
1520
1583
  session.consumeCall();
1521
1584
  }
1522
1585
  herdrAdvisorActivity.start();
1523
1586
  let scoutDetails: ScoutToolDetails | undefined;
1587
+ const coalescedUpdate = createCoalescedUpdate(
1588
+ (update: Parameters<NonNullable<typeof onUpdate>>[0]) =>
1589
+ onUpdate?.(update),
1590
+ ADVISOR_STREAM_UPDATE_INTERVAL_MS
1591
+ );
1592
+ const flushUpdate = () => {
1593
+ const result = coalescedUpdate.flush();
1594
+ if (result.failed) {
1595
+ throw result.error;
1596
+ }
1597
+ };
1524
1598
  try {
1525
- const result = await consultAdvisor(
1599
+ const result = await requestAdvisor(
1526
1600
  ctx,
1527
1601
  resolveAdvisorRequest(params.question),
1528
1602
  signal,
1529
1603
  (t, tx) =>
1530
- onUpdate?.({
1604
+ coalescedUpdate.update({
1531
1605
  content: [{ text: tx, type: "text" }],
1532
1606
  details: {
1533
1607
  advisor: advisorRef,
@@ -1545,7 +1619,7 @@ export const registerAdvisorTool = (
1545
1619
  params.includeTrackedFiles,
1546
1620
  (event) => {
1547
1621
  scoutDetails = scoutDetailsFromEvent(event, scoutDetails);
1548
- onUpdate?.({
1622
+ coalescedUpdate.update({
1549
1623
  content: [{ text: scoutDetails.text ?? "", type: "text" }],
1550
1624
  details: {
1551
1625
  advisor: advisorRef,
@@ -1554,9 +1628,9 @@ export const registerAdvisorTool = (
1554
1628
  },
1555
1629
  });
1556
1630
  },
1557
- _id,
1558
- dependencies.telemetry
1631
+ _id
1559
1632
  );
1633
+ flushUpdate();
1560
1634
  session.issueAdvice(
1561
1635
  result.adviceId,
1562
1636
  result.markdown,
@@ -1571,6 +1645,9 @@ export const registerAdvisorTool = (
1571
1645
  trigger: "executor-requested",
1572
1646
  usage: result.usage,
1573
1647
  });
1648
+ const usage = snapshotAdvisorUsage(result.usage);
1649
+ const piUsage = advisorUsageForPi(result.usage);
1650
+ updateAdvisorUsageStatus(ctx, session);
1574
1651
  return {
1575
1652
  content: [
1576
1653
  {
@@ -1589,9 +1666,15 @@ export const registerAdvisorTool = (
1589
1666
  thinking: result.thinkingText,
1590
1667
  trackedBytes: result.trackedBytes,
1591
1668
  untrackedBytes: result.untrackedBytes,
1669
+ ...(usage ? { usage } : {}),
1592
1670
  },
1671
+ ...(piUsage ? { usage: piUsage } : {}),
1593
1672
  };
1594
1673
  } catch (error) {
1674
+ // Publish the latest partial state before surfacing a provider or
1675
+ // execution error. A failure from the UI sink must not replace the
1676
+ // original error because this path is also used for provider failures.
1677
+ coalescedUpdate.flush();
1595
1678
  const message = error instanceof Error ? error.message : String(error);
1596
1679
  session.recordInvocation({
1597
1680
  executionEffect: "continued",
@@ -1600,10 +1683,12 @@ export const registerAdvisorTool = (
1600
1683
  model: advisorRef,
1601
1684
  trigger: "executor-requested",
1602
1685
  });
1686
+ updateAdvisorUsageStatus(ctx, session);
1603
1687
  notifyLocalFailure(ctx, message);
1604
1688
  notifyHerdrAdvisorFailure("Advisor consultation failed", message);
1605
1689
  throw error;
1606
1690
  } finally {
1691
+ coalescedUpdate.cancel();
1607
1692
  herdrAdvisorActivity.finish();
1608
1693
  }
1609
1694
  },
@@ -1680,18 +1765,19 @@ export const registerAdvisorTool = (
1680
1765
  details: { recorded: false },
1681
1766
  };
1682
1767
  }
1683
- const advice = session.claimAdvice(params.adviceId);
1768
+ const advice = session.reserveAdvice(params.adviceId);
1684
1769
  if (!advice) {
1685
- throw new Error("Unknown or already recorded adviceId.");
1770
+ throw new Error("Unknown, already recorded, or pending adviceId.");
1686
1771
  }
1687
1772
  try {
1688
- await appendOutcome({
1773
+ await appendAdvisorOutcome({
1689
1774
  adoption: params.adoption as (typeof ADOPTIONS)[number],
1690
1775
  advice: advice.advice,
1691
1776
  trigger: advice.trigger,
1692
1777
  validationStatus:
1693
1778
  params.validationStatus as (typeof VALIDATIONS)[number],
1694
1779
  });
1780
+ session.commitAdvice(params.adviceId);
1695
1781
  return {
1696
1782
  content: [
1697
1783
  { text: "Advisor outcome recorded locally.", type: "text" },
@@ -1699,6 +1785,7 @@ export const registerAdvisorTool = (
1699
1785
  details: { recorded: true },
1700
1786
  };
1701
1787
  } catch {
1788
+ session.releaseAdvice(params.adviceId);
1702
1789
  if (ctx.hasUI) {
1703
1790
  ctx.ui.notify(
1704
1791
  "Advisor outcome could not be recorded locally.",