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/CHANGELOG.md +38 -0
- package/README.md +11 -5
- package/extensions/index.ts +0 -10
- package/package.json +11 -13
- package/src/commands.ts +355 -118
- package/src/config.ts +29 -3
- package/src/model-stream.ts +123 -0
- package/src/scout-context.ts +34 -8
- package/src/scout.ts +7 -34
- package/src/session-state.ts +48 -2
- package/src/tools.ts +208 -121
- package/src/ui.ts +1060 -600
- package/src/usage.ts +199 -0
- package/src/telemetry.ts +0 -270
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 = "
|
|
17
|
-
export const FALLBACK_ADVISOR = "
|
|
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;
|
|
@@ -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,
|
|
@@ -208,9 +218,13 @@ export const getAdvisorSettings = () => ({
|
|
|
208
218
|
untrackedContent: advisorUntrackedContentRef,
|
|
209
219
|
});
|
|
210
220
|
|
|
221
|
+
/** Reads the live session budget at each enforcement boundary. */
|
|
222
|
+
export const getAdvisorMaxCallsPerSession = () =>
|
|
223
|
+
getAdvisorSettings().maxCallsPerSession;
|
|
224
|
+
|
|
211
225
|
export const splitRef = (ref: string): [string, string] => {
|
|
212
226
|
const i = ref.indexOf("/");
|
|
213
|
-
return i === -1 ? ["
|
|
227
|
+
return i === -1 ? ["openai-codex", ref] : [ref.slice(0, i), ref.slice(i + 1)];
|
|
214
228
|
};
|
|
215
229
|
|
|
216
230
|
export const configPaths = (ctx: ExtensionContext) => [
|
|
@@ -249,6 +263,8 @@ export interface AdvisorConfig {
|
|
|
249
263
|
executor?: string;
|
|
250
264
|
executorEffort?: string;
|
|
251
265
|
gateFailureMode?: GateFailureMode;
|
|
266
|
+
showUsageDetails?: boolean;
|
|
267
|
+
showUsageFooter?: boolean;
|
|
252
268
|
simpleMode?: boolean;
|
|
253
269
|
}
|
|
254
270
|
|
|
@@ -269,6 +285,7 @@ const CONFIG_KEYS = new Set<keyof AdvisorConfig>([
|
|
|
269
285
|
"advisorPlanGate",
|
|
270
286
|
"advisorSessionSummary",
|
|
271
287
|
"advisorScoutEnabled",
|
|
288
|
+
"showUsageDetails",
|
|
272
289
|
"simpleMode",
|
|
273
290
|
"alwaysOn",
|
|
274
291
|
"advisorToolResultMaxBytes",
|
|
@@ -282,6 +299,7 @@ const CONFIG_KEYS = new Set<keyof AdvisorConfig>([
|
|
|
282
299
|
"executor",
|
|
283
300
|
"executorEffort",
|
|
284
301
|
"gateFailureMode",
|
|
302
|
+
"showUsageFooter",
|
|
285
303
|
]);
|
|
286
304
|
const BOOLEAN_CONFIG_KEYS = [
|
|
287
305
|
"advisorPlanGate",
|
|
@@ -292,6 +310,8 @@ const BOOLEAN_CONFIG_KEYS = [
|
|
|
292
310
|
"advisorAutoLoopGate",
|
|
293
311
|
"advisorSessionSummary",
|
|
294
312
|
"advisorScoutEnabled",
|
|
313
|
+
"showUsageDetails",
|
|
314
|
+
"showUsageFooter",
|
|
295
315
|
"simpleMode",
|
|
296
316
|
"alwaysOn",
|
|
297
317
|
"advisorHerdrIntegration",
|
|
@@ -478,6 +498,8 @@ const resetDefaults = () => {
|
|
|
478
498
|
advisorUntrackedContentRef = false;
|
|
479
499
|
advisorTrackedFileContentRef = false;
|
|
480
500
|
advisorScoutEnabledRef = false;
|
|
501
|
+
showUsageDetailsRef = true;
|
|
502
|
+
showUsageFooterRef = false;
|
|
481
503
|
};
|
|
482
504
|
|
|
483
505
|
const applyOptionalConfig = <Key extends keyof AdvisorConfig>(
|
|
@@ -545,6 +567,8 @@ const applyConfig = (config: AdvisorConfig) => {
|
|
|
545
567
|
setAdvisorSessionSummaryRef
|
|
546
568
|
);
|
|
547
569
|
applyOptionalConfig(config, "advisorScoutEnabled", setAdvisorScoutEnabledRef);
|
|
570
|
+
applyOptionalConfig(config, "showUsageDetails", setShowUsageDetailsRef);
|
|
571
|
+
applyOptionalConfig(config, "showUsageFooter", setShowUsageFooterRef);
|
|
548
572
|
applyOptionalConfig(config, "simpleMode", setSimpleModeRef);
|
|
549
573
|
applyOptionalConfig(config, "alwaysOn", setAlwaysOnRef);
|
|
550
574
|
applyOptionalConfig(config, "gateFailureMode", setAdvisorFailureModeRef);
|
|
@@ -712,6 +736,8 @@ export const saveConfig = (_ctx: ExtensionContext) => {
|
|
|
712
736
|
advisorUntrackedContent: advisorUntrackedContentRef,
|
|
713
737
|
alwaysOn: alwaysOnRef,
|
|
714
738
|
gateFailureMode: advisorFailureModeRef,
|
|
739
|
+
showUsageDetails: showUsageDetailsRef,
|
|
740
|
+
showUsageFooter: showUsageFooterRef,
|
|
715
741
|
simpleMode: simpleModeRef,
|
|
716
742
|
};
|
|
717
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/scout-context.ts
CHANGED
|
@@ -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
|
|
195
|
-
|
|
196
|
-
|
|
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 (
|
|
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) >
|
|
451
|
+
required.reduce((sum, group) => sum + groupWireBytes(group), 0) >
|
|
452
|
+
maxManifestBytes
|
|
443
453
|
) {
|
|
444
454
|
return {
|
|
445
455
|
message:
|
|
446
|
-
"Required Scout context exceeds
|
|
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) <=
|
|
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
|
|
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: {
|
|
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,
|
package/src/session-state.ts
CHANGED
|
@@ -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 =
|
|
@@ -116,10 +124,12 @@ export class AdvisorSessionState {
|
|
|
116
124
|
string,
|
|
117
125
|
{ advice: string; trigger: ConsultationTrigger }
|
|
118
126
|
>();
|
|
127
|
+
readonly #pendingAdvice = new Set<string>();
|
|
119
128
|
readonly #reportedAdvice = new Set<string>();
|
|
120
129
|
#draftConsultations = 0;
|
|
121
130
|
#outcomes = 0;
|
|
122
131
|
#lastAdvice?: string;
|
|
132
|
+
#usage = emptyAdvisorUsageTotals();
|
|
123
133
|
|
|
124
134
|
resetTask() {
|
|
125
135
|
this.#previousSignature = undefined;
|
|
@@ -129,10 +139,12 @@ export class AdvisorSessionState {
|
|
|
129
139
|
this.#loopInterventions = 0;
|
|
130
140
|
this.#consumedCalls = 0;
|
|
131
141
|
this.#issuedAdvice.clear();
|
|
142
|
+
this.#pendingAdvice.clear();
|
|
132
143
|
this.#reportedAdvice.clear();
|
|
133
144
|
this.#draftConsultations = 0;
|
|
134
145
|
this.#outcomes = 0;
|
|
135
146
|
this.#lastAdvice = undefined;
|
|
147
|
+
this.#usage = emptyAdvisorUsageTotals();
|
|
136
148
|
}
|
|
137
149
|
|
|
138
150
|
clearBlocked() {
|
|
@@ -182,8 +194,19 @@ export class AdvisorSessionState {
|
|
|
182
194
|
return this.#consumedCalls;
|
|
183
195
|
}
|
|
184
196
|
|
|
197
|
+
/** Returns a copy of cumulative direct Advisor usage for this session. */
|
|
198
|
+
get usageTotals(): AdvisorUsageTotals {
|
|
199
|
+
return { ...this.#usage };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Returns the footer-ready direct Advisor usage status for this session. */
|
|
203
|
+
usageStatus() {
|
|
204
|
+
return formatAdvisorUsageStatus(this.#usage);
|
|
205
|
+
}
|
|
206
|
+
|
|
185
207
|
recordInvocation(record: AdvisorInvocationRecord) {
|
|
186
208
|
this.#invocations.push(record);
|
|
209
|
+
addAdvisorUsage(this.#usage, record.usage);
|
|
187
210
|
}
|
|
188
211
|
issueAdvice(
|
|
189
212
|
id: string,
|
|
@@ -216,16 +239,38 @@ export class AdvisorSessionState {
|
|
|
216
239
|
return true;
|
|
217
240
|
}
|
|
218
241
|
|
|
219
|
-
|
|
220
|
-
if (this.#reportedAdvice.has(id)) {
|
|
242
|
+
reserveAdvice(id: string) {
|
|
243
|
+
if (this.#reportedAdvice.has(id) || this.#pendingAdvice.has(id)) {
|
|
221
244
|
return;
|
|
222
245
|
}
|
|
223
246
|
const advice = this.#issuedAdvice.get(id);
|
|
224
247
|
if (!advice) {
|
|
225
248
|
return;
|
|
226
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
|
+
}
|
|
227
258
|
this.#reportedAdvice.add(id);
|
|
228
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);
|
|
229
274
|
return advice;
|
|
230
275
|
}
|
|
231
276
|
|
|
@@ -271,6 +316,7 @@ export class AdvisorSessionState {
|
|
|
271
316
|
`Triggers: ${["manual", "executor-requested", "repeated-tool-call", "completion-review", "custom-rule"].filter((trigger) => countTrigger(trigger as AdvisorTrigger) > 0).join(", ") || "none"}`,
|
|
272
317
|
`Models: ${models}`,
|
|
273
318
|
`Budget: ${budget}`,
|
|
319
|
+
`Usage: ${formatAdvisorUsageTotals(this.#usage)}`,
|
|
274
320
|
`Markdown advice: ${markdown.length} responses (${this.#draftConsultations} with drafts)`,
|
|
275
321
|
`Outcome reports: ${this.#outcomes}`,
|
|
276
322
|
`Gate decisions: ${decisions}`,
|