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/CHANGELOG.md +63 -0
- package/README.md +36 -68
- package/package.json +23 -11
- package/src/commands.ts +711 -184
- package/src/config.ts +54 -9
- package/src/model-stream.ts +133 -1
- package/src/session-state.ts +26 -2
- package/src/tools.ts +195 -66
- package/src/ui.ts +1060 -600
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
|
-
|
|
41
|
-
export let
|
|
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 =
|
|
457
|
-
advisorRef =
|
|
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
|
|
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`);
|
package/src/model-stream.ts
CHANGED
|
@@ -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
|
);
|
package/src/session-state.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|