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 +15 -0
- package/README.md +5 -2
- package/extensions/index.ts +0 -10
- package/package.json +1 -3
- package/src/commands.ts +32 -9
- package/src/config.ts +7 -3
- package/src/scout-context.ts +34 -8
- package/src/scout.ts +7 -34
- package/src/session-state.ts +22 -0
- package/src/tools.ts +108 -96
- package/src/usage.ts +199 -0
- package/src/telemetry.ts +0 -270
package/src/tools.ts
CHANGED
|
@@ -24,7 +24,6 @@ import {
|
|
|
24
24
|
advisorGitContextMaxCharsRef,
|
|
25
25
|
advisorGitContextRef,
|
|
26
26
|
advisorLoopThresholdRef,
|
|
27
|
-
advisorMaxCallsPerSessionRef,
|
|
28
27
|
advisorOutcomeLoggingRef,
|
|
29
28
|
advisorPlanGateRef,
|
|
30
29
|
advisorRedactSecretsRef,
|
|
@@ -35,6 +34,7 @@ import {
|
|
|
35
34
|
advisorUntrackedContentRef,
|
|
36
35
|
contextMaxCharsRef,
|
|
37
36
|
executorRef,
|
|
37
|
+
getAdvisorMaxCallsPerSession,
|
|
38
38
|
isSimpleMode,
|
|
39
39
|
loadConfig,
|
|
40
40
|
} from "./config.js";
|
|
@@ -68,6 +68,7 @@ import {
|
|
|
68
68
|
import {
|
|
69
69
|
buildScoutManifest,
|
|
70
70
|
reconstructScoutConversation,
|
|
71
|
+
SCOUT_MANIFEST_MAX_BYTES,
|
|
71
72
|
} from "./scout-context.js";
|
|
72
73
|
import {
|
|
73
74
|
AdvisorSessionState,
|
|
@@ -75,8 +76,13 @@ import {
|
|
|
75
76
|
type GateDecision,
|
|
76
77
|
type GateTrigger,
|
|
77
78
|
} from "./session-state.js";
|
|
78
|
-
import type { BenchmarkTelemetry } from "./telemetry.js";
|
|
79
79
|
import { readTrackedFiles, readUntrackedFiles } from "./untracked.js";
|
|
80
|
+
import {
|
|
81
|
+
advisorUsageCost,
|
|
82
|
+
advisorUsageForPi,
|
|
83
|
+
formatAdvisorUsage,
|
|
84
|
+
snapshotAdvisorUsage,
|
|
85
|
+
} from "./usage.js";
|
|
80
86
|
|
|
81
87
|
export type {
|
|
82
88
|
AdvisorInvocationRecord,
|
|
@@ -302,6 +308,7 @@ export interface AdvisorGateFailure {
|
|
|
302
308
|
markdown?: string;
|
|
303
309
|
message: string;
|
|
304
310
|
ok: false;
|
|
311
|
+
usage?: unknown;
|
|
305
312
|
}
|
|
306
313
|
export interface AdvisorConsultationResult {
|
|
307
314
|
adviceId: string;
|
|
@@ -327,14 +334,6 @@ export interface AdvisorGateResult {
|
|
|
327
334
|
}
|
|
328
335
|
export type AdvisorGateOutcome = AdvisorGateResult | AdvisorGateFailure;
|
|
329
336
|
|
|
330
|
-
export const advisorUsageCost = (usage: unknown): number | undefined => {
|
|
331
|
-
const value = usage as
|
|
332
|
-
| { cost?: { total?: unknown }; totalCost?: unknown }
|
|
333
|
-
| undefined;
|
|
334
|
-
const cost = value?.cost?.total ?? value?.totalCost;
|
|
335
|
-
return typeof cost === "number" ? cost : undefined;
|
|
336
|
-
};
|
|
337
|
-
|
|
338
337
|
const DECISION_LINE = /^Decision\s*:\s*(proceed|revise|blocked)\s*$/i;
|
|
339
338
|
const CODE_FENCE = /^(?:```|~~~)/;
|
|
340
339
|
const LINE_BREAK = /\r?\n/;
|
|
@@ -431,8 +430,7 @@ export const curateAdvisorConversation = async (
|
|
|
431
430
|
enabled = advisorScoutEnabledRef,
|
|
432
431
|
runScout: typeof runAdvisorScout = runAdvisorScout,
|
|
433
432
|
currentInvocationId?: string,
|
|
434
|
-
maxChars?: number
|
|
435
|
-
telemetry?: BenchmarkTelemetry
|
|
433
|
+
maxChars?: number
|
|
436
434
|
): Promise<{
|
|
437
435
|
conversation: string;
|
|
438
436
|
scout?: Exclude<ScoutOutcome, { cancelled: true }>;
|
|
@@ -445,7 +443,8 @@ export const curateAdvisorConversation = async (
|
|
|
445
443
|
}
|
|
446
444
|
const built = buildScoutManifest(ctx, {
|
|
447
445
|
currentInvocationId,
|
|
448
|
-
|
|
446
|
+
maxConversationChars: maxChars,
|
|
447
|
+
maxManifestBytes: SCOUT_MANIFEST_MAX_BYTES,
|
|
449
448
|
});
|
|
450
449
|
if (!built.ok) {
|
|
451
450
|
const scout: Exclude<ScoutOutcome, { cancelled: true }> = {
|
|
@@ -470,8 +469,7 @@ export const curateAdvisorConversation = async (
|
|
|
470
469
|
signal,
|
|
471
470
|
onScout,
|
|
472
471
|
undefined,
|
|
473
|
-
undefined
|
|
474
|
-
telemetry
|
|
472
|
+
undefined
|
|
475
473
|
);
|
|
476
474
|
if (!outcome.ok && outcome.cancelled) {
|
|
477
475
|
throw signal?.reason instanceof Error
|
|
@@ -504,8 +502,7 @@ const collectAdvisorResponse = async (
|
|
|
504
502
|
includeUntracked?: string[],
|
|
505
503
|
includeTracked?: string[],
|
|
506
504
|
onScout?: (event: ScoutLifecycleEvent) => void,
|
|
507
|
-
currentInvocationId?: string
|
|
508
|
-
telemetry?: BenchmarkTelemetry
|
|
505
|
+
currentInvocationId?: string
|
|
509
506
|
) => {
|
|
510
507
|
loadConfig(ctx);
|
|
511
508
|
const resolved = await resolveConfiguredModel(ctx, advisorRef, "Advisor");
|
|
@@ -551,8 +548,7 @@ const collectAdvisorResponse = async (
|
|
|
551
548
|
advisorScoutEnabledRef,
|
|
552
549
|
runAdvisorScout,
|
|
553
550
|
currentInvocationId,
|
|
554
|
-
conversationBudget
|
|
555
|
-
telemetry
|
|
551
|
+
conversationBudget
|
|
556
552
|
);
|
|
557
553
|
const { conversation, scout } = curated;
|
|
558
554
|
const preferences = await readProjectPreferences(
|
|
@@ -643,43 +639,22 @@ export const consultAdvisor = async (
|
|
|
643
639
|
includeUntracked?: string[],
|
|
644
640
|
includeTracked?: string[],
|
|
645
641
|
onScout?: (event: ScoutLifecycleEvent) => void,
|
|
646
|
-
currentInvocationId?: string
|
|
647
|
-
telemetry?: BenchmarkTelemetry
|
|
642
|
+
currentInvocationId?: string
|
|
648
643
|
): Promise<AdvisorConsultationResult> => {
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
telemetry
|
|
664
|
-
);
|
|
665
|
-
telemetry?.advisorEnd({
|
|
666
|
-
model: result.model,
|
|
667
|
-
outcome: "completed",
|
|
668
|
-
question,
|
|
669
|
-
response: result.markdown,
|
|
670
|
-
trigger,
|
|
671
|
-
usage: result.usage,
|
|
672
|
-
});
|
|
673
|
-
return { ...result, adviceId: randomUUID(), trigger };
|
|
674
|
-
} catch (error) {
|
|
675
|
-
telemetry?.advisorError({
|
|
676
|
-
category: signal?.aborted ? "cancelled" : "provider-error",
|
|
677
|
-
model: advisorRef,
|
|
678
|
-
question,
|
|
679
|
-
trigger,
|
|
680
|
-
});
|
|
681
|
-
throw error;
|
|
682
|
-
}
|
|
644
|
+
const result = await collectAdvisorResponse(
|
|
645
|
+
ctx,
|
|
646
|
+
ADVISOR_SYSTEM,
|
|
647
|
+
question,
|
|
648
|
+
signal,
|
|
649
|
+
onChunk,
|
|
650
|
+
gitContext,
|
|
651
|
+
draft,
|
|
652
|
+
includeUntracked,
|
|
653
|
+
includeTracked,
|
|
654
|
+
onScout,
|
|
655
|
+
currentInvocationId
|
|
656
|
+
);
|
|
657
|
+
return { ...result, adviceId: randomUUID(), trigger };
|
|
683
658
|
};
|
|
684
659
|
|
|
685
660
|
export const runAdvisorGate = async (
|
|
@@ -689,10 +664,8 @@ export const runAdvisorGate = async (
|
|
|
689
664
|
signal?: AbortSignal,
|
|
690
665
|
onChunk?: (thinking: string, text: string) => void,
|
|
691
666
|
onScout?: (event: ScoutLifecycleEvent) => void,
|
|
692
|
-
currentInvocationId?: string
|
|
693
|
-
telemetry?: BenchmarkTelemetry
|
|
667
|
+
currentInvocationId?: string
|
|
694
668
|
): Promise<AdvisorGateOutcome> => {
|
|
695
|
-
telemetry?.advisorStart({ model: advisorRef, question, trigger });
|
|
696
669
|
try {
|
|
697
670
|
const result = await collectAdvisorResponse(
|
|
698
671
|
ctx,
|
|
@@ -705,20 +678,11 @@ export const runAdvisorGate = async (
|
|
|
705
678
|
undefined,
|
|
706
679
|
undefined,
|
|
707
680
|
onScout,
|
|
708
|
-
currentInvocationId
|
|
709
|
-
telemetry
|
|
681
|
+
currentInvocationId
|
|
710
682
|
);
|
|
711
683
|
const parsed = parseAutomaticDecision(result.markdown);
|
|
712
|
-
telemetry?.advisorEnd({
|
|
713
|
-
model: result.model,
|
|
714
|
-
outcome: parsed.ok ? `decision:${parsed.decision}` : parsed.category,
|
|
715
|
-
question,
|
|
716
|
-
response: result.markdown,
|
|
717
|
-
trigger,
|
|
718
|
-
usage: result.usage,
|
|
719
|
-
});
|
|
720
684
|
if (!parsed.ok) {
|
|
721
|
-
return parsed;
|
|
685
|
+
return { ...parsed, usage: result.usage };
|
|
722
686
|
}
|
|
723
687
|
return {
|
|
724
688
|
...parsed,
|
|
@@ -728,12 +692,6 @@ export const runAdvisorGate = async (
|
|
|
728
692
|
usage: result.usage,
|
|
729
693
|
};
|
|
730
694
|
} catch (error) {
|
|
731
|
-
telemetry?.advisorError({
|
|
732
|
-
category: signal?.aborted ? "cancelled" : "provider-error",
|
|
733
|
-
model: advisorRef,
|
|
734
|
-
question,
|
|
735
|
-
trigger,
|
|
736
|
-
});
|
|
737
695
|
if (signal?.aborted) {
|
|
738
696
|
throw error;
|
|
739
697
|
}
|
|
@@ -749,6 +707,15 @@ export const runAdvisorGate = async (
|
|
|
749
707
|
}
|
|
750
708
|
};
|
|
751
709
|
|
|
710
|
+
const updateAdvisorUsageStatus = (
|
|
711
|
+
ctx: ExtensionContext,
|
|
712
|
+
session: AdvisorSessionState
|
|
713
|
+
) => {
|
|
714
|
+
if (ctx.hasUI) {
|
|
715
|
+
ctx.ui.setStatus("advisor-usage", session.usageStatus());
|
|
716
|
+
}
|
|
717
|
+
};
|
|
718
|
+
|
|
752
719
|
const notifyLocalFailure = (
|
|
753
720
|
ctx: ExtensionContext,
|
|
754
721
|
message: string,
|
|
@@ -814,7 +781,7 @@ const reserveAdvisorCall = (
|
|
|
814
781
|
if (event.toolName !== "ask_advisor" || isSimpleMode()) {
|
|
815
782
|
return;
|
|
816
783
|
}
|
|
817
|
-
if (!session.canConsult(
|
|
784
|
+
if (!session.canConsult(getAdvisorMaxCallsPerSession())) {
|
|
818
785
|
const message = "Advisor call budget exhausted for this session.";
|
|
819
786
|
if (ctx.hasUI) {
|
|
820
787
|
ctx.ui.notify(message, "warning");
|
|
@@ -840,12 +807,20 @@ const sendAutomaticGateCall = (pi: ExtensionAPI, event: ToolCallEvent) => {
|
|
|
840
807
|
);
|
|
841
808
|
};
|
|
842
809
|
|
|
843
|
-
const sendAutomaticGateFailure = (
|
|
810
|
+
const sendAutomaticGateFailure = (
|
|
811
|
+
pi: ExtensionAPI,
|
|
812
|
+
markdown: string,
|
|
813
|
+
usage?: unknown
|
|
814
|
+
) => {
|
|
815
|
+
const normalizedUsage = snapshotAdvisorUsage(usage);
|
|
844
816
|
pi.sendMessage(
|
|
845
817
|
{
|
|
846
818
|
content: markdown,
|
|
847
819
|
customType: "advisor-loop-result",
|
|
848
|
-
details: {
|
|
820
|
+
details: {
|
|
821
|
+
text: markdown,
|
|
822
|
+
...(normalizedUsage ? { usage: normalizedUsage } : {}),
|
|
823
|
+
},
|
|
849
824
|
display: true,
|
|
850
825
|
},
|
|
851
826
|
{ deliverAs: "steer" }
|
|
@@ -864,6 +839,9 @@ const sendAutomaticGateResult = (
|
|
|
864
839
|
advisor: result.model,
|
|
865
840
|
decision: result.decision,
|
|
866
841
|
text: result.markdown,
|
|
842
|
+
...(snapshotAdvisorUsage(result.usage)
|
|
843
|
+
? { usage: snapshotAdvisorUsage(result.usage) }
|
|
844
|
+
: {}),
|
|
867
845
|
},
|
|
868
846
|
display: true,
|
|
869
847
|
},
|
|
@@ -877,8 +855,7 @@ const handleAutomaticGate = async (
|
|
|
877
855
|
ctx: ExtensionContext,
|
|
878
856
|
session: AdvisorSessionState,
|
|
879
857
|
runGate: typeof runAdvisorGate,
|
|
880
|
-
scoutStatus: ScoutStatusManager
|
|
881
|
-
telemetry?: BenchmarkTelemetry
|
|
858
|
+
scoutStatus: ScoutStatusManager
|
|
882
859
|
): Promise<ToolCallEventResult | undefined> => {
|
|
883
860
|
if (
|
|
884
861
|
isSimpleMode() ||
|
|
@@ -893,7 +870,7 @@ const handleAutomaticGate = async (
|
|
|
893
870
|
return;
|
|
894
871
|
}
|
|
895
872
|
const reason = `Advisor loop gate: normalized signature for ${event.toolName} repeated ${advisorLoopThresholdRef} times without a materially different tool action.`;
|
|
896
|
-
if (!session.canConsult(
|
|
873
|
+
if (!session.canConsult(getAdvisorMaxCallsPerSession())) {
|
|
897
874
|
const failure = failureEffect(
|
|
898
875
|
"budget-exhausted",
|
|
899
876
|
"Advisor gate call budget is exhausted.",
|
|
@@ -931,8 +908,7 @@ const handleAutomaticGate = async (
|
|
|
931
908
|
ensureGateCall();
|
|
932
909
|
}
|
|
933
910
|
},
|
|
934
|
-
event.toolCallId
|
|
935
|
-
telemetry
|
|
911
|
+
event.toolCallId
|
|
936
912
|
);
|
|
937
913
|
ensureGateCall();
|
|
938
914
|
if (!result.ok) {
|
|
@@ -942,7 +918,9 @@ const handleAutomaticGate = async (
|
|
|
942
918
|
kind: "gate",
|
|
943
919
|
model: advisorRef,
|
|
944
920
|
trigger: "repeated-tool-call",
|
|
921
|
+
usage: result.usage,
|
|
945
922
|
});
|
|
923
|
+
updateAdvisorUsageStatus(ctx, session);
|
|
946
924
|
const failure = failureEffect(
|
|
947
925
|
result.category,
|
|
948
926
|
result.message,
|
|
@@ -951,7 +929,8 @@ const handleAutomaticGate = async (
|
|
|
951
929
|
);
|
|
952
930
|
sendAutomaticGateFailure(
|
|
953
931
|
pi,
|
|
954
|
-
`**Advisor gate failure (${result.category}):** ${result.message}
|
|
932
|
+
`**Advisor gate failure (${result.category}):** ${result.message}`,
|
|
933
|
+
result.usage
|
|
955
934
|
);
|
|
956
935
|
return failure.block
|
|
957
936
|
? { block: true, reason: `${reason}\n${failure.reason}` }
|
|
@@ -966,6 +945,7 @@ const handleAutomaticGate = async (
|
|
|
966
945
|
trigger: result.trigger,
|
|
967
946
|
usage: result.usage,
|
|
968
947
|
});
|
|
948
|
+
updateAdvisorUsageStatus(ctx, session);
|
|
969
949
|
sendAutomaticGateResult(pi, result);
|
|
970
950
|
if (result.decision === "proceed") {
|
|
971
951
|
session.resetRepetition();
|
|
@@ -1011,6 +991,7 @@ interface AdvisorToolDetails {
|
|
|
1011
991
|
thinking?: string;
|
|
1012
992
|
trackedBytes?: number;
|
|
1013
993
|
untrackedBytes?: number;
|
|
994
|
+
usage?: unknown;
|
|
1014
995
|
}
|
|
1015
996
|
interface AdvisorRenderState {
|
|
1016
997
|
phase?: string;
|
|
@@ -1059,7 +1040,7 @@ export const scoutDetailsFromEvent = (
|
|
|
1059
1040
|
selectedLabels: outcome.selectedLabels,
|
|
1060
1041
|
status: "curated",
|
|
1061
1042
|
synthesis: outcome.selection.synthesis,
|
|
1062
|
-
usage: outcome.metrics.usage,
|
|
1043
|
+
usage: snapshotAdvisorUsage(outcome.metrics.usage),
|
|
1063
1044
|
}
|
|
1064
1045
|
: {
|
|
1065
1046
|
availableCount: outcome.metrics.availableCount,
|
|
@@ -1069,7 +1050,7 @@ export const scoutDetailsFromEvent = (
|
|
|
1069
1050
|
omittedBeforeScout: outcome.metrics.omittedBeforeScout,
|
|
1070
1051
|
selectedCount: 0,
|
|
1071
1052
|
status: "fallback",
|
|
1072
|
-
usage: outcome.metrics.usage,
|
|
1053
|
+
usage: snapshotAdvisorUsage(outcome.metrics.usage),
|
|
1073
1054
|
};
|
|
1074
1055
|
};
|
|
1075
1056
|
|
|
@@ -1173,6 +1154,10 @@ export const renderScoutDetails = (
|
|
|
1173
1154
|
` ${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`}`
|
|
1174
1155
|
),
|
|
1175
1156
|
];
|
|
1157
|
+
const usage = formatAdvisorUsage(scout.usage);
|
|
1158
|
+
if (usage) {
|
|
1159
|
+
lines.push(theme.fg("dim", ` Usage: ${usage}`));
|
|
1160
|
+
}
|
|
1176
1161
|
if (scout.fallbackReason) {
|
|
1177
1162
|
lines.push(theme.fg("warning", ` ${scout.fallbackReason}`));
|
|
1178
1163
|
}
|
|
@@ -1293,6 +1278,10 @@ const renderFinalAdvisorResult = (
|
|
|
1293
1278
|
if (details?.advisor) {
|
|
1294
1279
|
lines.push(theme.fg("dim", ` ${details.advisor}`));
|
|
1295
1280
|
}
|
|
1281
|
+
const usage = formatAdvisorUsage(details?.usage);
|
|
1282
|
+
if (usage) {
|
|
1283
|
+
lines.push(theme.fg("dim", ` Usage: ${usage}`));
|
|
1284
|
+
}
|
|
1296
1285
|
const attachments = [
|
|
1297
1286
|
details?.draftBytes
|
|
1298
1287
|
? `Draft attached · ${details.draftBytes} B`
|
|
@@ -1357,7 +1346,6 @@ export const registerAdvisorTool = (
|
|
|
1357
1346
|
dependencies: {
|
|
1358
1347
|
runGate?: typeof runAdvisorGate;
|
|
1359
1348
|
statusManager?: ScoutStatusManager;
|
|
1360
|
-
telemetry?: BenchmarkTelemetry;
|
|
1361
1349
|
} = {}
|
|
1362
1350
|
) => {
|
|
1363
1351
|
const reservedCalls = new Set<string>();
|
|
@@ -1385,7 +1373,12 @@ export const registerAdvisorTool = (
|
|
|
1385
1373
|
"advisor-loop-result",
|
|
1386
1374
|
(message, { expanded }, theme) => {
|
|
1387
1375
|
const details = message.details as
|
|
1388
|
-
| {
|
|
1376
|
+
| {
|
|
1377
|
+
advisor?: string;
|
|
1378
|
+
decision?: GateDecision;
|
|
1379
|
+
text?: string;
|
|
1380
|
+
usage?: unknown;
|
|
1381
|
+
}
|
|
1389
1382
|
| undefined;
|
|
1390
1383
|
const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
|
|
1391
1384
|
box.addChild(
|
|
@@ -1401,6 +1394,10 @@ export const registerAdvisorTool = (
|
|
|
1401
1394
|
if (details?.advisor) {
|
|
1402
1395
|
box.addChild(new Text(theme.fg("dim", ` ${details.advisor}`), 0, 0));
|
|
1403
1396
|
}
|
|
1397
|
+
const usage = formatAdvisorUsage(details?.usage);
|
|
1398
|
+
if (usage) {
|
|
1399
|
+
box.addChild(new Text(theme.fg("dim", ` Usage: ${usage}`), 0, 0));
|
|
1400
|
+
}
|
|
1404
1401
|
if (details?.text) {
|
|
1405
1402
|
box.addChild(
|
|
1406
1403
|
new Markdown(
|
|
@@ -1428,10 +1425,13 @@ export const registerAdvisorTool = (
|
|
|
1428
1425
|
}
|
|
1429
1426
|
);
|
|
1430
1427
|
|
|
1431
|
-
pi.on("session_start", () => {
|
|
1428
|
+
pi.on("session_start", (_event, ctx) => {
|
|
1432
1429
|
session.resetTask();
|
|
1433
1430
|
reservedCalls.clear();
|
|
1434
1431
|
herdrAdvisorBlock.clear();
|
|
1432
|
+
if (ctx?.hasUI) {
|
|
1433
|
+
ctx.ui.setStatus("advisor-usage", undefined);
|
|
1434
|
+
}
|
|
1435
1435
|
});
|
|
1436
1436
|
|
|
1437
1437
|
pi.on("before_agent_start", (_event, ctx) => {
|
|
@@ -1442,7 +1442,7 @@ export const registerAdvisorTool = (
|
|
|
1442
1442
|
const guidelines = advisorInvocationGuidelines();
|
|
1443
1443
|
const budget = isSimpleMode()
|
|
1444
1444
|
? undefined
|
|
1445
|
-
: session.remainingCalls(
|
|
1445
|
+
: session.remainingCalls(getAdvisorMaxCallsPerSession());
|
|
1446
1446
|
if (budget !== undefined) {
|
|
1447
1447
|
guidelines.push(
|
|
1448
1448
|
`Advisor calls remaining this session: ${budget}.\nReserve calls for material decisions, repeated failures, or final review.`
|
|
@@ -1476,8 +1476,7 @@ export const registerAdvisorTool = (
|
|
|
1476
1476
|
ctx,
|
|
1477
1477
|
session,
|
|
1478
1478
|
dependencies.runGate ?? runAdvisorGate,
|
|
1479
|
-
scoutStatus
|
|
1480
|
-
dependencies.telemetry
|
|
1479
|
+
scoutStatus
|
|
1481
1480
|
);
|
|
1482
1481
|
});
|
|
1483
1482
|
|
|
@@ -1488,7 +1487,7 @@ export const registerAdvisorTool = (
|
|
|
1488
1487
|
if (isSimpleMode() || session.blocked || !advisorSessionSummaryRef) {
|
|
1489
1488
|
return;
|
|
1490
1489
|
}
|
|
1491
|
-
const summary = session.summary(
|
|
1490
|
+
const summary = session.summary(getAdvisorMaxCallsPerSession());
|
|
1492
1491
|
if (summary && ctx.hasUI) {
|
|
1493
1492
|
ctx.ui.notify(summary, "info");
|
|
1494
1493
|
}
|
|
@@ -1498,11 +1497,15 @@ export const registerAdvisorTool = (
|
|
|
1498
1497
|
reservedCalls.clear();
|
|
1499
1498
|
scoutStatus.clear(ctx);
|
|
1500
1499
|
herdrAdvisorBlock.clear();
|
|
1500
|
+
if (ctx?.hasUI) {
|
|
1501
|
+
ctx.ui.setStatus("advisor-usage", undefined);
|
|
1502
|
+
}
|
|
1501
1503
|
});
|
|
1502
1504
|
|
|
1503
1505
|
pi.registerTool({
|
|
1504
1506
|
description:
|
|
1505
1507
|
"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.
|
|
1506
1509
|
async execute(_id, params, signal, onUpdate, ctx) {
|
|
1507
1510
|
reservedCalls.delete(_id);
|
|
1508
1511
|
if (
|
|
@@ -1514,7 +1517,7 @@ export const registerAdvisorTool = (
|
|
|
1514
1517
|
);
|
|
1515
1518
|
}
|
|
1516
1519
|
if (!isSimpleMode()) {
|
|
1517
|
-
if (!session.canConsult(
|
|
1520
|
+
if (!session.canConsult(getAdvisorMaxCallsPerSession())) {
|
|
1518
1521
|
throw new Error("Advisor call budget exhausted for this session.");
|
|
1519
1522
|
}
|
|
1520
1523
|
session.consumeCall();
|
|
@@ -1554,8 +1557,7 @@ export const registerAdvisorTool = (
|
|
|
1554
1557
|
},
|
|
1555
1558
|
});
|
|
1556
1559
|
},
|
|
1557
|
-
_id
|
|
1558
|
-
dependencies.telemetry
|
|
1560
|
+
_id
|
|
1559
1561
|
);
|
|
1560
1562
|
session.issueAdvice(
|
|
1561
1563
|
result.adviceId,
|
|
@@ -1571,6 +1573,11 @@ export const registerAdvisorTool = (
|
|
|
1571
1573
|
trigger: "executor-requested",
|
|
1572
1574
|
usage: result.usage,
|
|
1573
1575
|
});
|
|
1576
|
+
const usage = snapshotAdvisorUsage(result.usage);
|
|
1577
|
+
const piUsage = advisorUsageForPi(result.usage);
|
|
1578
|
+
if (ctx.hasUI) {
|
|
1579
|
+
ctx.ui.setStatus("advisor-usage", session.usageStatus());
|
|
1580
|
+
}
|
|
1574
1581
|
return {
|
|
1575
1582
|
content: [
|
|
1576
1583
|
{
|
|
@@ -1589,7 +1596,9 @@ export const registerAdvisorTool = (
|
|
|
1589
1596
|
thinking: result.thinkingText,
|
|
1590
1597
|
trackedBytes: result.trackedBytes,
|
|
1591
1598
|
untrackedBytes: result.untrackedBytes,
|
|
1599
|
+
...(usage ? { usage } : {}),
|
|
1592
1600
|
},
|
|
1601
|
+
...(piUsage ? { usage: piUsage } : {}),
|
|
1593
1602
|
};
|
|
1594
1603
|
} catch (error) {
|
|
1595
1604
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -1600,6 +1609,9 @@ export const registerAdvisorTool = (
|
|
|
1600
1609
|
model: advisorRef,
|
|
1601
1610
|
trigger: "executor-requested",
|
|
1602
1611
|
});
|
|
1612
|
+
if (ctx.hasUI) {
|
|
1613
|
+
ctx.ui.setStatus("advisor-usage", session.usageStatus());
|
|
1614
|
+
}
|
|
1603
1615
|
notifyLocalFailure(ctx, message);
|
|
1604
1616
|
notifyHerdrAdvisorFailure("Advisor consultation failed", message);
|
|
1605
1617
|
throw error;
|
package/src/usage.ts
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import type { Usage } from "@earendil-works/pi-ai/compat";
|
|
2
|
+
|
|
3
|
+
/** Normalized usage returned by an Advisor or Scout provider response. */
|
|
4
|
+
export interface AdvisorUsageSnapshot {
|
|
5
|
+
cacheRead?: number;
|
|
6
|
+
cacheWrite?: number;
|
|
7
|
+
cost?: number;
|
|
8
|
+
input?: number;
|
|
9
|
+
output?: number;
|
|
10
|
+
totalTokens?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Session-local totals for direct Advisor model requests. */
|
|
14
|
+
export interface AdvisorUsageTotals {
|
|
15
|
+
cacheRead?: number;
|
|
16
|
+
cacheWrite?: number;
|
|
17
|
+
calls: number;
|
|
18
|
+
cost?: number;
|
|
19
|
+
costCalls: number;
|
|
20
|
+
input?: number;
|
|
21
|
+
knownCalls: number;
|
|
22
|
+
output?: number;
|
|
23
|
+
totalTokens?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const finite = (value: unknown) =>
|
|
27
|
+
typeof value === "number" && Number.isFinite(value) && value >= 0
|
|
28
|
+
? value
|
|
29
|
+
: undefined;
|
|
30
|
+
|
|
31
|
+
const add = (left: number | undefined, right: number | undefined) =>
|
|
32
|
+
left === undefined || right === undefined ? (left ?? right) : left + right;
|
|
33
|
+
|
|
34
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
35
|
+
Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
36
|
+
|
|
37
|
+
const costFields = ["input", "output", "cacheRead", "cacheWrite", "total"];
|
|
38
|
+
|
|
39
|
+
/** Extracts provider-agnostic usage fields without trusting provider metadata. */
|
|
40
|
+
export const snapshotAdvisorUsage = (
|
|
41
|
+
usage: unknown
|
|
42
|
+
): AdvisorUsageSnapshot | undefined => {
|
|
43
|
+
if (!isRecord(usage)) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
const cost = isRecord(usage.cost) ? usage.cost : undefined;
|
|
47
|
+
const snapshot = {
|
|
48
|
+
cacheRead: finite(usage.cacheRead),
|
|
49
|
+
cacheWrite: finite(usage.cacheWrite),
|
|
50
|
+
cost: finite(cost?.total) ?? finite(usage.totalCost) ?? finite(usage.cost),
|
|
51
|
+
input: finite(usage.input),
|
|
52
|
+
output: finite(usage.output),
|
|
53
|
+
totalTokens: finite(usage.totalTokens),
|
|
54
|
+
} satisfies AdvisorUsageSnapshot;
|
|
55
|
+
const hasCostField = costFields.some(
|
|
56
|
+
(field) => finite(cost?.[field]) !== undefined
|
|
57
|
+
);
|
|
58
|
+
return Object.values(snapshot).some((value) => value !== undefined) ||
|
|
59
|
+
hasCostField
|
|
60
|
+
? snapshot
|
|
61
|
+
: undefined;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/** Returns the reported provider cost, when the response includes one. */
|
|
65
|
+
export const advisorUsageCost = (usage: unknown): number | undefined =>
|
|
66
|
+
snapshotAdvisorUsage(usage)?.cost;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Converts supported provider usage to Pi's complete nested-tool usage shape.
|
|
70
|
+
* Missing fields become zero only at this Pi API boundary; absent usage remains
|
|
71
|
+
* undefined so an unavailable request is never presented as a zero-cost call.
|
|
72
|
+
*/
|
|
73
|
+
export const advisorUsageForPi = (usage: unknown): Usage | undefined => {
|
|
74
|
+
const snapshot = snapshotAdvisorUsage(usage);
|
|
75
|
+
if (!(snapshot && isRecord(usage))) {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
const cost = isRecord(usage.cost) ? usage.cost : undefined;
|
|
79
|
+
const input = snapshot.input ?? 0;
|
|
80
|
+
const output = snapshot.output ?? 0;
|
|
81
|
+
const cacheRead = snapshot.cacheRead ?? 0;
|
|
82
|
+
const cacheWrite = snapshot.cacheWrite ?? 0;
|
|
83
|
+
const cacheWrite1h = finite(usage.cacheWrite1h);
|
|
84
|
+
const reasoning = finite(usage.reasoning);
|
|
85
|
+
return {
|
|
86
|
+
cacheRead,
|
|
87
|
+
cacheWrite,
|
|
88
|
+
...(cacheWrite1h === undefined ? {} : { cacheWrite1h }),
|
|
89
|
+
...(reasoning === undefined ? {} : { reasoning }),
|
|
90
|
+
cost: {
|
|
91
|
+
cacheRead: finite(cost?.cacheRead) ?? 0,
|
|
92
|
+
cacheWrite: finite(cost?.cacheWrite) ?? 0,
|
|
93
|
+
input: finite(cost?.input) ?? 0,
|
|
94
|
+
output: finite(cost?.output) ?? 0,
|
|
95
|
+
total: snapshot.cost ?? 0,
|
|
96
|
+
},
|
|
97
|
+
input,
|
|
98
|
+
output,
|
|
99
|
+
totalTokens:
|
|
100
|
+
snapshot.totalTokens ?? input + output + cacheRead + cacheWrite,
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/** Creates empty totals without treating absent usage as zero usage. */
|
|
105
|
+
export const emptyAdvisorUsageTotals = (): AdvisorUsageTotals => ({
|
|
106
|
+
calls: 0,
|
|
107
|
+
costCalls: 0,
|
|
108
|
+
knownCalls: 0,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
/** Adds one direct Advisor response to session-local usage totals. */
|
|
112
|
+
export const addAdvisorUsage = (totals: AdvisorUsageTotals, usage: unknown) => {
|
|
113
|
+
totals.calls += 1;
|
|
114
|
+
const snapshot = snapshotAdvisorUsage(usage);
|
|
115
|
+
if (!snapshot) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
totals.knownCalls += 1;
|
|
119
|
+
totals.cacheRead = add(totals.cacheRead, snapshot.cacheRead);
|
|
120
|
+
totals.cacheWrite = add(totals.cacheWrite, snapshot.cacheWrite);
|
|
121
|
+
totals.input = add(totals.input, snapshot.input);
|
|
122
|
+
totals.output = add(totals.output, snapshot.output);
|
|
123
|
+
totals.totalTokens = add(totals.totalTokens, snapshot.totalTokens);
|
|
124
|
+
if (snapshot.cost !== undefined) {
|
|
125
|
+
totals.cost = add(totals.cost, snapshot.cost);
|
|
126
|
+
totals.costCalls += 1;
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const formatTokens = (value: number) => {
|
|
131
|
+
if (value < 1000) {
|
|
132
|
+
return String(value);
|
|
133
|
+
}
|
|
134
|
+
if (value < 10_000) {
|
|
135
|
+
return `${(value / 1000).toFixed(1)}k`;
|
|
136
|
+
}
|
|
137
|
+
if (value < 1_000_000) {
|
|
138
|
+
return `${Math.round(value / 1000)}k`;
|
|
139
|
+
}
|
|
140
|
+
if (value < 10_000_000) {
|
|
141
|
+
return `${(value / 1_000_000).toFixed(1)}M`;
|
|
142
|
+
}
|
|
143
|
+
return `${Math.round(value / 1_000_000)}M`;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const formatCost = (value: number) => `$${value.toFixed(4)}`;
|
|
147
|
+
|
|
148
|
+
const formatUsageFields = (usage: AdvisorUsageSnapshot): string | undefined => {
|
|
149
|
+
const tokens = [
|
|
150
|
+
usage.input === undefined ? undefined : `↑${formatTokens(usage.input)}`,
|
|
151
|
+
usage.output === undefined ? undefined : `↓${formatTokens(usage.output)}`,
|
|
152
|
+
usage.cacheRead === undefined
|
|
153
|
+
? undefined
|
|
154
|
+
: `cr:${formatTokens(usage.cacheRead)}`,
|
|
155
|
+
usage.cacheWrite === undefined
|
|
156
|
+
? undefined
|
|
157
|
+
: `cw:${formatTokens(usage.cacheWrite)}`,
|
|
158
|
+
].filter((value): value is string => value !== undefined);
|
|
159
|
+
if (tokens.length === 0 && usage.totalTokens !== undefined) {
|
|
160
|
+
tokens.push(`tokens:${formatTokens(usage.totalTokens)}`);
|
|
161
|
+
}
|
|
162
|
+
if (usage.cost !== undefined) {
|
|
163
|
+
tokens.push(formatCost(usage.cost));
|
|
164
|
+
}
|
|
165
|
+
return tokens.join(" · ") || undefined;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
/** Formats one response for an Advisor or Scout result renderer. */
|
|
169
|
+
export const formatAdvisorUsage = (usage: unknown): string | undefined => {
|
|
170
|
+
const snapshot = snapshotAdvisorUsage(usage);
|
|
171
|
+
return snapshot ? formatUsageFields(snapshot) : undefined;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
/** Formats cumulative direct Advisor usage for a footer or session summary. */
|
|
175
|
+
export const formatAdvisorUsageTotals = (
|
|
176
|
+
totals: AdvisorUsageTotals
|
|
177
|
+
): string => {
|
|
178
|
+
const usage = formatUsageFields(totals);
|
|
179
|
+
const missing = totals.calls - totals.knownCalls;
|
|
180
|
+
const parts = [
|
|
181
|
+
usage,
|
|
182
|
+
missing > 0 ? `${missing} without usage data` : undefined,
|
|
183
|
+
];
|
|
184
|
+
return (
|
|
185
|
+
parts.filter((value): value is string => value !== undefined).join(" · ") ||
|
|
186
|
+
"unavailable"
|
|
187
|
+
);
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
/** Formats the current direct Advisor usage status for the Pi footer. */
|
|
191
|
+
export const formatAdvisorUsageStatus = (
|
|
192
|
+
totals: AdvisorUsageTotals
|
|
193
|
+
): string | undefined => {
|
|
194
|
+
if (totals.calls === 0) {
|
|
195
|
+
return undefined;
|
|
196
|
+
}
|
|
197
|
+
const label = `Advisor: ${totals.calls} call${totals.calls === 1 ? "" : "s"}`;
|
|
198
|
+
return `${label} · ${formatAdvisorUsageTotals(totals)}`;
|
|
199
|
+
};
|