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/CHANGELOG.md CHANGED
@@ -4,6 +4,21 @@ All notable changes to this project are documented here.
4
4
 
5
5
  The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## 0.4.0
8
+
9
+ ### Added
10
+
11
+ - Exposed provider-reported Advisor and Scout token/cache/cost details per result, tracked cumulative direct Advisor usage in the session footer and optional summary, and attached normalized `ask_advisor` usage to Pi's built-in cost totals without double-counting manual consultations or automatic gates.
12
+
13
+ ### Removed
14
+
15
+ - Removed the repository-only Benchmark suite and its benchmark-specific telemetry instrumentation.
16
+
17
+ ### Fixed
18
+
19
+ - Kept Scout's serialized manifest limit separate from the Advisor conversation budget so required context does not fall back solely because of manifest metadata overhead.
20
+ - Made unlimited Advisor call budgets live at every enforcement boundary, preventing stale finite limits from triggering false Herdr budget notifications after switching to unlimited.
21
+
7
22
  ## 0.3.6
8
23
 
9
24
  ### Fixed
package/README.md CHANGED
@@ -21,6 +21,7 @@ The idea is simple: keep implementation on a fast model and borrow frontier reas
21
21
  - **Configurable review gates** before plans, after repeated failures, and before declaring completion.
22
22
  - **Automatic loop detection** for repeated tool calls, with explicit proceed, revise, or blocked decisions.
23
23
  - **Separate model and reasoning controls** for the Executor and Advisor.
24
+ - **Advisor usage accounting** with per-response token/cost details and cumulative direct usage in the Pi footer and session summary.
24
25
  - **Privacy controls** for conversation history, repository context, explicit tracked/untracked file handoff, tool results, secret redaction, and outcome logging.
25
26
  - **Optional persistent activation, Simple mode, session summaries, and Herdr integration.**
26
27
  - **EXPERIMENTAL Advisor Scout** that uses the configured Executor model to curate conversation evidence before every Advisor call.
@@ -55,7 +56,7 @@ Unknown fields in `advisor.json` are preserved for forward compatibility and rep
55
56
  You can also enable the flow and select both models at once:
56
57
 
57
58
  ```text
58
- /advisor executor=anthropic/claude-sonnet-5 advisor=openai/gpt-5.6-sol
59
+ /advisor executor=openai-codex/gpt-5.6-luna advisor=openai-codex/gpt-5.6-sol
59
60
  ```
60
61
 
61
62
  ## How it works
@@ -68,6 +69,8 @@ You can also enable the flow and select both models at once:
68
69
 
69
70
  A normal consultation never blocks execution. The optional automatic loop gate is different: it evaluates repeated tool calls and applies the configured failure policy when the Advisor says to revise, reports a block, is unavailable, or returns an invalid decision.
70
71
 
72
+ Advisor responses show provider-reported input, output, cache, and cost details when available. Successful `ask_advisor` tool results also carry normalized usage into Pi's built-in `Tools/summaries` and `/cost` totals. Manual consultations and automatic gates remain in the separate session-local direct Advisor accounting because they are custom messages, so they are not double-counted in Pi's Executor totals. Missing or partial provider usage is shown as unavailable rather than fabricated as zero usage.
73
+
71
74
  Successful calls return an opaque `adviceId`. If global outcome logging is enabled, the Executor can call `record_advisor_outcome` once to record whether the advice was adopted and whether final validation passed.
72
75
 
73
76
  ### Experimental Advisor Scout
@@ -76,7 +79,7 @@ Experimental Advisor Scout is off by default. Enable `Experimental Advisor Scout
76
79
 
77
80
  Scout runs before `ask_advisor`, `/advisor-manual`, and automatic Advisor gates. It uses the configured Executor model and Executor reasoning effort in a separate model call. This adds cost and latency, but can reduce cost in the Advisor call. The compact result shows the model, selection counts, and elapsed time; `Ctrl+O` shows bounded selected labels and the synthesis.
78
81
 
79
- Scout receives a bounded manifest of conversation and tool-history groups after the normal tool disclosure, result-cap, and redaction policies are applied. The manifest and reconstructed Scout conversation share the Advisor's remaining context budget after repository context; a zero remaining budget produces no history groups. For a pending `ask_advisor` call, Scout receives only the allowlisted question and Git-context preference, never the draft or explicit attachment paths. Scout does not receive the deterministic Git context, draft, project preferences, or explicit tracked and untracked attachments. Those regions are appended later through their existing consent and cap rules.
82
+ Scout receives a bounded manifest of conversation and tool-history groups after the normal tool disclosure, result-cap, and redaction policies are applied. The Scout manifest has its own fixed transport limit, while the reconstructed conversation remains bounded by the Advisor's remaining context budget after repository context; manifest metadata no longer consumes that Advisor conversation budget. A zero remaining budget produces no history groups. For a pending `ask_advisor` call, Scout receives only the allowlisted question and Git-context preference, never the draft or explicit attachment paths. Scout does not receive the deterministic Git context, draft, project preferences, or explicit tracked and untracked attachments. Those regions are appended later through their existing consent and cap rules.
80
83
 
81
84
  This experiment adapts the context-boundary idea from Zhang et al., ["FastContext: Training Efficient Repository Explorer for Coding Agents"](https://arxiv.org/html/2606.14066v1). It is not a reproduction of FastContext. pi-advisor Scout curates conversation history only.
82
85
 
@@ -2,7 +2,6 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { registerCommands } from "../src/commands.js";
3
3
  import { setHerdrBlockedEmitter } from "../src/herdr.js";
4
4
  import { AdvisorSessionState } from "../src/session-state.js";
5
- import { createBenchmarkTelemetry } from "../src/telemetry.js";
6
5
  import {
7
6
  consultAdvisor as consultAdvisorImplementation,
8
7
  parseAutomaticDecision as parseAutomaticDecisionImplementation,
@@ -37,20 +36,11 @@ export default function (pi: ExtensionAPI) {
37
36
  setHerdrBlockedEmitter((active, label) =>
38
37
  pi.events.emit("herdr:blocked", { active, label })
39
38
  );
40
- const benchmarkTelemetry = createBenchmarkTelemetry(pi.events);
41
- if (benchmarkTelemetry) {
42
- // Diagnostics only: never rewrite provider payloads or affect production sessions.
43
- pi.on("before_provider_request", (event) => {
44
- benchmarkTelemetry.providerRequest(event.payload);
45
- });
46
- }
47
39
  registerAdvisorTool(pi, sessionState, {
48
40
  statusManager: scoutStatus,
49
- telemetry: benchmarkTelemetry,
50
41
  });
51
42
  registerCommands(pi, {
52
43
  sessionState,
53
44
  statusManager: scoutStatus,
54
- telemetry: benchmarkTelemetry,
55
45
  });
56
46
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-advisor-flow",
3
- "version": "0.3.6",
3
+ "version": "0.4.0",
4
4
  "description": "Advanced Executor/Advisor flow for Pi, fully configurable and extendable.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -28,8 +28,6 @@
28
28
  "README.md"
29
29
  ],
30
30
  "scripts": {
31
- "benchmark": "bun benchmarks/src/cli.ts",
32
- "benchmark:report": "bun benchmarks/src/cli.ts report",
33
31
  "format": "bunx ultracite fix --linter-enabled=false",
34
32
  "lint": "bunx ultracite check",
35
33
  "lint:fix": "bunx ultracite fix",
package/src/commands.ts CHANGED
@@ -6,12 +6,12 @@ import {
6
6
  import { Box, Markdown, Text } from "@earendil-works/pi-tui";
7
7
  import {
8
8
  advisorEffortRef,
9
- advisorMaxCallsPerSessionRef,
10
9
  advisorRef,
11
10
  alwaysOnRef,
12
11
  contextMaxCharsRef,
13
12
  executorEffortRef,
14
13
  executorRef,
14
+ getAdvisorMaxCallsPerSession,
15
15
  getAdvisorSettings,
16
16
  isSimpleMode,
17
17
  loadConfig,
@@ -52,7 +52,6 @@ import {
52
52
  import { herdrAdvisorActivity, notifyHerdrAdvisorFailure } from "./herdr.js";
53
53
  import type { ScoutLifecycleEvent } from "./scout.js";
54
54
  import type { AdvisorSessionState } from "./session-state.js";
55
- import type { BenchmarkTelemetry } from "./telemetry.js";
56
55
  import {
57
56
  adviceForDisplay,
58
57
  appendScoutLifecycleEntry,
@@ -70,6 +69,11 @@ import {
70
69
  type ContextPreset,
71
70
  SearchableModelSelector,
72
71
  } from "./ui.js";
72
+ import {
73
+ advisorUsageCost,
74
+ formatAdvisorUsage,
75
+ snapshotAdvisorUsage,
76
+ } from "./usage.js";
73
77
 
74
78
  const DEFAULT_EFFORT_LEVEL = "Default (Model Default)";
75
79
  const SELECTED_PREFIX = "✓ ";
@@ -145,6 +149,7 @@ type ManualConsult = (
145
149
  thinkingText: string;
146
150
  draftBytes?: number;
147
151
  preferenceBytes?: number;
152
+ usage?: unknown;
148
153
  }>;
149
154
  type ThinkingLevel = Parameters<ExtensionAPI["setThinkingLevel"]>[0];
150
155
 
@@ -169,7 +174,6 @@ export const registerCommands = (
169
174
  consult?: ManualConsult;
170
175
  sessionState?: AdvisorSessionState;
171
176
  statusManager?: ScoutStatusManager;
172
- telemetry?: BenchmarkTelemetry;
173
177
  } = {}
174
178
  ) => {
175
179
  const advisorSessionState =
@@ -190,10 +194,14 @@ export const registerCommands = (
190
194
  undefined,
191
195
  undefined,
192
196
  onScout,
193
- undefined,
194
- dependencies.telemetry
197
+ undefined
195
198
  ));
196
199
  const manualConsultations = new Map<AbortController, symbol>();
200
+ const updateAdvisorUsageStatus = (ctx: ExtensionContext) => {
201
+ if (ctx.hasUI) {
202
+ ctx.ui.setStatus("advisor-usage", advisorSessionState.usageStatus());
203
+ }
204
+ };
197
205
  const setManualStatus = (
198
206
  ctx: ExtensionContext,
199
207
  controller: AbortController,
@@ -257,21 +265,30 @@ export const registerCommands = (
257
265
  }
258
266
  }
259
267
  )
260
- .then(({ markdown }) => {
268
+ .then(({ markdown, usage }) => {
261
269
  if (controller.signal.aborted) {
262
270
  return;
263
271
  }
264
272
  advisorSessionState.recordInvocation({
273
+ cost: advisorUsageCost(usage),
265
274
  executionEffect: "continued",
266
275
  kind: "markdown",
267
276
  model: advisorRef,
268
277
  trigger: "manual",
278
+ usage,
269
279
  });
280
+ updateAdvisorUsageStatus(ctx);
281
+ const normalizedUsage = snapshotAdvisorUsage(usage);
270
282
  pi.sendMessage(
271
283
  {
272
284
  content: `Manual Advisor consultation${question ? ` (${question})` : ""}:\n\n${markdown}`,
273
285
  customType: "advisor-manual-result",
274
- details: { advisor: advisorRef, question, text: markdown },
286
+ details: {
287
+ advisor: advisorRef,
288
+ question,
289
+ text: markdown,
290
+ ...(normalizedUsage ? { usage: normalizedUsage } : {}),
291
+ },
275
292
  display: true,
276
293
  },
277
294
  {
@@ -294,6 +311,7 @@ export const registerCommands = (
294
311
  model: advisorRef,
295
312
  trigger: "manual",
296
313
  });
314
+ updateAdvisorUsageStatus(ctx);
297
315
  pi.sendMessage(
298
316
  {
299
317
  content: `Manual Advisor consultation failed: ${message}`,
@@ -424,7 +442,7 @@ export const registerCommands = (
424
442
  "advisor-manual-result",
425
443
  (message, { expanded }, theme) => {
426
444
  const details = message.details as
427
- | { advisor?: string; text?: string }
445
+ | { advisor?: string; text?: string; usage?: unknown }
428
446
  | undefined;
429
447
  const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
430
448
  const advice =
@@ -443,6 +461,10 @@ export const registerCommands = (
443
461
  if (details?.advisor) {
444
462
  box.addChild(new Text(theme.fg("dim", ` ${details.advisor}`), 0, 0));
445
463
  }
464
+ const usage = formatAdvisorUsage(details?.usage);
465
+ if (usage) {
466
+ box.addChild(new Text(theme.fg("dim", ` Usage: ${usage}`), 0, 0));
467
+ }
446
468
  box.addChild(
447
469
  new Markdown(
448
470
  adviceForDisplay(advice, expanded),
@@ -486,6 +508,7 @@ export const registerCommands = (
486
508
  pi.on("session_shutdown", (_event, ctx) => {
487
509
  if (ctx.hasUI) {
488
510
  ctx.ui.setStatus("advisor-manual", undefined);
511
+ ctx.ui.setStatus("advisor-usage", undefined);
489
512
  }
490
513
  for (const [controller, token] of manualConsultations) {
491
514
  controller.abort();
@@ -506,7 +529,7 @@ export const registerCommands = (
506
529
  if (
507
530
  !(
508
531
  isSimpleMode() ||
509
- advisorSessionState.canConsult(advisorMaxCallsPerSessionRef)
532
+ advisorSessionState.canConsult(getAdvisorMaxCallsPerSession())
510
533
  )
511
534
  ) {
512
535
  const message = "Advisor call budget exhausted for this session.";
package/src/config.ts CHANGED
@@ -13,8 +13,8 @@ import {
13
13
  isValidGitContextLevel,
14
14
  } from "./git.js";
15
15
 
16
- export const FALLBACK_EXECUTOR = "aikeys/claude-sonnet-5";
17
- export const FALLBACK_ADVISOR = "aikeys/claude-fable-5";
16
+ export const FALLBACK_EXECUTOR = "openai-codex/gpt-5.6-luna";
17
+ export const FALLBACK_ADVISOR = "openai-codex/gpt-5.6-sol";
18
18
  export const DEFAULT_CONTEXT_MAX_CHARS = 15_000;
19
19
  export const MAX_CONTEXT_MAX_CHARS = Number.MAX_SAFE_INTEGER;
20
20
  export const DEFAULT_ADVISOR_TOOL_RESULT_MAX_LINES = DEFAULT_MAX_LINES;
@@ -208,9 +208,13 @@ export const getAdvisorSettings = () => ({
208
208
  untrackedContent: advisorUntrackedContentRef,
209
209
  });
210
210
 
211
+ /** Reads the live session budget at each enforcement boundary. */
212
+ export const getAdvisorMaxCallsPerSession = () =>
213
+ getAdvisorSettings().maxCallsPerSession;
214
+
211
215
  export const splitRef = (ref: string): [string, string] => {
212
216
  const i = ref.indexOf("/");
213
- return i === -1 ? ["aikeys", ref] : [ref.slice(0, i), ref.slice(i + 1)];
217
+ return i === -1 ? ["openai-codex", ref] : [ref.slice(0, i), ref.slice(i + 1)];
214
218
  };
215
219
 
216
220
  export const configPaths = (ctx: ExtensionContext) => [
@@ -169,9 +169,14 @@ const pendingInvocationDisclosure = (
169
169
 
170
170
  export interface BuildScoutManifestOptions {
171
171
  currentInvocationId?: string;
172
+ /** @deprecated Use maxManifestBytes for the Scout transport budget. */
172
173
  maxBytes?: number;
174
+ /** Maximum reconstructed Advisor conversation characters. */
175
+ maxConversationChars?: number;
173
176
  maxGroupBytes?: number;
174
177
  maxGroups?: number;
178
+ /** Maximum serialized group-manifest bytes sent to Scout. */
179
+ maxManifestBytes?: number;
175
180
  policies?: AdvisorToolPolicies;
176
181
  redact?: boolean;
177
182
  toolResultMaxBytes?: number;
@@ -191,9 +196,13 @@ export const buildScoutManifest = (
191
196
  options.toolResultMaxLines ?? advisorToolResultMaxLinesRef;
192
197
  const toolResultMaxBytes =
193
198
  options.toolResultMaxBytes ?? advisorToolResultMaxBytesRef;
194
- const maxBytes = options.maxBytes ?? SCOUT_MANIFEST_MAX_BYTES;
195
- const maxGroups = options.maxGroups ?? SCOUT_MANIFEST_MAX_GROUPS;
196
- const maxGroupBytes = options.maxGroupBytes ?? SCOUT_GROUP_MAX_BYTES;
199
+ const {
200
+ maxBytes,
201
+ maxConversationChars,
202
+ maxGroupBytes = SCOUT_GROUP_MAX_BYTES,
203
+ maxGroups = SCOUT_MANIFEST_MAX_GROUPS,
204
+ maxManifestBytes = maxBytes ?? SCOUT_MANIFEST_MAX_BYTES,
205
+ } = options;
197
206
 
198
207
  let latestUserIndex = -1;
199
208
  const callOwners = new Map<string, { index: number; name: string }>();
@@ -423,7 +432,7 @@ export const buildScoutManifest = (
423
432
  const availableBytes =
424
433
  groups.reduce((sum, group) => sum + groupWireBytes(group), 0) +
425
434
  protocolOmittedBytes;
426
- if (maxBytes <= 0) {
435
+ if (maxManifestBytes <= 0) {
427
436
  return {
428
437
  manifest: {
429
438
  availableBytes: 0,
@@ -439,22 +448,39 @@ export const buildScoutManifest = (
439
448
  if (
440
449
  required.some((group) => group.bytes > maxGroupBytes) ||
441
450
  required.length > maxGroups ||
442
- required.reduce((sum, group) => sum + groupWireBytes(group), 0) > maxBytes
451
+ required.reduce((sum, group) => sum + groupWireBytes(group), 0) >
452
+ maxManifestBytes
443
453
  ) {
444
454
  return {
445
455
  message:
446
- "Required Scout context exceeds a fixed manifest or per-group limit.",
456
+ "Required Scout context exceeds the Scout manifest transport limit.",
457
+ ok: false,
458
+ reason: "required-group-overflow",
459
+ };
460
+ }
461
+ const contentChars = (items: ScoutContextGroup[]) =>
462
+ items.reduce((sum, group) => sum + group.content.length, 0) +
463
+ Math.max(0, items.length - 1) * 2;
464
+ if (
465
+ maxConversationChars !== undefined &&
466
+ contentChars(required) > maxConversationChars
467
+ ) {
468
+ return {
469
+ message:
470
+ "Required Scout context exceeds the Advisor conversation budget.",
447
471
  ok: false,
448
472
  reason: "required-group-overflow",
449
473
  };
450
474
  }
451
-
452
475
  const selected = groups.filter(
453
476
  (group) => group.required || group.bytes <= maxGroupBytes
454
477
  );
455
478
  const fits = () =>
456
479
  selected.length <= maxGroups &&
457
- selected.reduce((sum, group) => sum + groupWireBytes(group), 0) <= maxBytes;
480
+ selected.reduce((sum, group) => sum + groupWireBytes(group), 0) <=
481
+ maxManifestBytes &&
482
+ (maxConversationChars === undefined ||
483
+ contentChars(selected) <= maxConversationChars);
458
484
  while (!fits()) {
459
485
  const optionalIndex = selected.findIndex((group) => !group.required);
460
486
  if (optionalIndex < 0) {
package/src/scout.ts CHANGED
@@ -13,7 +13,7 @@ import {
13
13
  SCOUT_SYNTHESIS_MAX_BYTES,
14
14
  type ScoutManifest,
15
15
  } from "./scout-context.js";
16
- import type { BenchmarkTelemetry } from "./telemetry.js";
16
+ import { snapshotAdvisorUsage } from "./usage.js";
17
17
 
18
18
  export const SCOUT_TIMEOUT_MS = 30_000;
19
19
 
@@ -224,42 +224,12 @@ export const runAdvisorScout = async (
224
224
  parentSignal?: AbortSignal,
225
225
  onEvent?: (event: ScoutLifecycleEvent) => void,
226
226
  timeoutMs = SCOUT_TIMEOUT_MS,
227
- dependencies: ScoutDependencies = defaultDependencies,
228
- telemetry?: BenchmarkTelemetry
227
+ dependencies: ScoutDependencies = defaultDependencies
229
228
  // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: cancellation, timeout, provider, and schema outcomes remain explicitly distinct.
230
229
  ): Promise<ScoutOutcome> => {
231
230
  const startedAt = Date.now();
232
231
  const publish = (event: ScoutLifecycleEvent) => {
233
232
  onEvent?.(event);
234
- if (event.type === "chunk") {
235
- return;
236
- }
237
- if (event.type === "call" || event.type === "cancelled") {
238
- telemetry?.scout(event);
239
- } else if (event.type === "success") {
240
- telemetry?.scout({
241
- availableCount: event.outcome.metrics.availableCount,
242
- latencyMs: event.outcome.metrics.latencyMs,
243
- model: event.outcome.model,
244
- omittedBeforeScout: event.outcome.metrics.omittedBeforeScout,
245
- selectedCount: event.outcome.metrics.selectedCount,
246
- selectedLabels: event.outcome.selectedLabels,
247
- synthesis: event.outcome.selection.synthesis,
248
- type: "success",
249
- usage: event.outcome.metrics.usage,
250
- });
251
- } else {
252
- telemetry?.scout({
253
- availableCount: event.outcome.metrics.availableCount,
254
- fallback: `${event.outcome.category}: ${event.outcome.message}`,
255
- latencyMs: event.outcome.metrics.latencyMs,
256
- model: event.outcome.model,
257
- omittedBeforeScout: event.outcome.metrics.omittedBeforeScout,
258
- selectedCount: event.outcome.metrics.selectedCount,
259
- type: "fallback",
260
- usage: event.outcome.metrics.usage,
261
- });
262
- }
263
233
  };
264
234
  if (parentSignal?.aborted) {
265
235
  publish({ type: "cancelled" });
@@ -361,7 +331,10 @@ export const runAdvisorScout = async (
361
331
  ? ("invalid-selection" as const)
362
332
  : ("empty-response" as const),
363
333
  message,
364
- metrics: { ...baseMetrics(manifest, startedAt), usage: streamed.usage },
334
+ metrics: {
335
+ ...baseMetrics(manifest, startedAt),
336
+ usage: snapshotAdvisorUsage(streamed.usage),
337
+ },
365
338
  model: executorRef,
366
339
  ok: false as const,
367
340
  };
@@ -383,7 +356,7 @@ export const runAdvisorScout = async (
383
356
  .filter((group) => group.required)
384
357
  .map((group) => group.id),
385
358
  ]).size,
386
- usage: streamed.usage,
359
+ usage: snapshotAdvisorUsage(streamed.usage),
387
360
  },
388
361
  model: executorRef,
389
362
  ok: true as const,
@@ -1,3 +1,11 @@
1
+ import {
2
+ type AdvisorUsageTotals,
3
+ addAdvisorUsage,
4
+ emptyAdvisorUsageTotals,
5
+ formatAdvisorUsageStatus,
6
+ formatAdvisorUsageTotals,
7
+ } from "./usage.js";
8
+
1
9
  export type GateDecision = "proceed" | "revise" | "blocked";
2
10
  export type ConsultationTrigger = "manual" | "executor-requested";
3
11
  export type GateTrigger =
@@ -120,6 +128,7 @@ export class AdvisorSessionState {
120
128
  #draftConsultations = 0;
121
129
  #outcomes = 0;
122
130
  #lastAdvice?: string;
131
+ #usage = emptyAdvisorUsageTotals();
123
132
 
124
133
  resetTask() {
125
134
  this.#previousSignature = undefined;
@@ -133,6 +142,7 @@ export class AdvisorSessionState {
133
142
  this.#draftConsultations = 0;
134
143
  this.#outcomes = 0;
135
144
  this.#lastAdvice = undefined;
145
+ this.#usage = emptyAdvisorUsageTotals();
136
146
  }
137
147
 
138
148
  clearBlocked() {
@@ -182,8 +192,19 @@ export class AdvisorSessionState {
182
192
  return this.#consumedCalls;
183
193
  }
184
194
 
195
+ /** Returns a copy of cumulative direct Advisor usage for this session. */
196
+ get usageTotals(): AdvisorUsageTotals {
197
+ return { ...this.#usage };
198
+ }
199
+
200
+ /** Returns the footer-ready direct Advisor usage status for this session. */
201
+ usageStatus() {
202
+ return formatAdvisorUsageStatus(this.#usage);
203
+ }
204
+
185
205
  recordInvocation(record: AdvisorInvocationRecord) {
186
206
  this.#invocations.push(record);
207
+ addAdvisorUsage(this.#usage, record.usage);
187
208
  }
188
209
  issueAdvice(
189
210
  id: string,
@@ -271,6 +292,7 @@ export class AdvisorSessionState {
271
292
  `Triggers: ${["manual", "executor-requested", "repeated-tool-call", "completion-review", "custom-rule"].filter((trigger) => countTrigger(trigger as AdvisorTrigger) > 0).join(", ") || "none"}`,
272
293
  `Models: ${models}`,
273
294
  `Budget: ${budget}`,
295
+ `Usage: ${formatAdvisorUsageTotals(this.#usage)}`,
274
296
  `Markdown advice: ${markdown.length} responses (${this.#draftConsultations} with drafts)`,
275
297
  `Outcome reports: ${this.#outcomes}`,
276
298
  `Gate decisions: ${decisions}`,