pi-advisor-flow 0.4.0 → 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/CHANGELOG.md +23 -0
- package/README.md +8 -5
- package/package.json +11 -11
- package/src/commands.ts +327 -113
- package/src/config.ts +22 -0
- package/src/model-stream.ts +123 -0
- package/src/session-state.ts +26 -2
- package/src/tools.ts +117 -42
- package/src/ui.ts +1060 -600
package/src/config.ts
CHANGED
|
@@ -66,6 +66,8 @@ export let advisorOutcomeLoggingRef = false;
|
|
|
66
66
|
export let advisorUntrackedContentRef = false;
|
|
67
67
|
export let advisorTrackedFileContentRef = false;
|
|
68
68
|
export let advisorScoutEnabledRef = false;
|
|
69
|
+
export let showUsageDetailsRef = true;
|
|
70
|
+
export let showUsageFooterRef = false;
|
|
69
71
|
|
|
70
72
|
export const setExecutorRef = (ref: string) => {
|
|
71
73
|
executorRef = ref;
|
|
@@ -174,6 +176,12 @@ export const setAdvisorTrackedFileContentRef = (enabled: boolean) => {
|
|
|
174
176
|
export const setAdvisorScoutEnabledRef = (enabled: boolean) => {
|
|
175
177
|
advisorScoutEnabledRef = enabled;
|
|
176
178
|
};
|
|
179
|
+
export const setShowUsageDetailsRef = (enabled: boolean) => {
|
|
180
|
+
showUsageDetailsRef = enabled;
|
|
181
|
+
};
|
|
182
|
+
export const setShowUsageFooterRef = (enabled: boolean) => {
|
|
183
|
+
showUsageFooterRef = enabled;
|
|
184
|
+
};
|
|
177
185
|
|
|
178
186
|
/**
|
|
179
187
|
* Returns the current live settings state. Use this at UI boundaries instead of
|
|
@@ -200,6 +208,8 @@ export const getAdvisorSettings = () => ({
|
|
|
200
208
|
redactSecrets: advisorRedactSecretsRef,
|
|
201
209
|
scoutEnabled: advisorScoutEnabledRef,
|
|
202
210
|
sessionSummary: advisorSessionSummaryRef,
|
|
211
|
+
showUsageDetails: showUsageDetailsRef,
|
|
212
|
+
showUsageFooter: showUsageFooterRef,
|
|
203
213
|
simpleMode: simpleModeRef,
|
|
204
214
|
toolPolicies: { ...advisorToolPoliciesRef },
|
|
205
215
|
toolResultMaxBytes: advisorToolResultMaxBytesRef,
|
|
@@ -253,6 +263,8 @@ export interface AdvisorConfig {
|
|
|
253
263
|
executor?: string;
|
|
254
264
|
executorEffort?: string;
|
|
255
265
|
gateFailureMode?: GateFailureMode;
|
|
266
|
+
showUsageDetails?: boolean;
|
|
267
|
+
showUsageFooter?: boolean;
|
|
256
268
|
simpleMode?: boolean;
|
|
257
269
|
}
|
|
258
270
|
|
|
@@ -273,6 +285,7 @@ const CONFIG_KEYS = new Set<keyof AdvisorConfig>([
|
|
|
273
285
|
"advisorPlanGate",
|
|
274
286
|
"advisorSessionSummary",
|
|
275
287
|
"advisorScoutEnabled",
|
|
288
|
+
"showUsageDetails",
|
|
276
289
|
"simpleMode",
|
|
277
290
|
"alwaysOn",
|
|
278
291
|
"advisorToolResultMaxBytes",
|
|
@@ -286,6 +299,7 @@ const CONFIG_KEYS = new Set<keyof AdvisorConfig>([
|
|
|
286
299
|
"executor",
|
|
287
300
|
"executorEffort",
|
|
288
301
|
"gateFailureMode",
|
|
302
|
+
"showUsageFooter",
|
|
289
303
|
]);
|
|
290
304
|
const BOOLEAN_CONFIG_KEYS = [
|
|
291
305
|
"advisorPlanGate",
|
|
@@ -296,6 +310,8 @@ const BOOLEAN_CONFIG_KEYS = [
|
|
|
296
310
|
"advisorAutoLoopGate",
|
|
297
311
|
"advisorSessionSummary",
|
|
298
312
|
"advisorScoutEnabled",
|
|
313
|
+
"showUsageDetails",
|
|
314
|
+
"showUsageFooter",
|
|
299
315
|
"simpleMode",
|
|
300
316
|
"alwaysOn",
|
|
301
317
|
"advisorHerdrIntegration",
|
|
@@ -482,6 +498,8 @@ const resetDefaults = () => {
|
|
|
482
498
|
advisorUntrackedContentRef = false;
|
|
483
499
|
advisorTrackedFileContentRef = false;
|
|
484
500
|
advisorScoutEnabledRef = false;
|
|
501
|
+
showUsageDetailsRef = true;
|
|
502
|
+
showUsageFooterRef = false;
|
|
485
503
|
};
|
|
486
504
|
|
|
487
505
|
const applyOptionalConfig = <Key extends keyof AdvisorConfig>(
|
|
@@ -549,6 +567,8 @@ const applyConfig = (config: AdvisorConfig) => {
|
|
|
549
567
|
setAdvisorSessionSummaryRef
|
|
550
568
|
);
|
|
551
569
|
applyOptionalConfig(config, "advisorScoutEnabled", setAdvisorScoutEnabledRef);
|
|
570
|
+
applyOptionalConfig(config, "showUsageDetails", setShowUsageDetailsRef);
|
|
571
|
+
applyOptionalConfig(config, "showUsageFooter", setShowUsageFooterRef);
|
|
552
572
|
applyOptionalConfig(config, "simpleMode", setSimpleModeRef);
|
|
553
573
|
applyOptionalConfig(config, "alwaysOn", setAlwaysOnRef);
|
|
554
574
|
applyOptionalConfig(config, "gateFailureMode", setAdvisorFailureModeRef);
|
|
@@ -716,6 +736,8 @@ export const saveConfig = (_ctx: ExtensionContext) => {
|
|
|
716
736
|
advisorUntrackedContent: advisorUntrackedContentRef,
|
|
717
737
|
alwaysOn: alwaysOnRef,
|
|
718
738
|
gateFailureMode: advisorFailureModeRef,
|
|
739
|
+
showUsageDetails: showUsageDetailsRef,
|
|
740
|
+
showUsageFooter: showUsageFooterRef,
|
|
719
741
|
simpleMode: simpleModeRef,
|
|
720
742
|
};
|
|
721
743
|
writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`);
|
package/src/model-stream.ts
CHANGED
|
@@ -56,6 +56,129 @@ export interface CollectedTextStream {
|
|
|
56
56
|
usage?: unknown;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
export const ADVISOR_STREAM_UPDATE_INTERVAL_MS = 90;
|
|
60
|
+
|
|
61
|
+
export interface CoalescedUpdateResult {
|
|
62
|
+
error?: unknown;
|
|
63
|
+
failed: boolean;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface CoalescedUpdate<T> {
|
|
67
|
+
cancel: () => void;
|
|
68
|
+
flush: () => CoalescedUpdateResult;
|
|
69
|
+
update: (value: T) => void;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface CoalescedUpdateScheduler {
|
|
73
|
+
clearTimeout: (timer: ReturnType<typeof setTimeout>) => void;
|
|
74
|
+
now: () => number;
|
|
75
|
+
setTimeout: (
|
|
76
|
+
callback: () => void,
|
|
77
|
+
delay: number
|
|
78
|
+
) => ReturnType<typeof setTimeout>;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Keep stream updates responsive without forwarding every provider delta to
|
|
83
|
+
* the UI. The first update in a burst is immediate; later updates are kept as
|
|
84
|
+
* the latest value and published at most once per interval. `flush()` closes
|
|
85
|
+
* the publisher and is intended for terminal success/error paths.
|
|
86
|
+
*/
|
|
87
|
+
export const createCoalescedUpdate = <T>(
|
|
88
|
+
publish: (value: T) => void,
|
|
89
|
+
intervalMs = ADVISOR_STREAM_UPDATE_INTERVAL_MS,
|
|
90
|
+
scheduler: CoalescedUpdateScheduler = {
|
|
91
|
+
clearTimeout,
|
|
92
|
+
now: Date.now,
|
|
93
|
+
setTimeout: (callback, delay) => setTimeout(callback, delay),
|
|
94
|
+
}
|
|
95
|
+
): CoalescedUpdate<T> => {
|
|
96
|
+
if (!Number.isFinite(intervalMs) || intervalMs <= 0) {
|
|
97
|
+
throw new Error("Coalesced update interval must be positive and finite.");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let closed = false;
|
|
101
|
+
let hasPending = false;
|
|
102
|
+
let pending: T | undefined;
|
|
103
|
+
let lastPublishedAt: number | undefined;
|
|
104
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
105
|
+
let publishError: unknown;
|
|
106
|
+
let publishFailed = false;
|
|
107
|
+
|
|
108
|
+
const clearTimer = () => {
|
|
109
|
+
if (timer !== undefined) {
|
|
110
|
+
scheduler.clearTimeout(timer);
|
|
111
|
+
timer = undefined;
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const publishPending = () => {
|
|
116
|
+
timer = undefined;
|
|
117
|
+
if (!hasPending) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const value = pending as T;
|
|
121
|
+
pending = undefined;
|
|
122
|
+
hasPending = false;
|
|
123
|
+
lastPublishedAt = scheduler.now();
|
|
124
|
+
try {
|
|
125
|
+
publish(value);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
publishFailed = true;
|
|
128
|
+
publishError = error;
|
|
129
|
+
closed = true;
|
|
130
|
+
clearTimer();
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const schedule = () => {
|
|
135
|
+
const elapsed =
|
|
136
|
+
lastPublishedAt === undefined
|
|
137
|
+
? intervalMs
|
|
138
|
+
: scheduler.now() - lastPublishedAt;
|
|
139
|
+
const delay = Math.max(0, intervalMs - elapsed);
|
|
140
|
+
if (delay === 0) {
|
|
141
|
+
publishPending();
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
timer = scheduler.setTimeout(publishPending, delay);
|
|
145
|
+
timer.unref?.();
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
cancel: () => {
|
|
150
|
+
closed = true;
|
|
151
|
+
clearTimer();
|
|
152
|
+
pending = undefined;
|
|
153
|
+
hasPending = false;
|
|
154
|
+
},
|
|
155
|
+
flush: () => {
|
|
156
|
+
if (!closed) {
|
|
157
|
+
closed = true;
|
|
158
|
+
clearTimer();
|
|
159
|
+
publishPending();
|
|
160
|
+
}
|
|
161
|
+
return { error: publishError, failed: publishFailed };
|
|
162
|
+
},
|
|
163
|
+
update: (value) => {
|
|
164
|
+
if (closed) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (publishFailed) {
|
|
168
|
+
throw publishError;
|
|
169
|
+
}
|
|
170
|
+
pending = value;
|
|
171
|
+
hasPending = true;
|
|
172
|
+
if (timer === undefined) {
|
|
173
|
+
schedule();
|
|
174
|
+
}
|
|
175
|
+
if (publishFailed) {
|
|
176
|
+
throw publishError;
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
};
|
|
181
|
+
|
|
59
182
|
export const collectTextStream = async (
|
|
60
183
|
resolved: ResolvedConfiguredModel,
|
|
61
184
|
options: CollectTextStreamOptions,
|
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
|
|
package/src/tools.ts
CHANGED
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
contextMaxCharsRef,
|
|
36
36
|
executorRef,
|
|
37
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 {
|
|
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 {
|
|
@@ -712,7 +718,10 @@ const updateAdvisorUsageStatus = (
|
|
|
712
718
|
session: AdvisorSessionState
|
|
713
719
|
) => {
|
|
714
720
|
if (ctx.hasUI) {
|
|
715
|
-
ctx.ui.setStatus(
|
|
721
|
+
ctx.ui.setStatus(
|
|
722
|
+
"advisor-usage",
|
|
723
|
+
getAdvisorSettings().showUsageFooter ? session.usageStatus() : undefined
|
|
724
|
+
);
|
|
716
725
|
}
|
|
717
726
|
};
|
|
718
727
|
|
|
@@ -740,12 +749,15 @@ export const gateFailureEffectForMode = (
|
|
|
740
749
|
: ("session-blocked" as const);
|
|
741
750
|
};
|
|
742
751
|
|
|
743
|
-
const gateDecisionEffect = (
|
|
752
|
+
const gateDecisionEffect = (
|
|
753
|
+
decision: GateDecision,
|
|
754
|
+
failureMode: "block-session" | "block-tool" | "warn-and-continue"
|
|
755
|
+
) => {
|
|
744
756
|
if (decision === "proceed") {
|
|
745
757
|
return "continued" as const;
|
|
746
758
|
}
|
|
747
759
|
return decision === "blocked"
|
|
748
|
-
? (
|
|
760
|
+
? gateFailureEffectForMode(failureMode)
|
|
749
761
|
: ("tool-blocked" as const);
|
|
750
762
|
};
|
|
751
763
|
|
|
@@ -753,15 +765,42 @@ const failureEffect = (
|
|
|
753
765
|
category: GateFailureCategory,
|
|
754
766
|
message: string,
|
|
755
767
|
ctx: ExtensionContext,
|
|
756
|
-
session: AdvisorSessionState
|
|
768
|
+
session: AdvisorSessionState,
|
|
769
|
+
failureMode: "block-session" | "block-tool" | "warn-and-continue"
|
|
757
770
|
) => {
|
|
758
771
|
const reason = `Advisor gate ${category}: ${message}`;
|
|
759
|
-
notifyLocalFailure(ctx, message,
|
|
772
|
+
notifyLocalFailure(ctx, message, failureMode === "block-session");
|
|
760
773
|
notifyHerdrAdvisorFailure("Advisor gate failure", reason);
|
|
761
|
-
if (
|
|
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
|
+
}
|
|
762
801
|
return { block: false, effect: "continued" as const, reason };
|
|
763
802
|
}
|
|
764
|
-
if (
|
|
803
|
+
if (failureMode === "block-tool") {
|
|
765
804
|
return { block: true, effect: "tool-blocked" as const, reason };
|
|
766
805
|
}
|
|
767
806
|
session.block(reason);
|
|
@@ -870,12 +909,14 @@ const handleAutomaticGate = async (
|
|
|
870
909
|
return;
|
|
871
910
|
}
|
|
872
911
|
const reason = `Advisor loop gate: normalized signature for ${event.toolName} repeated ${advisorLoopThresholdRef} times without a materially different tool action.`;
|
|
912
|
+
const failureMode = advisorFailureModeRef;
|
|
873
913
|
if (!session.canConsult(getAdvisorMaxCallsPerSession())) {
|
|
874
914
|
const failure = failureEffect(
|
|
875
915
|
"budget-exhausted",
|
|
876
916
|
"Advisor gate call budget is exhausted.",
|
|
877
917
|
ctx,
|
|
878
|
-
session
|
|
918
|
+
session,
|
|
919
|
+
failureMode
|
|
879
920
|
);
|
|
880
921
|
return failure.block ? { block: true, reason: failure.reason } : undefined;
|
|
881
922
|
}
|
|
@@ -913,7 +954,7 @@ const handleAutomaticGate = async (
|
|
|
913
954
|
ensureGateCall();
|
|
914
955
|
if (!result.ok) {
|
|
915
956
|
session.recordInvocation({
|
|
916
|
-
executionEffect: gateFailureEffectForMode(
|
|
957
|
+
executionEffect: gateFailureEffectForMode(failureMode),
|
|
917
958
|
failure: result.category,
|
|
918
959
|
kind: "gate",
|
|
919
960
|
model: advisorRef,
|
|
@@ -925,7 +966,8 @@ const handleAutomaticGate = async (
|
|
|
925
966
|
result.category,
|
|
926
967
|
result.message,
|
|
927
968
|
ctx,
|
|
928
|
-
session
|
|
969
|
+
session,
|
|
970
|
+
failureMode
|
|
929
971
|
);
|
|
930
972
|
sendAutomaticGateFailure(
|
|
931
973
|
pi,
|
|
@@ -939,7 +981,7 @@ const handleAutomaticGate = async (
|
|
|
939
981
|
session.recordInvocation({
|
|
940
982
|
cost: advisorUsageCost(result.usage),
|
|
941
983
|
decision: result.decision,
|
|
942
|
-
executionEffect: gateDecisionEffect(result.decision),
|
|
984
|
+
executionEffect: gateDecisionEffect(result.decision, failureMode),
|
|
943
985
|
kind: "gate",
|
|
944
986
|
model: result.model,
|
|
945
987
|
trigger: result.trigger,
|
|
@@ -953,11 +995,13 @@ const handleAutomaticGate = async (
|
|
|
953
995
|
}
|
|
954
996
|
const gateReason = `Advisor loop review: ${result.markdown}`;
|
|
955
997
|
if (result.decision === "blocked") {
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
998
|
+
const effect = blockedDecisionEffect(
|
|
999
|
+
gateReason,
|
|
1000
|
+
ctx,
|
|
1001
|
+
session,
|
|
1002
|
+
failureMode
|
|
1003
|
+
);
|
|
1004
|
+
return effect.block ? { block: true, reason: effect.reason } : undefined;
|
|
961
1005
|
}
|
|
962
1006
|
return { block: true, reason: gateReason };
|
|
963
1007
|
} finally {
|
|
@@ -1074,6 +1118,11 @@ export class ScoutStatusManager {
|
|
|
1074
1118
|
readonly #active = new Set<symbol>();
|
|
1075
1119
|
readonly #known = new Set<symbol>();
|
|
1076
1120
|
readonly #retired = new Set<symbol>();
|
|
1121
|
+
private readonly showStatus: boolean;
|
|
1122
|
+
|
|
1123
|
+
constructor(showStatus = true) {
|
|
1124
|
+
this.showStatus = showStatus;
|
|
1125
|
+
}
|
|
1077
1126
|
|
|
1078
1127
|
register(token: symbol) {
|
|
1079
1128
|
if (!this.#retired.has(token)) {
|
|
@@ -1088,7 +1137,9 @@ export class ScoutStatusManager {
|
|
|
1088
1137
|
this.#known.add(token);
|
|
1089
1138
|
if (event.type === "call" || event.type === "chunk") {
|
|
1090
1139
|
this.#active.add(token);
|
|
1091
|
-
|
|
1140
|
+
if (this.showStatus) {
|
|
1141
|
+
ctx.ui.setStatus("advisor-scout", "Scout curating…");
|
|
1142
|
+
}
|
|
1092
1143
|
return;
|
|
1093
1144
|
}
|
|
1094
1145
|
this.release(ctx, token);
|
|
@@ -1098,7 +1149,7 @@ export class ScoutStatusManager {
|
|
|
1098
1149
|
this.#active.delete(token);
|
|
1099
1150
|
this.#known.delete(token);
|
|
1100
1151
|
this.#retired.add(token);
|
|
1101
|
-
if (!ctx.hasUI) {
|
|
1152
|
+
if (!(ctx.hasUI && this.showStatus)) {
|
|
1102
1153
|
return;
|
|
1103
1154
|
}
|
|
1104
1155
|
ctx.ui.setStatus(
|
|
@@ -1113,7 +1164,7 @@ export class ScoutStatusManager {
|
|
|
1113
1164
|
}
|
|
1114
1165
|
this.#known.clear();
|
|
1115
1166
|
this.#active.clear();
|
|
1116
|
-
if (ctx.hasUI) {
|
|
1167
|
+
if (ctx.hasUI && this.showStatus) {
|
|
1117
1168
|
ctx.ui.setStatus("advisor-scout", undefined);
|
|
1118
1169
|
}
|
|
1119
1170
|
}
|
|
@@ -1154,9 +1205,11 @@ export const renderScoutDetails = (
|
|
|
1154
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`}`
|
|
1155
1206
|
),
|
|
1156
1207
|
];
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1208
|
+
if (getAdvisorSettings().showUsageDetails) {
|
|
1209
|
+
const usage = formatAdvisorUsage(scout.usage);
|
|
1210
|
+
if (usage) {
|
|
1211
|
+
lines.push(theme.fg("dim", ` Usage: ${usage}`));
|
|
1212
|
+
}
|
|
1160
1213
|
}
|
|
1161
1214
|
if (scout.fallbackReason) {
|
|
1162
1215
|
lines.push(theme.fg("warning", ` ${scout.fallbackReason}`));
|
|
@@ -1278,9 +1331,11 @@ const renderFinalAdvisorResult = (
|
|
|
1278
1331
|
if (details?.advisor) {
|
|
1279
1332
|
lines.push(theme.fg("dim", ` ${details.advisor}`));
|
|
1280
1333
|
}
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1334
|
+
if (getAdvisorSettings().showUsageDetails) {
|
|
1335
|
+
const usage = formatAdvisorUsage(details?.usage);
|
|
1336
|
+
if (usage) {
|
|
1337
|
+
lines.push(theme.fg("dim", ` Usage: ${usage}`));
|
|
1338
|
+
}
|
|
1284
1339
|
}
|
|
1285
1340
|
const attachments = [
|
|
1286
1341
|
details?.draftBytes
|
|
@@ -1344,10 +1399,14 @@ export const registerAdvisorTool = (
|
|
|
1344
1399
|
pi: ExtensionAPI,
|
|
1345
1400
|
session: AdvisorSessionState = advisorSessionState,
|
|
1346
1401
|
dependencies: {
|
|
1402
|
+
appendOutcome?: typeof appendOutcome;
|
|
1403
|
+
consult?: typeof consultAdvisor;
|
|
1347
1404
|
runGate?: typeof runAdvisorGate;
|
|
1348
1405
|
statusManager?: ScoutStatusManager;
|
|
1349
1406
|
} = {}
|
|
1350
1407
|
) => {
|
|
1408
|
+
const appendAdvisorOutcome = dependencies.appendOutcome ?? appendOutcome;
|
|
1409
|
+
const requestAdvisor = dependencies.consult ?? consultAdvisor;
|
|
1351
1410
|
const reservedCalls = new Set<string>();
|
|
1352
1411
|
const scoutStatus = dependencies.statusManager ?? new ScoutStatusManager();
|
|
1353
1412
|
|
|
@@ -1394,9 +1453,11 @@ export const registerAdvisorTool = (
|
|
|
1394
1453
|
if (details?.advisor) {
|
|
1395
1454
|
box.addChild(new Text(theme.fg("dim", ` ${details.advisor}`), 0, 0));
|
|
1396
1455
|
}
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
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
|
+
}
|
|
1400
1461
|
}
|
|
1401
1462
|
if (details?.text) {
|
|
1402
1463
|
box.addChild(
|
|
@@ -1505,7 +1566,6 @@ export const registerAdvisorTool = (
|
|
|
1505
1566
|
pi.registerTool({
|
|
1506
1567
|
description:
|
|
1507
1568
|
"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.
|
|
1509
1569
|
async execute(_id, params, signal, onUpdate, ctx) {
|
|
1510
1570
|
reservedCalls.delete(_id);
|
|
1511
1571
|
if (
|
|
@@ -1524,13 +1584,24 @@ export const registerAdvisorTool = (
|
|
|
1524
1584
|
}
|
|
1525
1585
|
herdrAdvisorActivity.start();
|
|
1526
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
|
+
};
|
|
1527
1598
|
try {
|
|
1528
|
-
const result = await
|
|
1599
|
+
const result = await requestAdvisor(
|
|
1529
1600
|
ctx,
|
|
1530
1601
|
resolveAdvisorRequest(params.question),
|
|
1531
1602
|
signal,
|
|
1532
1603
|
(t, tx) =>
|
|
1533
|
-
|
|
1604
|
+
coalescedUpdate.update({
|
|
1534
1605
|
content: [{ text: tx, type: "text" }],
|
|
1535
1606
|
details: {
|
|
1536
1607
|
advisor: advisorRef,
|
|
@@ -1548,7 +1619,7 @@ export const registerAdvisorTool = (
|
|
|
1548
1619
|
params.includeTrackedFiles,
|
|
1549
1620
|
(event) => {
|
|
1550
1621
|
scoutDetails = scoutDetailsFromEvent(event, scoutDetails);
|
|
1551
|
-
|
|
1622
|
+
coalescedUpdate.update({
|
|
1552
1623
|
content: [{ text: scoutDetails.text ?? "", type: "text" }],
|
|
1553
1624
|
details: {
|
|
1554
1625
|
advisor: advisorRef,
|
|
@@ -1559,6 +1630,7 @@ export const registerAdvisorTool = (
|
|
|
1559
1630
|
},
|
|
1560
1631
|
_id
|
|
1561
1632
|
);
|
|
1633
|
+
flushUpdate();
|
|
1562
1634
|
session.issueAdvice(
|
|
1563
1635
|
result.adviceId,
|
|
1564
1636
|
result.markdown,
|
|
@@ -1575,9 +1647,7 @@ export const registerAdvisorTool = (
|
|
|
1575
1647
|
});
|
|
1576
1648
|
const usage = snapshotAdvisorUsage(result.usage);
|
|
1577
1649
|
const piUsage = advisorUsageForPi(result.usage);
|
|
1578
|
-
|
|
1579
|
-
ctx.ui.setStatus("advisor-usage", session.usageStatus());
|
|
1580
|
-
}
|
|
1650
|
+
updateAdvisorUsageStatus(ctx, session);
|
|
1581
1651
|
return {
|
|
1582
1652
|
content: [
|
|
1583
1653
|
{
|
|
@@ -1601,6 +1671,10 @@ export const registerAdvisorTool = (
|
|
|
1601
1671
|
...(piUsage ? { usage: piUsage } : {}),
|
|
1602
1672
|
};
|
|
1603
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();
|
|
1604
1678
|
const message = error instanceof Error ? error.message : String(error);
|
|
1605
1679
|
session.recordInvocation({
|
|
1606
1680
|
executionEffect: "continued",
|
|
@@ -1609,13 +1683,12 @@ export const registerAdvisorTool = (
|
|
|
1609
1683
|
model: advisorRef,
|
|
1610
1684
|
trigger: "executor-requested",
|
|
1611
1685
|
});
|
|
1612
|
-
|
|
1613
|
-
ctx.ui.setStatus("advisor-usage", session.usageStatus());
|
|
1614
|
-
}
|
|
1686
|
+
updateAdvisorUsageStatus(ctx, session);
|
|
1615
1687
|
notifyLocalFailure(ctx, message);
|
|
1616
1688
|
notifyHerdrAdvisorFailure("Advisor consultation failed", message);
|
|
1617
1689
|
throw error;
|
|
1618
1690
|
} finally {
|
|
1691
|
+
coalescedUpdate.cancel();
|
|
1619
1692
|
herdrAdvisorActivity.finish();
|
|
1620
1693
|
}
|
|
1621
1694
|
},
|
|
@@ -1692,18 +1765,19 @@ export const registerAdvisorTool = (
|
|
|
1692
1765
|
details: { recorded: false },
|
|
1693
1766
|
};
|
|
1694
1767
|
}
|
|
1695
|
-
const advice = session.
|
|
1768
|
+
const advice = session.reserveAdvice(params.adviceId);
|
|
1696
1769
|
if (!advice) {
|
|
1697
|
-
throw new Error("Unknown
|
|
1770
|
+
throw new Error("Unknown, already recorded, or pending adviceId.");
|
|
1698
1771
|
}
|
|
1699
1772
|
try {
|
|
1700
|
-
await
|
|
1773
|
+
await appendAdvisorOutcome({
|
|
1701
1774
|
adoption: params.adoption as (typeof ADOPTIONS)[number],
|
|
1702
1775
|
advice: advice.advice,
|
|
1703
1776
|
trigger: advice.trigger,
|
|
1704
1777
|
validationStatus:
|
|
1705
1778
|
params.validationStatus as (typeof VALIDATIONS)[number],
|
|
1706
1779
|
});
|
|
1780
|
+
session.commitAdvice(params.adviceId);
|
|
1707
1781
|
return {
|
|
1708
1782
|
content: [
|
|
1709
1783
|
{ text: "Advisor outcome recorded locally.", type: "text" },
|
|
@@ -1711,6 +1785,7 @@ export const registerAdvisorTool = (
|
|
|
1711
1785
|
details: { recorded: true },
|
|
1712
1786
|
};
|
|
1713
1787
|
} catch {
|
|
1788
|
+
session.releaseAdvice(params.adviceId);
|
|
1714
1789
|
if (ctx.hasUI) {
|
|
1715
1790
|
ctx.ui.notify(
|
|
1716
1791
|
"Advisor outcome could not be recorded locally.",
|