pi-advisor-flow 0.4.0 → 0.5.1

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/config.ts CHANGED
@@ -13,8 +13,6 @@ import {
13
13
  isValidGitContextLevel,
14
14
  } from "./git.js";
15
15
 
16
- export const FALLBACK_EXECUTOR = "openai-codex/gpt-5.6-luna";
17
- export const FALLBACK_ADVISOR = "openai-codex/gpt-5.6-sol";
18
16
  export const DEFAULT_CONTEXT_MAX_CHARS = 15_000;
19
17
  export const MAX_CONTEXT_MAX_CHARS = Number.MAX_SAFE_INTEGER;
20
18
  export const DEFAULT_ADVISOR_TOOL_RESULT_MAX_LINES = DEFAULT_MAX_LINES;
@@ -37,8 +35,11 @@ export const GATE_FAILURE_MODES: GateFailureMode[] = [
37
35
  "warn-and-continue",
38
36
  ];
39
37
 
40
- export let executorRef = FALLBACK_EXECUTOR;
41
- export let advisorRef = FALLBACK_ADVISOR;
38
+ // An empty ref means no model has been selected yet.
39
+ export let executorRef = "";
40
+ export let advisorRef = "";
41
+ let persistedExecutorRef: string | undefined;
42
+ let persistedAdvisorRef: string | undefined;
42
43
  export let executorEffortRef: string | undefined;
43
44
  export let advisorEffortRef: string | undefined;
44
45
  export let contextMaxCharsRef = DEFAULT_CONTEXT_MAX_CHARS;
@@ -66,6 +67,8 @@ export let advisorOutcomeLoggingRef = false;
66
67
  export let advisorUntrackedContentRef = false;
67
68
  export let advisorTrackedFileContentRef = false;
68
69
  export let advisorScoutEnabledRef = false;
70
+ export let showUsageDetailsRef = true;
71
+ export let showUsageFooterRef = false;
69
72
 
70
73
  export const setExecutorRef = (ref: string) => {
71
74
  executorRef = ref;
@@ -73,6 +76,11 @@ export const setExecutorRef = (ref: string) => {
73
76
  export const setAdvisorRef = (ref: string) => {
74
77
  advisorRef = ref;
75
78
  };
79
+ /** Returns model refs explicitly persisted in the global Advisor config. */
80
+ export const getPersistedModelRefs = () => ({
81
+ advisor: persistedAdvisorRef,
82
+ executor: persistedExecutorRef,
83
+ });
76
84
  export const setExecutorEffortRef = (effort: string | undefined) => {
77
85
  executorEffortRef = effort;
78
86
  };
@@ -174,6 +182,12 @@ export const setAdvisorTrackedFileContentRef = (enabled: boolean) => {
174
182
  export const setAdvisorScoutEnabledRef = (enabled: boolean) => {
175
183
  advisorScoutEnabledRef = enabled;
176
184
  };
185
+ export const setShowUsageDetailsRef = (enabled: boolean) => {
186
+ showUsageDetailsRef = enabled;
187
+ };
188
+ export const setShowUsageFooterRef = (enabled: boolean) => {
189
+ showUsageFooterRef = enabled;
190
+ };
177
191
 
178
192
  /**
179
193
  * Returns the current live settings state. Use this at UI boundaries instead of
@@ -200,6 +214,8 @@ export const getAdvisorSettings = () => ({
200
214
  redactSecrets: advisorRedactSecretsRef,
201
215
  scoutEnabled: advisorScoutEnabledRef,
202
216
  sessionSummary: advisorSessionSummaryRef,
217
+ showUsageDetails: showUsageDetailsRef,
218
+ showUsageFooter: showUsageFooterRef,
203
219
  simpleMode: simpleModeRef,
204
220
  toolPolicies: { ...advisorToolPoliciesRef },
205
221
  toolResultMaxBytes: advisorToolResultMaxBytesRef,
@@ -253,6 +269,8 @@ export interface AdvisorConfig {
253
269
  executor?: string;
254
270
  executorEffort?: string;
255
271
  gateFailureMode?: GateFailureMode;
272
+ showUsageDetails?: boolean;
273
+ showUsageFooter?: boolean;
256
274
  simpleMode?: boolean;
257
275
  }
258
276
 
@@ -273,6 +291,7 @@ const CONFIG_KEYS = new Set<keyof AdvisorConfig>([
273
291
  "advisorPlanGate",
274
292
  "advisorSessionSummary",
275
293
  "advisorScoutEnabled",
294
+ "showUsageDetails",
276
295
  "simpleMode",
277
296
  "alwaysOn",
278
297
  "advisorToolResultMaxBytes",
@@ -286,6 +305,7 @@ const CONFIG_KEYS = new Set<keyof AdvisorConfig>([
286
305
  "executor",
287
306
  "executorEffort",
288
307
  "gateFailureMode",
308
+ "showUsageFooter",
289
309
  ]);
290
310
  const BOOLEAN_CONFIG_KEYS = [
291
311
  "advisorPlanGate",
@@ -296,6 +316,8 @@ const BOOLEAN_CONFIG_KEYS = [
296
316
  "advisorAutoLoopGate",
297
317
  "advisorSessionSummary",
298
318
  "advisorScoutEnabled",
319
+ "showUsageDetails",
320
+ "showUsageFooter",
299
321
  "simpleMode",
300
322
  "alwaysOn",
301
323
  "advisorHerdrIntegration",
@@ -453,8 +475,10 @@ export const validateConfig = (
453
475
  };
454
476
 
455
477
  const resetDefaults = () => {
456
- executorRef = FALLBACK_EXECUTOR;
457
- advisorRef = FALLBACK_ADVISOR;
478
+ executorRef = "";
479
+ advisorRef = "";
480
+ persistedExecutorRef = undefined;
481
+ persistedAdvisorRef = undefined;
458
482
  executorEffortRef = undefined;
459
483
  advisorEffortRef = undefined;
460
484
  contextMaxCharsRef = DEFAULT_CONTEXT_MAX_CHARS;
@@ -482,6 +506,8 @@ const resetDefaults = () => {
482
506
  advisorUntrackedContentRef = false;
483
507
  advisorTrackedFileContentRef = false;
484
508
  advisorScoutEnabledRef = false;
509
+ showUsageDetailsRef = true;
510
+ showUsageFooterRef = false;
485
511
  };
486
512
 
487
513
  const applyOptionalConfig = <Key extends keyof AdvisorConfig>(
@@ -504,6 +530,9 @@ const applyNonEmptyStringConfig = (
504
530
  }
505
531
  };
506
532
 
533
+ const configuredModelRef = (value: string | undefined): string | undefined =>
534
+ value?.trim() || undefined;
535
+
507
536
  const applyConfig = (config: AdvisorConfig) => {
508
537
  applyNonEmptyStringConfig(config.executor, setExecutorRef);
509
538
  applyNonEmptyStringConfig(config.advisor, setAdvisorRef);
@@ -549,6 +578,8 @@ const applyConfig = (config: AdvisorConfig) => {
549
578
  setAdvisorSessionSummaryRef
550
579
  );
551
580
  applyOptionalConfig(config, "advisorScoutEnabled", setAdvisorScoutEnabledRef);
581
+ applyOptionalConfig(config, "showUsageDetails", setShowUsageDetailsRef);
582
+ applyOptionalConfig(config, "showUsageFooter", setShowUsageFooterRef);
552
583
  applyOptionalConfig(config, "simpleMode", setSimpleModeRef);
553
584
  applyOptionalConfig(config, "alwaysOn", setAlwaysOnRef);
554
585
  applyOptionalConfig(config, "gateFailureMode", setAdvisorFailureModeRef);
@@ -647,6 +678,8 @@ export const loadConfig = (_ctx: ExtensionContext) => {
647
678
  const globalConfig = existsSync(global)
648
679
  ? readConfigCached(global)
649
680
  : undefined;
681
+ persistedExecutorRef = configuredModelRef(globalConfig?.executor);
682
+ persistedAdvisorRef = configuredModelRef(globalConfig?.advisor);
650
683
  if (globalConfig) {
651
684
  applyConfig(globalConfig);
652
685
  const unknownKeys = unknownConfigKeys(globalConfig as ConfigRecord);
@@ -671,8 +704,18 @@ export const loadConfig = (_ctx: ExtensionContext) => {
671
704
  };
672
705
 
673
706
  /** Saves user-controlled configuration globally without outcome consent. */
674
- export const saveConfig = (_ctx: ExtensionContext) => {
707
+ export interface SaveConfigOptions {
708
+ persistAdvisor?: boolean;
709
+ persistExecutor?: boolean;
710
+ }
711
+
712
+ export const saveConfig = (
713
+ _ctx: ExtensionContext,
714
+ options: SaveConfigOptions = {}
715
+ ) => {
675
716
  const path = join(getAgentDir(), "advisor.json");
717
+ const persistAdvisor = options.persistAdvisor ?? true;
718
+ const persistExecutor = options.persistExecutor ?? true;
676
719
  let existing: Record<string, unknown> = {};
677
720
  try {
678
721
  const parsed = JSON.parse(readFileSync(path, "utf8"));
@@ -687,7 +730,7 @@ export const saveConfig = (_ctx: ExtensionContext) => {
687
730
  }
688
731
  const data = {
689
732
  ...existing,
690
- advisor: advisorRef,
733
+ ...(persistAdvisor && advisorRef ? { advisor: advisorRef } : {}),
691
734
  advisorAutoLoopGate: advisorAutoLoopGateRef,
692
735
  advisorBlockOnBlocked: advisorBlockOnBlockedRef,
693
736
  advisorCollapseResponses: advisorCollapseResponsesRef,
@@ -698,7 +741,7 @@ export const saveConfig = (_ctx: ExtensionContext) => {
698
741
  advisorLoopThreshold: advisorLoopThresholdRef,
699
742
  advisorPlanGate: advisorPlanGateRef,
700
743
  contextMaxChars: contextMaxCharsRef,
701
- executor: executorRef,
744
+ ...(persistExecutor && executorRef ? { executor: executorRef } : {}),
702
745
  executorEffort: executorEffortRef,
703
746
  ...(advisorMaxCallsPerSessionRef === undefined
704
747
  ? {}
@@ -716,6 +759,8 @@ export const saveConfig = (_ctx: ExtensionContext) => {
716
759
  advisorUntrackedContent: advisorUntrackedContentRef,
717
760
  alwaysOn: alwaysOnRef,
718
761
  gateFailureMode: advisorFailureModeRef,
762
+ showUsageDetails: showUsageDetailsRef,
763
+ showUsageFooter: showUsageFooterRef,
719
764
  simpleMode: simpleModeRef,
720
765
  };
721
766
  writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`);
@@ -18,9 +18,12 @@ export interface ResolvedConfiguredModel {
18
18
 
19
19
  export const resolveConfiguredModel = async (
20
20
  ctx: ExtensionContext,
21
- ref: string,
21
+ ref: string | undefined,
22
22
  label: string
23
23
  ): Promise<ResolvedConfiguredModel> => {
24
+ if (!ref) {
25
+ throw new Error(`${label} model not configured`);
26
+ }
24
27
  const [provider, modelId] = splitRef(ref);
25
28
  const model = ctx.modelRegistry.find(provider, modelId);
26
29
  if (!model) {
@@ -56,6 +59,129 @@ export interface CollectedTextStream {
56
59
  usage?: unknown;
57
60
  }
58
61
 
62
+ export const ADVISOR_STREAM_UPDATE_INTERVAL_MS = 90;
63
+
64
+ export interface CoalescedUpdateResult {
65
+ error?: unknown;
66
+ failed: boolean;
67
+ }
68
+
69
+ export interface CoalescedUpdate<T> {
70
+ cancel: () => void;
71
+ flush: () => CoalescedUpdateResult;
72
+ update: (value: T) => void;
73
+ }
74
+
75
+ export interface CoalescedUpdateScheduler {
76
+ clearTimeout: (timer: ReturnType<typeof setTimeout>) => void;
77
+ now: () => number;
78
+ setTimeout: (
79
+ callback: () => void,
80
+ delay: number
81
+ ) => ReturnType<typeof setTimeout>;
82
+ }
83
+
84
+ /**
85
+ * Keep stream updates responsive without forwarding every provider delta to
86
+ * the UI. The first update in a burst is immediate; later updates are kept as
87
+ * the latest value and published at most once per interval. `flush()` closes
88
+ * the publisher and is intended for terminal success/error paths.
89
+ */
90
+ export const createCoalescedUpdate = <T>(
91
+ publish: (value: T) => void,
92
+ intervalMs = ADVISOR_STREAM_UPDATE_INTERVAL_MS,
93
+ scheduler: CoalescedUpdateScheduler = {
94
+ clearTimeout,
95
+ now: Date.now,
96
+ setTimeout: (callback, delay) => setTimeout(callback, delay),
97
+ }
98
+ ): CoalescedUpdate<T> => {
99
+ if (!Number.isFinite(intervalMs) || intervalMs <= 0) {
100
+ throw new Error("Coalesced update interval must be positive and finite.");
101
+ }
102
+
103
+ let closed = false;
104
+ let hasPending = false;
105
+ let pending: T | undefined;
106
+ let lastPublishedAt: number | undefined;
107
+ let timer: ReturnType<typeof setTimeout> | undefined;
108
+ let publishError: unknown;
109
+ let publishFailed = false;
110
+
111
+ const clearTimer = () => {
112
+ if (timer !== undefined) {
113
+ scheduler.clearTimeout(timer);
114
+ timer = undefined;
115
+ }
116
+ };
117
+
118
+ const publishPending = () => {
119
+ timer = undefined;
120
+ if (!hasPending) {
121
+ return;
122
+ }
123
+ const value = pending as T;
124
+ pending = undefined;
125
+ hasPending = false;
126
+ lastPublishedAt = scheduler.now();
127
+ try {
128
+ publish(value);
129
+ } catch (error) {
130
+ publishFailed = true;
131
+ publishError = error;
132
+ closed = true;
133
+ clearTimer();
134
+ }
135
+ };
136
+
137
+ const schedule = () => {
138
+ const elapsed =
139
+ lastPublishedAt === undefined
140
+ ? intervalMs
141
+ : scheduler.now() - lastPublishedAt;
142
+ const delay = Math.max(0, intervalMs - elapsed);
143
+ if (delay === 0) {
144
+ publishPending();
145
+ return;
146
+ }
147
+ timer = scheduler.setTimeout(publishPending, delay);
148
+ timer.unref?.();
149
+ };
150
+
151
+ return {
152
+ cancel: () => {
153
+ closed = true;
154
+ clearTimer();
155
+ pending = undefined;
156
+ hasPending = false;
157
+ },
158
+ flush: () => {
159
+ if (!closed) {
160
+ closed = true;
161
+ clearTimer();
162
+ publishPending();
163
+ }
164
+ return { error: publishError, failed: publishFailed };
165
+ },
166
+ update: (value) => {
167
+ if (closed) {
168
+ return;
169
+ }
170
+ if (publishFailed) {
171
+ throw publishError;
172
+ }
173
+ pending = value;
174
+ hasPending = true;
175
+ if (timer === undefined) {
176
+ schedule();
177
+ }
178
+ if (publishFailed) {
179
+ throw publishError;
180
+ }
181
+ },
182
+ };
183
+ };
184
+
59
185
  export const collectTextStream = async (
60
186
  resolved: ResolvedConfiguredModel,
61
187
  options: CollectTextStreamOptions,
@@ -70,7 +196,13 @@ export const collectTextStream = async (
70
196
  apiKey: resolved.apiKey,
71
197
  env: resolved.env,
72
198
  headers: resolved.headers,
199
+ // `stream()` uses the provider-facing name while the extension's public
200
+ // option keeps the Pi-facing `reasoning` name. Preserve both so the
201
+ // configured effort reaches providers that serialize reasoning_effort.
73
202
  reasoning: options.reasoning as never,
203
+ ...(options.reasoning === undefined
204
+ ? {}
205
+ : { reasoningEffort: options.reasoning as never }),
74
206
  signal: options.signal,
75
207
  }
76
208
  );
@@ -124,6 +124,7 @@ export class AdvisorSessionState {
124
124
  string,
125
125
  { advice: string; trigger: ConsultationTrigger }
126
126
  >();
127
+ readonly #pendingAdvice = new Set<string>();
127
128
  readonly #reportedAdvice = new Set<string>();
128
129
  #draftConsultations = 0;
129
130
  #outcomes = 0;
@@ -138,6 +139,7 @@ export class AdvisorSessionState {
138
139
  this.#loopInterventions = 0;
139
140
  this.#consumedCalls = 0;
140
141
  this.#issuedAdvice.clear();
142
+ this.#pendingAdvice.clear();
141
143
  this.#reportedAdvice.clear();
142
144
  this.#draftConsultations = 0;
143
145
  this.#outcomes = 0;
@@ -237,16 +239,38 @@ export class AdvisorSessionState {
237
239
  return true;
238
240
  }
239
241
 
240
- claimAdvice(id: string) {
241
- if (this.#reportedAdvice.has(id)) {
242
+ reserveAdvice(id: string) {
243
+ if (this.#reportedAdvice.has(id) || this.#pendingAdvice.has(id)) {
242
244
  return;
243
245
  }
244
246
  const advice = this.#issuedAdvice.get(id);
245
247
  if (!advice) {
246
248
  return;
247
249
  }
250
+ this.#pendingAdvice.add(id);
251
+ return advice;
252
+ }
253
+
254
+ commitAdvice(id: string) {
255
+ if (!this.#pendingAdvice.delete(id)) {
256
+ return false;
257
+ }
248
258
  this.#reportedAdvice.add(id);
249
259
  this.#outcomes += 1;
260
+ return true;
261
+ }
262
+
263
+ releaseAdvice(id: string) {
264
+ this.#pendingAdvice.delete(id);
265
+ }
266
+
267
+ /** Compatibility helper for synchronous callers that can commit immediately. */
268
+ claimAdvice(id: string) {
269
+ const advice = this.reserveAdvice(id);
270
+ if (!advice) {
271
+ return;
272
+ }
273
+ this.commitAdvice(id);
250
274
  return advice;
251
275
  }
252
276