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/tools.ts
CHANGED
|
@@ -10,7 +10,14 @@ import {
|
|
|
10
10
|
type ToolCallEventResult,
|
|
11
11
|
type ToolRenderResultOptions,
|
|
12
12
|
} from "@earendil-works/pi-coding-agent";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
Box,
|
|
15
|
+
type Component,
|
|
16
|
+
Markdown,
|
|
17
|
+
Text,
|
|
18
|
+
truncateToWidth,
|
|
19
|
+
visibleWidth,
|
|
20
|
+
} from "@earendil-works/pi-tui";
|
|
14
21
|
import { Type } from "typebox";
|
|
15
22
|
import {
|
|
16
23
|
advisorAutoLoopGateRef,
|
|
@@ -35,6 +42,7 @@ import {
|
|
|
35
42
|
contextMaxCharsRef,
|
|
36
43
|
executorRef,
|
|
37
44
|
getAdvisorMaxCallsPerSession,
|
|
45
|
+
getAdvisorSettings,
|
|
38
46
|
isSimpleMode,
|
|
39
47
|
loadConfig,
|
|
40
48
|
} from "./config.js";
|
|
@@ -57,7 +65,12 @@ import {
|
|
|
57
65
|
herdrAdvisorBlock,
|
|
58
66
|
notifyHerdrAdvisorFailure,
|
|
59
67
|
} from "./herdr.js";
|
|
60
|
-
import {
|
|
68
|
+
import {
|
|
69
|
+
ADVISOR_STREAM_UPDATE_INTERVAL_MS,
|
|
70
|
+
collectTextStream,
|
|
71
|
+
createCoalescedUpdate,
|
|
72
|
+
resolveConfiguredModel,
|
|
73
|
+
} from "./model-stream.js";
|
|
61
74
|
import { ADOPTIONS, appendOutcome, VALIDATIONS } from "./outcomes.js";
|
|
62
75
|
import { readProjectPreferences } from "./preferences.js";
|
|
63
76
|
import {
|
|
@@ -105,6 +118,56 @@ export const SPINNER_FRAMES = [
|
|
|
105
118
|
"⠇",
|
|
106
119
|
"⠏",
|
|
107
120
|
];
|
|
121
|
+
|
|
122
|
+
const THINKING_PREFIX = " 💭 ";
|
|
123
|
+
const THINKING_PREFIX_WIDTH = visibleWidth(THINKING_PREFIX);
|
|
124
|
+
type ThinkingTheme = Pick<Theme, "fg">;
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Renders visible nested-model thinking with the same Markdown semantics as
|
|
128
|
+
* Pi's assistant thinking blocks while keeping the compact speech-bubble cue.
|
|
129
|
+
* The prefix is added after Markdown parsing so it cannot change block syntax.
|
|
130
|
+
*
|
|
131
|
+
* Note: During streaming, incomplete Markdown (e.g., `**text` without closing `**`)
|
|
132
|
+
* displays raw markers transiently until delimiters arrive. This mirrors expected
|
|
133
|
+
* behavior when typing Markdown incrementally and is acceptable in the context
|
|
134
|
+
* of brief thinking previews. When thinking is complete, all markers render.
|
|
135
|
+
*/
|
|
136
|
+
class ThinkingMarkdown implements Component {
|
|
137
|
+
private readonly markdown: Markdown;
|
|
138
|
+
private readonly prefix: string;
|
|
139
|
+
|
|
140
|
+
constructor(thinking: string, theme: ThinkingTheme) {
|
|
141
|
+
this.markdown = new Markdown(thinking.trim(), 0, 0, getMarkdownTheme(), {
|
|
142
|
+
color: (text) => theme.fg("thinkingText", text),
|
|
143
|
+
italic: true,
|
|
144
|
+
});
|
|
145
|
+
this.prefix = theme.fg("thinkingText", THINKING_PREFIX);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
render(width: number): string[] {
|
|
149
|
+
const renderWidth = Math.max(1, Math.floor(width));
|
|
150
|
+
const contentWidth = Math.max(1, renderWidth - THINKING_PREFIX_WIDTH);
|
|
151
|
+
const lines = this.markdown.render(contentWidth);
|
|
152
|
+
return lines.map((line, index) =>
|
|
153
|
+
truncateToWidth(
|
|
154
|
+
index === 0 ? `${this.prefix}${line}` : line,
|
|
155
|
+
renderWidth,
|
|
156
|
+
""
|
|
157
|
+
)
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
invalidate(): void {
|
|
162
|
+
this.markdown.invalidate();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export const renderThinkingMarkdown = (
|
|
167
|
+
thinking: string,
|
|
168
|
+
theme: ThinkingTheme
|
|
169
|
+
): Component => new ThinkingMarkdown(thinking, theme);
|
|
170
|
+
|
|
108
171
|
export const resolveAdvisorRequest = (question?: string) =>
|
|
109
172
|
question?.trim() || undefined;
|
|
110
173
|
export const advisorMessageText = (
|
|
@@ -712,7 +775,10 @@ const updateAdvisorUsageStatus = (
|
|
|
712
775
|
session: AdvisorSessionState
|
|
713
776
|
) => {
|
|
714
777
|
if (ctx.hasUI) {
|
|
715
|
-
ctx.ui.setStatus(
|
|
778
|
+
ctx.ui.setStatus(
|
|
779
|
+
"advisor-usage",
|
|
780
|
+
getAdvisorSettings().showUsageFooter ? session.usageStatus() : undefined
|
|
781
|
+
);
|
|
716
782
|
}
|
|
717
783
|
};
|
|
718
784
|
|
|
@@ -740,12 +806,15 @@ export const gateFailureEffectForMode = (
|
|
|
740
806
|
: ("session-blocked" as const);
|
|
741
807
|
};
|
|
742
808
|
|
|
743
|
-
const gateDecisionEffect = (
|
|
809
|
+
const gateDecisionEffect = (
|
|
810
|
+
decision: GateDecision,
|
|
811
|
+
failureMode: "block-session" | "block-tool" | "warn-and-continue"
|
|
812
|
+
) => {
|
|
744
813
|
if (decision === "proceed") {
|
|
745
814
|
return "continued" as const;
|
|
746
815
|
}
|
|
747
816
|
return decision === "blocked"
|
|
748
|
-
? (
|
|
817
|
+
? gateFailureEffectForMode(failureMode)
|
|
749
818
|
: ("tool-blocked" as const);
|
|
750
819
|
};
|
|
751
820
|
|
|
@@ -753,15 +822,42 @@ const failureEffect = (
|
|
|
753
822
|
category: GateFailureCategory,
|
|
754
823
|
message: string,
|
|
755
824
|
ctx: ExtensionContext,
|
|
756
|
-
session: AdvisorSessionState
|
|
825
|
+
session: AdvisorSessionState,
|
|
826
|
+
failureMode: "block-session" | "block-tool" | "warn-and-continue"
|
|
757
827
|
) => {
|
|
758
828
|
const reason = `Advisor gate ${category}: ${message}`;
|
|
759
|
-
notifyLocalFailure(ctx, message,
|
|
829
|
+
notifyLocalFailure(ctx, message, failureMode === "block-session");
|
|
760
830
|
notifyHerdrAdvisorFailure("Advisor gate failure", reason);
|
|
761
|
-
if (
|
|
831
|
+
if (failureMode === "warn-and-continue") {
|
|
832
|
+
return { block: false, effect: "continued" as const, reason };
|
|
833
|
+
}
|
|
834
|
+
if (failureMode === "block-tool") {
|
|
835
|
+
return { block: true, effect: "tool-blocked" as const, reason };
|
|
836
|
+
}
|
|
837
|
+
session.block(reason);
|
|
838
|
+
herdrAdvisorBlock.set(reason);
|
|
839
|
+
if (advisorBlockOnBlockedRef) {
|
|
840
|
+
ctx.abort();
|
|
841
|
+
}
|
|
842
|
+
return { block: true, effect: "session-blocked" as const, reason };
|
|
843
|
+
};
|
|
844
|
+
|
|
845
|
+
const blockedDecisionEffect = (
|
|
846
|
+
reason: string,
|
|
847
|
+
ctx: ExtensionContext,
|
|
848
|
+
session: AdvisorSessionState,
|
|
849
|
+
failureMode: "block-session" | "block-tool" | "warn-and-continue"
|
|
850
|
+
) => {
|
|
851
|
+
if (failureMode === "warn-and-continue") {
|
|
852
|
+
if (ctx.hasUI) {
|
|
853
|
+
ctx.ui.notify(
|
|
854
|
+
"Advisor gate returned blocked; continuing by configuration.",
|
|
855
|
+
"warning"
|
|
856
|
+
);
|
|
857
|
+
}
|
|
762
858
|
return { block: false, effect: "continued" as const, reason };
|
|
763
859
|
}
|
|
764
|
-
if (
|
|
860
|
+
if (failureMode === "block-tool") {
|
|
765
861
|
return { block: true, effect: "tool-blocked" as const, reason };
|
|
766
862
|
}
|
|
767
863
|
session.block(reason);
|
|
@@ -870,12 +966,14 @@ const handleAutomaticGate = async (
|
|
|
870
966
|
return;
|
|
871
967
|
}
|
|
872
968
|
const reason = `Advisor loop gate: normalized signature for ${event.toolName} repeated ${advisorLoopThresholdRef} times without a materially different tool action.`;
|
|
969
|
+
const failureMode = advisorFailureModeRef;
|
|
873
970
|
if (!session.canConsult(getAdvisorMaxCallsPerSession())) {
|
|
874
971
|
const failure = failureEffect(
|
|
875
972
|
"budget-exhausted",
|
|
876
973
|
"Advisor gate call budget is exhausted.",
|
|
877
974
|
ctx,
|
|
878
|
-
session
|
|
975
|
+
session,
|
|
976
|
+
failureMode
|
|
879
977
|
);
|
|
880
978
|
return failure.block ? { block: true, reason: failure.reason } : undefined;
|
|
881
979
|
}
|
|
@@ -913,7 +1011,7 @@ const handleAutomaticGate = async (
|
|
|
913
1011
|
ensureGateCall();
|
|
914
1012
|
if (!result.ok) {
|
|
915
1013
|
session.recordInvocation({
|
|
916
|
-
executionEffect: gateFailureEffectForMode(
|
|
1014
|
+
executionEffect: gateFailureEffectForMode(failureMode),
|
|
917
1015
|
failure: result.category,
|
|
918
1016
|
kind: "gate",
|
|
919
1017
|
model: advisorRef,
|
|
@@ -925,7 +1023,8 @@ const handleAutomaticGate = async (
|
|
|
925
1023
|
result.category,
|
|
926
1024
|
result.message,
|
|
927
1025
|
ctx,
|
|
928
|
-
session
|
|
1026
|
+
session,
|
|
1027
|
+
failureMode
|
|
929
1028
|
);
|
|
930
1029
|
sendAutomaticGateFailure(
|
|
931
1030
|
pi,
|
|
@@ -939,7 +1038,7 @@ const handleAutomaticGate = async (
|
|
|
939
1038
|
session.recordInvocation({
|
|
940
1039
|
cost: advisorUsageCost(result.usage),
|
|
941
1040
|
decision: result.decision,
|
|
942
|
-
executionEffect: gateDecisionEffect(result.decision),
|
|
1041
|
+
executionEffect: gateDecisionEffect(result.decision, failureMode),
|
|
943
1042
|
kind: "gate",
|
|
944
1043
|
model: result.model,
|
|
945
1044
|
trigger: result.trigger,
|
|
@@ -953,11 +1052,13 @@ const handleAutomaticGate = async (
|
|
|
953
1052
|
}
|
|
954
1053
|
const gateReason = `Advisor loop review: ${result.markdown}`;
|
|
955
1054
|
if (result.decision === "blocked") {
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
1055
|
+
const effect = blockedDecisionEffect(
|
|
1056
|
+
gateReason,
|
|
1057
|
+
ctx,
|
|
1058
|
+
session,
|
|
1059
|
+
failureMode
|
|
1060
|
+
);
|
|
1061
|
+
return effect.block ? { block: true, reason: effect.reason } : undefined;
|
|
961
1062
|
}
|
|
962
1063
|
return { block: true, reason: gateReason };
|
|
963
1064
|
} finally {
|
|
@@ -1074,6 +1175,11 @@ export class ScoutStatusManager {
|
|
|
1074
1175
|
readonly #active = new Set<symbol>();
|
|
1075
1176
|
readonly #known = new Set<symbol>();
|
|
1076
1177
|
readonly #retired = new Set<symbol>();
|
|
1178
|
+
private readonly showStatus: boolean;
|
|
1179
|
+
|
|
1180
|
+
constructor(showStatus = true) {
|
|
1181
|
+
this.showStatus = showStatus;
|
|
1182
|
+
}
|
|
1077
1183
|
|
|
1078
1184
|
register(token: symbol) {
|
|
1079
1185
|
if (!this.#retired.has(token)) {
|
|
@@ -1088,7 +1194,9 @@ export class ScoutStatusManager {
|
|
|
1088
1194
|
this.#known.add(token);
|
|
1089
1195
|
if (event.type === "call" || event.type === "chunk") {
|
|
1090
1196
|
this.#active.add(token);
|
|
1091
|
-
|
|
1197
|
+
if (this.showStatus) {
|
|
1198
|
+
ctx.ui.setStatus("advisor-scout", "Scout curating…");
|
|
1199
|
+
}
|
|
1092
1200
|
return;
|
|
1093
1201
|
}
|
|
1094
1202
|
this.release(ctx, token);
|
|
@@ -1098,7 +1206,7 @@ export class ScoutStatusManager {
|
|
|
1098
1206
|
this.#active.delete(token);
|
|
1099
1207
|
this.#known.delete(token);
|
|
1100
1208
|
this.#retired.add(token);
|
|
1101
|
-
if (!ctx.hasUI) {
|
|
1209
|
+
if (!(ctx.hasUI && this.showStatus)) {
|
|
1102
1210
|
return;
|
|
1103
1211
|
}
|
|
1104
1212
|
ctx.ui.setStatus(
|
|
@@ -1113,7 +1221,7 @@ export class ScoutStatusManager {
|
|
|
1113
1221
|
}
|
|
1114
1222
|
this.#known.clear();
|
|
1115
1223
|
this.#active.clear();
|
|
1116
|
-
if (ctx.hasUI) {
|
|
1224
|
+
if (ctx.hasUI && this.showStatus) {
|
|
1117
1225
|
ctx.ui.setStatus("advisor-scout", undefined);
|
|
1118
1226
|
}
|
|
1119
1227
|
}
|
|
@@ -1154,28 +1262,28 @@ export const renderScoutDetails = (
|
|
|
1154
1262
|
` ${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
1263
|
),
|
|
1156
1264
|
];
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1265
|
+
if (getAdvisorSettings().showUsageDetails) {
|
|
1266
|
+
const usage = formatAdvisorUsage(scout.usage);
|
|
1267
|
+
if (usage) {
|
|
1268
|
+
lines.push(theme.fg("dim", ` Usage: ${usage}`));
|
|
1269
|
+
}
|
|
1160
1270
|
}
|
|
1161
1271
|
if (scout.fallbackReason) {
|
|
1162
1272
|
lines.push(theme.fg("warning", ` ${scout.fallbackReason}`));
|
|
1163
1273
|
}
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
` 💭 ${scout.thinking.replace(/\n/g, " ").slice(-200)}`
|
|
1169
|
-
)
|
|
1170
|
-
);
|
|
1274
|
+
const thinking = scout.thinking && active ? scout.thinking.slice(-200) : "";
|
|
1275
|
+
box.addChild(new Text(lines.join("\n"), 0, 0));
|
|
1276
|
+
if (thinking.trim()) {
|
|
1277
|
+
box.addChild(renderThinkingMarkdown(thinking, theme));
|
|
1171
1278
|
}
|
|
1279
|
+
const expandedLines: string[] = [];
|
|
1172
1280
|
if (expanded && scout.selectedLabels?.length) {
|
|
1173
|
-
|
|
1281
|
+
expandedLines.push(
|
|
1174
1282
|
theme.fg("dim", ` Selected: ${scout.selectedLabels.join("; ")}`)
|
|
1175
1283
|
);
|
|
1176
1284
|
}
|
|
1177
1285
|
if (expanded && scout.synthesis) {
|
|
1178
|
-
|
|
1286
|
+
expandedLines.push(
|
|
1179
1287
|
theme.fg(
|
|
1180
1288
|
"dim",
|
|
1181
1289
|
` Scout synthesis (untrusted inference): ${scout.synthesis}`
|
|
@@ -1183,14 +1291,16 @@ export const renderScoutDetails = (
|
|
|
1183
1291
|
);
|
|
1184
1292
|
}
|
|
1185
1293
|
if (expanded && scout.omittedBeforeScout) {
|
|
1186
|
-
|
|
1294
|
+
expandedLines.push(
|
|
1187
1295
|
theme.fg(
|
|
1188
1296
|
"dim",
|
|
1189
1297
|
` ${scout.omittedBeforeScout} group(s) omitted before Scout`
|
|
1190
1298
|
)
|
|
1191
1299
|
);
|
|
1192
1300
|
}
|
|
1193
|
-
|
|
1301
|
+
if (expandedLines.length > 0) {
|
|
1302
|
+
box.addChild(new Text(expandedLines.join("\n"), 0, 0));
|
|
1303
|
+
}
|
|
1194
1304
|
};
|
|
1195
1305
|
|
|
1196
1306
|
const syncRenderPhase = (context: AdvisorToolContext, phase: string) => {
|
|
@@ -1230,14 +1340,14 @@ const renderPartialAdvisorResult = (
|
|
|
1230
1340
|
const lines = [
|
|
1231
1341
|
`${theme.fg("warning", theme.bold(`◆ ADVISOR ${frame}`))} ${theme.fg("dim", "· Working…")}`,
|
|
1232
1342
|
];
|
|
1233
|
-
|
|
1343
|
+
box.addChild(new Text(lines.join("\n"), 0, 0));
|
|
1344
|
+
if (details?.thinking?.trim()) {
|
|
1234
1345
|
const thought =
|
|
1235
1346
|
details.thinking.length > 200
|
|
1236
1347
|
? details.thinking.slice(-200)
|
|
1237
1348
|
: details.thinking;
|
|
1238
|
-
|
|
1349
|
+
box.addChild(renderThinkingMarkdown(thought, theme));
|
|
1239
1350
|
}
|
|
1240
|
-
box.addChild(new Text(lines.join("\n"), 0, 0));
|
|
1241
1351
|
if (details?.text) {
|
|
1242
1352
|
box.addChild(
|
|
1243
1353
|
new Markdown(
|
|
@@ -1278,9 +1388,11 @@ const renderFinalAdvisorResult = (
|
|
|
1278
1388
|
if (details?.advisor) {
|
|
1279
1389
|
lines.push(theme.fg("dim", ` ${details.advisor}`));
|
|
1280
1390
|
}
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1391
|
+
if (getAdvisorSettings().showUsageDetails) {
|
|
1392
|
+
const usage = formatAdvisorUsage(details?.usage);
|
|
1393
|
+
if (usage) {
|
|
1394
|
+
lines.push(theme.fg("dim", ` Usage: ${usage}`));
|
|
1395
|
+
}
|
|
1284
1396
|
}
|
|
1285
1397
|
const attachments = [
|
|
1286
1398
|
details?.draftBytes
|
|
@@ -1299,17 +1411,14 @@ const renderFinalAdvisorResult = (
|
|
|
1299
1411
|
if (attachments.length) {
|
|
1300
1412
|
lines.push(theme.fg("dim", ` ${attachments.join(" · ")}`));
|
|
1301
1413
|
}
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
theme.fg(
|
|
1306
|
-
"thinkingText",
|
|
1307
|
-
` 💭 ${thought}${details.thinking.length > 300 ? "…" : ""}`
|
|
1308
|
-
)
|
|
1309
|
-
);
|
|
1310
|
-
}
|
|
1414
|
+
const thinking = details?.thinking?.trim()
|
|
1415
|
+
? `${details.thinking.slice(0, 300)}${details.thinking.length > 300 ? "…" : ""}`
|
|
1416
|
+
: "";
|
|
1311
1417
|
const displayAdvice = advice || "(Advisor returned no advice.)";
|
|
1312
1418
|
box.addChild(new Text(lines.join("\n"), 0, 0));
|
|
1419
|
+
if (thinking) {
|
|
1420
|
+
box.addChild(renderThinkingMarkdown(thinking, theme));
|
|
1421
|
+
}
|
|
1313
1422
|
box.addChild(
|
|
1314
1423
|
new Markdown(
|
|
1315
1424
|
adviceForDisplay(displayAdvice, expanded),
|
|
@@ -1344,10 +1453,14 @@ export const registerAdvisorTool = (
|
|
|
1344
1453
|
pi: ExtensionAPI,
|
|
1345
1454
|
session: AdvisorSessionState = advisorSessionState,
|
|
1346
1455
|
dependencies: {
|
|
1456
|
+
appendOutcome?: typeof appendOutcome;
|
|
1457
|
+
consult?: typeof consultAdvisor;
|
|
1347
1458
|
runGate?: typeof runAdvisorGate;
|
|
1348
1459
|
statusManager?: ScoutStatusManager;
|
|
1349
1460
|
} = {}
|
|
1350
1461
|
) => {
|
|
1462
|
+
const appendAdvisorOutcome = dependencies.appendOutcome ?? appendOutcome;
|
|
1463
|
+
const requestAdvisor = dependencies.consult ?? consultAdvisor;
|
|
1351
1464
|
const reservedCalls = new Set<string>();
|
|
1352
1465
|
const scoutStatus = dependencies.statusManager ?? new ScoutStatusManager();
|
|
1353
1466
|
|
|
@@ -1394,9 +1507,11 @@ export const registerAdvisorTool = (
|
|
|
1394
1507
|
if (details?.advisor) {
|
|
1395
1508
|
box.addChild(new Text(theme.fg("dim", ` ${details.advisor}`), 0, 0));
|
|
1396
1509
|
}
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1510
|
+
if (getAdvisorSettings().showUsageDetails) {
|
|
1511
|
+
const usage = formatAdvisorUsage(details?.usage);
|
|
1512
|
+
if (usage) {
|
|
1513
|
+
box.addChild(new Text(theme.fg("dim", ` Usage: ${usage}`), 0, 0));
|
|
1514
|
+
}
|
|
1400
1515
|
}
|
|
1401
1516
|
if (details?.text) {
|
|
1402
1517
|
box.addChild(
|
|
@@ -1505,7 +1620,6 @@ export const registerAdvisorTool = (
|
|
|
1505
1620
|
pi.registerTool({
|
|
1506
1621
|
description:
|
|
1507
1622
|
"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
1623
|
async execute(_id, params, signal, onUpdate, ctx) {
|
|
1510
1624
|
reservedCalls.delete(_id);
|
|
1511
1625
|
if (
|
|
@@ -1524,13 +1638,24 @@ export const registerAdvisorTool = (
|
|
|
1524
1638
|
}
|
|
1525
1639
|
herdrAdvisorActivity.start();
|
|
1526
1640
|
let scoutDetails: ScoutToolDetails | undefined;
|
|
1641
|
+
const coalescedUpdate = createCoalescedUpdate(
|
|
1642
|
+
(update: Parameters<NonNullable<typeof onUpdate>>[0]) =>
|
|
1643
|
+
onUpdate?.(update),
|
|
1644
|
+
ADVISOR_STREAM_UPDATE_INTERVAL_MS
|
|
1645
|
+
);
|
|
1646
|
+
const flushUpdate = () => {
|
|
1647
|
+
const result = coalescedUpdate.flush();
|
|
1648
|
+
if (result.failed) {
|
|
1649
|
+
throw result.error;
|
|
1650
|
+
}
|
|
1651
|
+
};
|
|
1527
1652
|
try {
|
|
1528
|
-
const result = await
|
|
1653
|
+
const result = await requestAdvisor(
|
|
1529
1654
|
ctx,
|
|
1530
1655
|
resolveAdvisorRequest(params.question),
|
|
1531
1656
|
signal,
|
|
1532
1657
|
(t, tx) =>
|
|
1533
|
-
|
|
1658
|
+
coalescedUpdate.update({
|
|
1534
1659
|
content: [{ text: tx, type: "text" }],
|
|
1535
1660
|
details: {
|
|
1536
1661
|
advisor: advisorRef,
|
|
@@ -1548,7 +1673,7 @@ export const registerAdvisorTool = (
|
|
|
1548
1673
|
params.includeTrackedFiles,
|
|
1549
1674
|
(event) => {
|
|
1550
1675
|
scoutDetails = scoutDetailsFromEvent(event, scoutDetails);
|
|
1551
|
-
|
|
1676
|
+
coalescedUpdate.update({
|
|
1552
1677
|
content: [{ text: scoutDetails.text ?? "", type: "text" }],
|
|
1553
1678
|
details: {
|
|
1554
1679
|
advisor: advisorRef,
|
|
@@ -1559,6 +1684,7 @@ export const registerAdvisorTool = (
|
|
|
1559
1684
|
},
|
|
1560
1685
|
_id
|
|
1561
1686
|
);
|
|
1687
|
+
flushUpdate();
|
|
1562
1688
|
session.issueAdvice(
|
|
1563
1689
|
result.adviceId,
|
|
1564
1690
|
result.markdown,
|
|
@@ -1575,9 +1701,7 @@ export const registerAdvisorTool = (
|
|
|
1575
1701
|
});
|
|
1576
1702
|
const usage = snapshotAdvisorUsage(result.usage);
|
|
1577
1703
|
const piUsage = advisorUsageForPi(result.usage);
|
|
1578
|
-
|
|
1579
|
-
ctx.ui.setStatus("advisor-usage", session.usageStatus());
|
|
1580
|
-
}
|
|
1704
|
+
updateAdvisorUsageStatus(ctx, session);
|
|
1581
1705
|
return {
|
|
1582
1706
|
content: [
|
|
1583
1707
|
{
|
|
@@ -1601,6 +1725,10 @@ export const registerAdvisorTool = (
|
|
|
1601
1725
|
...(piUsage ? { usage: piUsage } : {}),
|
|
1602
1726
|
};
|
|
1603
1727
|
} catch (error) {
|
|
1728
|
+
// Publish the latest partial state before surfacing a provider or
|
|
1729
|
+
// execution error. A failure from the UI sink must not replace the
|
|
1730
|
+
// original error because this path is also used for provider failures.
|
|
1731
|
+
coalescedUpdate.flush();
|
|
1604
1732
|
const message = error instanceof Error ? error.message : String(error);
|
|
1605
1733
|
session.recordInvocation({
|
|
1606
1734
|
executionEffect: "continued",
|
|
@@ -1609,13 +1737,12 @@ export const registerAdvisorTool = (
|
|
|
1609
1737
|
model: advisorRef,
|
|
1610
1738
|
trigger: "executor-requested",
|
|
1611
1739
|
});
|
|
1612
|
-
|
|
1613
|
-
ctx.ui.setStatus("advisor-usage", session.usageStatus());
|
|
1614
|
-
}
|
|
1740
|
+
updateAdvisorUsageStatus(ctx, session);
|
|
1615
1741
|
notifyLocalFailure(ctx, message);
|
|
1616
1742
|
notifyHerdrAdvisorFailure("Advisor consultation failed", message);
|
|
1617
1743
|
throw error;
|
|
1618
1744
|
} finally {
|
|
1745
|
+
coalescedUpdate.cancel();
|
|
1619
1746
|
herdrAdvisorActivity.finish();
|
|
1620
1747
|
}
|
|
1621
1748
|
},
|
|
@@ -1692,18 +1819,19 @@ export const registerAdvisorTool = (
|
|
|
1692
1819
|
details: { recorded: false },
|
|
1693
1820
|
};
|
|
1694
1821
|
}
|
|
1695
|
-
const advice = session.
|
|
1822
|
+
const advice = session.reserveAdvice(params.adviceId);
|
|
1696
1823
|
if (!advice) {
|
|
1697
|
-
throw new Error("Unknown
|
|
1824
|
+
throw new Error("Unknown, already recorded, or pending adviceId.");
|
|
1698
1825
|
}
|
|
1699
1826
|
try {
|
|
1700
|
-
await
|
|
1827
|
+
await appendAdvisorOutcome({
|
|
1701
1828
|
adoption: params.adoption as (typeof ADOPTIONS)[number],
|
|
1702
1829
|
advice: advice.advice,
|
|
1703
1830
|
trigger: advice.trigger,
|
|
1704
1831
|
validationStatus:
|
|
1705
1832
|
params.validationStatus as (typeof VALIDATIONS)[number],
|
|
1706
1833
|
});
|
|
1834
|
+
session.commitAdvice(params.adviceId);
|
|
1707
1835
|
return {
|
|
1708
1836
|
content: [
|
|
1709
1837
|
{ text: "Advisor outcome recorded locally.", type: "text" },
|
|
@@ -1711,6 +1839,7 @@ export const registerAdvisorTool = (
|
|
|
1711
1839
|
details: { recorded: true },
|
|
1712
1840
|
};
|
|
1713
1841
|
} catch {
|
|
1842
|
+
session.releaseAdvice(params.adviceId);
|
|
1714
1843
|
if (ctx.hasUI) {
|
|
1715
1844
|
ctx.ui.notify(
|
|
1716
1845
|
"Advisor outcome could not be recorded locally.",
|