pi-advisor-flow 0.9.2-dev.5 → 0.10.1-dev.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/dist/index.js CHANGED
@@ -43,6 +43,7 @@ var GATE_FAILURE_MODES = [
43
43
  // src/config/state.ts
44
44
  var executorRef = "";
45
45
  var advisorRef = "";
46
+ var advisorFallbackModelRef;
46
47
  var advisorAgentsMdContextRef = true;
47
48
  var persistedExecutorRef;
48
49
  var persistedAdvisorRef;
@@ -95,6 +96,9 @@ var setExecutorRef = (ref) => {
95
96
  var setAdvisorRef = (ref) => {
96
97
  advisorRef = ref;
97
98
  };
99
+ var setAdvisorFallbackModelRef = (ref) => {
100
+ advisorFallbackModelRef = ref?.trim() || undefined;
101
+ };
98
102
  var setAdvisorAgentsMdContextRef = (enabled) => {
99
103
  advisorAgentsMdContextRef = enabled;
100
104
  };
@@ -251,6 +255,7 @@ var getAdvisorSettings = () => ({
251
255
  effort: advisorEffortRef,
252
256
  failureGate: advisorFailureGateRef,
253
257
  failureMode: advisorFailureModeRef,
258
+ fallbackModel: advisorFallbackModelRef,
254
259
  gitContext: advisorGitContextRef,
255
260
  gitContextMaxChars: advisorGitContextMaxCharsRef,
256
261
  herdrIntegration: advisorHerdrIntegrationRef,
@@ -521,6 +526,12 @@ var CONFIG_SCHEMA = {
521
526
  persisted: true,
522
527
  type: "boolean"
523
528
  },
529
+ advisorFallbackModel: {
530
+ accepted: "a provider/model string",
531
+ current: () => configuredModelRef(advisorFallbackModelRef),
532
+ persisted: true,
533
+ type: "string"
534
+ },
524
535
  advisorGitContext: {
525
536
  accepted: GIT_CONTEXT_LEVELS.join(", "),
526
537
  current: () => advisorGitContextRef,
@@ -868,6 +879,7 @@ import { CONFIG_DIR_NAME, getAgentDir } from "@earendil-works/pi-coding-agent";
868
879
  var resetDefaults = () => {
869
880
  setExecutorRef("");
870
881
  setAdvisorRef("");
882
+ setAdvisorFallbackModelRef(undefined);
871
883
  setAdvisorAgentsMdContextRef(true);
872
884
  setPersistedModelRefs(undefined, undefined);
873
885
  setExecutorEffortRef(undefined);
@@ -928,6 +940,7 @@ var applyNonEmptyStringConfig = (value, apply) => {
928
940
  var applyConfig = (config) => {
929
941
  applyNonEmptyStringConfig(config.executor, setExecutorRef);
930
942
  applyNonEmptyStringConfig(config.advisor, setAdvisorRef);
943
+ applyNonEmptyStringConfig(config.advisorFallbackModel, setAdvisorFallbackModelRef);
931
944
  applyNonEmptyStringConfig(config.executorEffort, setExecutorEffortRef);
932
945
  applyNonEmptyStringConfig(config.advisorEffort, setAdvisorEffortRef);
933
946
  applyOptionalConfig(config, "advisorAgentsMdContext", setAdvisorAgentsMdContextRef);
@@ -1136,14 +1149,16 @@ var advisorModelAccess = (ctx) => {
1136
1149
  }
1137
1150
  return denial;
1138
1151
  };
1139
- var sameModelAdvisorDisabled = (ctx, model = ctx.model) => {
1140
- if (!(advisorDisableSameModelRef && advisorRef && model)) {
1152
+ var sameModelAdvisorDisabled = (ctx, model = ctx.model, advisorModelRef = advisorRef) => {
1153
+ const selectedAdvisorRef = advisorModelRef ?? advisorRef;
1154
+ if (!(advisorDisableSameModelRef && selectedAdvisorRef && model)) {
1141
1155
  return false;
1142
1156
  }
1143
- const [provider, id] = splitRef(advisorRef);
1157
+ const [provider, id] = splitRef(selectedAdvisorRef);
1144
1158
  return provider === model.provider && id === model.id;
1145
1159
  };
1146
1160
  var sameModelAdvisorNotice = "Advisor disabled: executor and advisor are the same model.";
1161
+ var fallbackSameModelAdvisorNotice = "Advisor fallback skipped: executor and fallback Advisor are the same model.";
1147
1162
  var advisorModelIsAllowed = (ctx) => advisorModelAccess(ctx).allowed && !sameModelAdvisorDisabled(ctx);
1148
1163
  var advisorModelAccessReason = (ctx) => {
1149
1164
  const access = advisorModelAccess(ctx);
@@ -1465,6 +1480,9 @@ class SearchableModelSelector extends ModelSelectorAdapter {
1465
1480
  }
1466
1481
  }
1467
1482
 
1483
+ // src/ui/types.ts
1484
+ var FALLBACK_ADVISOR_MODEL_DISABLED = "Disabled (no fallback)";
1485
+
1468
1486
  // src/commands/model-picker.ts
1469
1487
  var selectAdvisorModels = async (ctx, options) => {
1470
1488
  if (!ctx.hasUI) {
@@ -1478,7 +1496,13 @@ var selectAdvisorModels = async (ctx, options) => {
1478
1496
  const allOptions = [
1479
1497
  ...new Set(refs ?? [options.executor, options.advisor].filter((ref) => Boolean(ref)))
1480
1498
  ];
1481
- let { advisor, advisorEffort, executor, executorEffort } = options;
1499
+ let {
1500
+ advisor,
1501
+ advisorEffort,
1502
+ advisorFallbackModel,
1503
+ executor,
1504
+ executorEffort
1505
+ } = options;
1482
1506
  if (options.selectExecutor) {
1483
1507
  const selectedExecutor = await ctx.ui.custom((tui, theme, keybindings, done) => new SearchableModelSelector({
1484
1508
  allOptions,
@@ -1521,10 +1545,36 @@ var selectAdvisorModels = async (ctx, options) => {
1521
1545
  advisor = selectedAdvisor;
1522
1546
  advisorEffort = selectedEffort(selectedAdvisorEffort);
1523
1547
  }
1548
+ const selectedFallback = await ctx.ui.custom((tui, theme, keybindings, done) => new SearchableModelSelector({
1549
+ allOptions: [
1550
+ FALLBACK_ADVISOR_MODEL_DISABLED,
1551
+ ...new Set([
1552
+ ...allOptions,
1553
+ ...refs || !advisorFallbackModel ? [] : [advisorFallbackModel]
1554
+ ])
1555
+ ],
1556
+ currentOption: advisorFallbackModel ?? FALLBACK_ADVISOR_MODEL_DISABLED,
1557
+ keybindings,
1558
+ onCancel: () => done(undefined),
1559
+ onSelect: done,
1560
+ theme,
1561
+ title: "Select Fallback Advisor Model (optional)",
1562
+ tui
1563
+ }));
1564
+ if (selectedFallback === undefined) {
1565
+ return;
1566
+ }
1567
+ advisorFallbackModel = selectedFallback === FALLBACK_ADVISOR_MODEL_DISABLED ? undefined : selectedFallback;
1524
1568
  if (!(advisor && executor)) {
1525
1569
  return;
1526
1570
  }
1527
- return { advisor, advisorEffort, executor, executorEffort };
1571
+ return {
1572
+ advisor,
1573
+ advisorEffort,
1574
+ advisorFallbackModel,
1575
+ executor,
1576
+ executorEffort
1577
+ };
1528
1578
  };
1529
1579
 
1530
1580
  // src/herdr-shared.ts
@@ -3891,17 +3941,56 @@ var advisorImageNotice = (context) => {
3891
3941
 
3892
3942
  Image disclosure: ${disclosure}. Only the images explicitly attached below have pixels available. Other image markers, image paths, and text descriptions are not visual evidence.`;
3893
3943
  };
3894
- var collectAdvisorResponse = async (options) => {
3895
- const { ctx, question, signal, systemPrompt } = options;
3896
- loadConfig(ctx);
3897
- if (sameModelAdvisorDisabled(ctx)) {
3898
- throw new Error(sameModelAdvisorNotice);
3944
+ var advisorPrivacyKey = (ctx) => {
3945
+ const settings = getAdvisorSettings();
3946
+ return JSON.stringify({
3947
+ agentsMdContext: settings.agentsMdContext,
3948
+ contextMaxChars: settings.contextMaxChars,
3949
+ cwd: ctx.cwd,
3950
+ gitContext: settings.gitContext,
3951
+ gitContextMaxChars: settings.gitContextMaxChars,
3952
+ projectTrusted: ctx.isProjectTrusted(),
3953
+ redactSecrets: settings.redactSecrets,
3954
+ scoutEnabled: settings.scoutEnabled,
3955
+ toolPolicies: settings.toolPolicies,
3956
+ toolResultMaxBytes: settings.toolResultMaxBytes,
3957
+ toolResultMaxLines: settings.toolResultMaxLines,
3958
+ trackedFileContent: settings.trackedFileContent,
3959
+ untrackedContent: settings.untrackedContent
3960
+ });
3961
+ };
3962
+ var assertAdvisorFollowUpPrivacy = (payload, ctx) => {
3963
+ if (payload.privacyKey !== advisorPrivacyKey(ctx)) {
3964
+ throw new Error("Follow-up consultation unavailable: privacy or disclosure settings changed after the original consultation. Issue a fresh consultation.");
3899
3965
  }
3900
- const accessReason = advisorModelAccessReason(ctx);
3901
- if (accessReason) {
3902
- throw new Error(accessReason);
3966
+ };
3967
+ var prepareFollowUp = (followUp, question, ctx) => {
3968
+ if (!question?.trim()) {
3969
+ throw new Error("Follow-up consultations require a non-empty question. Issue a fresh consultation if no question is needed.");
3903
3970
  }
3904
- const resolved = await resolveConfiguredModel(ctx, advisorRef, "Advisor");
3971
+ assertAdvisorFollowUpPrivacy(followUp.payload, ctx);
3972
+ const outboundQuestion = advisorRedactSecretsRef || followUp.payload.redacted ? redactSecrets(question) : question;
3973
+ const messages = structuredClone(followUp.payload.messages);
3974
+ messages.push({
3975
+ content: [
3976
+ {
3977
+ text: `Follow-up focus:
3978
+ ${outboundQuestion}`,
3979
+ type: "text"
3980
+ }
3981
+ ],
3982
+ role: "user",
3983
+ timestamp: Date.now()
3984
+ });
3985
+ return {
3986
+ messages,
3987
+ privacyKey: followUp.payload.privacyKey,
3988
+ redacted: advisorRedactSecretsRef || followUp.payload.redacted,
3989
+ systemPrompt: followUp.payload.systemPrompt
3990
+ };
3991
+ };
3992
+ var prepareFreshRequest = async (options, resolved) => {
3993
+ const { question } = options;
3905
3994
  const context = await assembleConsultationContext({
3906
3995
  ...options,
3907
3996
  supportsImages: resolved.model.input.includes("image")
@@ -3929,42 +4018,136 @@ ${escapeRepositoryText(label)}: attached image pixels (untrusted data).`,
3929
4018
  timestamp: Date.now()
3930
4019
  }
3931
4020
  ];
3932
- const streamed = await collectTextStream(resolved, {
4021
+ return {
4022
+ context,
3933
4023
  messages,
4024
+ privacyKey: advisorPrivacyKey(options.ctx),
4025
+ redacted: advisorRedactSecretsRef,
4026
+ systemPrompt: options.systemPrompt
4027
+ };
4028
+ };
4029
+ var combinedFallbackError = (primaryModel, primaryError, fallbackModel, fallbackError) => new Error(`Advisor model ${primaryModel} failed: ${primaryError.message}. Fallback Advisor model ${fallbackModel} failed: ${fallbackError.message}.`, { cause: fallbackError });
4030
+ var advisorContextDetails = (context) => ({
4031
+ agentRulesBytes: context?.projectRules?.bytes || undefined,
4032
+ draftBytes: context?.draftText ? Buffer.byteLength(context.draftText, "utf-8") : undefined,
4033
+ imageBytes: context?.images.reduce((sum, item) => sum + Buffer.from(item.image.data, "base64").length, 0) || undefined,
4034
+ imageCount: context?.images.length,
4035
+ imageOmissions: context?.imageOmissions || undefined,
4036
+ imagePartsSeen: context?.imagePartsSeen || undefined,
4037
+ preferenceBytes: context?.preferences?.bytes,
4038
+ scout: context?.scout,
4039
+ trackedBytes: context?.tracked.reduce((sum, item) => sum + item.bytes, 0) || undefined,
4040
+ untrackedBytes: context?.untracked.reduce((sum, item) => sum + item.bytes, 0) || undefined
4041
+ });
4042
+ var runAdvisorAttempt = async (options, prepared, resolved, modelRef) => {
4043
+ const streamed = await collectTextStream(resolved, {
4044
+ messages: structuredClone(prepared.messages),
3934
4045
  onChunk: options.onChunk,
3935
4046
  reasoning: advisorEffortRef,
3936
- signal,
3937
- systemPrompt
4047
+ signal: options.signal,
4048
+ systemPrompt: prepared.systemPrompt
3938
4049
  });
3939
- const markdown = streamed.text;
4050
+ const { text: markdown } = streamed;
3940
4051
  if (!markdown.trim()) {
3941
4052
  throw new AdvisorNoAdviceError;
3942
4053
  }
3943
- const response = {
3944
- agentRulesBytes: context.projectRules?.bytes || undefined,
3945
- draftBytes: context.draftText ? Buffer.byteLength(context.draftText, "utf-8") : undefined,
3946
- imageBytes: context.images.reduce((sum, item) => sum + Buffer.from(item.image.data, "base64").length, 0) || undefined,
3947
- imageCount: context.images.length,
3948
- imageOmissions: context.imageOmissions || undefined,
3949
- imagePartsSeen: context.imagePartsSeen || undefined,
4054
+ return {
4055
+ ...advisorContextDetails(prepared.context),
4056
+ followUp: Boolean(options.followUp),
4057
+ followUpPayload: {
4058
+ depth: options.followUp ? options.followUp.payload.depth + 1 : 0,
4059
+ messages: structuredClone(prepared.messages),
4060
+ model: modelRef,
4061
+ privacyKey: prepared.privacyKey,
4062
+ redacted: prepared.redacted,
4063
+ systemPrompt: prepared.systemPrompt
4064
+ },
3950
4065
  markdown,
3951
- model: advisorRef,
3952
- preferenceBytes: context.preferences?.bytes,
4066
+ model: modelRef,
3953
4067
  thinkingText: streamed.thinking,
3954
- trackedBytes: context.tracked.reduce((sum, item) => sum + item.bytes, 0) || undefined,
3955
- untrackedBytes: context.untracked.reduce((sum, item) => sum + item.bytes, 0) || undefined,
3956
4068
  usage: streamed.usage
3957
4069
  };
3958
- if (context.scout) {
3959
- response.scout = context.scout;
4070
+ };
4071
+ var runFallbackAdvisorAttempt = async (options, primaryModel, primaryError, prepared) => {
4072
+ const { ctx } = options;
4073
+ let request = prepared;
4074
+ if (!advisorFallbackModelRef) {
4075
+ throw primaryError;
4076
+ }
4077
+ const fallbackModel = advisorFallbackModelRef;
4078
+ if (fallbackModel === primaryModel) {
4079
+ throw combinedFallbackError(primaryModel, primaryError, fallbackModel, new Error("fallback is the same model as the primary Advisor"));
4080
+ }
4081
+ if (sameModelAdvisorDisabled(ctx, ctx.model, fallbackModel)) {
4082
+ throw combinedFallbackError(primaryModel, primaryError, fallbackModel, new Error(fallbackSameModelAdvisorNotice));
4083
+ }
4084
+ const fallbackAccessReason = advisorModelAccessReason(ctx);
4085
+ if (fallbackAccessReason) {
4086
+ throw combinedFallbackError(primaryModel, primaryError, fallbackModel, new Error(fallbackAccessReason));
4087
+ }
4088
+ let resolvedFallback;
4089
+ try {
4090
+ resolvedFallback = await resolveConfiguredModel(ctx, fallbackModel, "Advisor fallback");
4091
+ } catch (error) {
4092
+ throw combinedFallbackError(primaryModel, primaryError, fallbackModel, error instanceof Error ? error : new Error(String(error)));
4093
+ }
4094
+ if (!request) {
4095
+ try {
4096
+ request = options.followUp ? prepareFollowUp(options.followUp, options.question, options.ctx) : await prepareFreshRequest(options, resolvedFallback);
4097
+ } catch (error) {
4098
+ throw combinedFallbackError(primaryModel, primaryError, fallbackModel, error instanceof Error ? error : new Error(String(error)));
4099
+ }
4100
+ }
4101
+ try {
4102
+ return await runAdvisorAttempt(options, request, resolvedFallback, fallbackModel);
4103
+ } catch (error) {
4104
+ if (options.signal?.aborted) {
4105
+ throw error;
4106
+ }
4107
+ throw combinedFallbackError(primaryModel, primaryError, fallbackModel, error instanceof Error ? error : new Error(String(error)));
3960
4108
  }
3961
- return response;
3962
4109
  };
3963
- var consultAdvisor = async (ctx, question, signal, onChunk, trigger = "executor-requested", gitContext, draft, includeUntracked, includeTracked, onScout, currentInvocationId) => {
4110
+ var collectAdvisorResponse = async (options) => {
4111
+ const { ctx, signal } = options;
4112
+ loadConfig(ctx);
4113
+ const primaryModel = options.followUp?.payload.model ?? advisorRef;
4114
+ if (sameModelAdvisorDisabled(ctx, ctx.model, primaryModel)) {
4115
+ throw new Error(primaryModel === advisorRef ? sameModelAdvisorNotice : fallbackSameModelAdvisorNotice);
4116
+ }
4117
+ const accessReason = advisorModelAccessReason(ctx);
4118
+ if (accessReason) {
4119
+ throw new Error(accessReason);
4120
+ }
4121
+ let primaryError;
4122
+ let prepared;
4123
+ let resolvedPrimary;
4124
+ try {
4125
+ resolvedPrimary = await resolveConfiguredModel(ctx, primaryModel, "Advisor");
4126
+ } catch (error) {
4127
+ primaryError = error instanceof Error ? error : new Error(String(error));
4128
+ }
4129
+ if (resolvedPrimary) {
4130
+ prepared = options.followUp ? prepareFollowUp(options.followUp, options.question, options.ctx) : await prepareFreshRequest(options, resolvedPrimary);
4131
+ try {
4132
+ return await runAdvisorAttempt(options, prepared, resolvedPrimary, primaryModel);
4133
+ } catch (error) {
4134
+ primaryError = error instanceof Error ? error : new Error(String(error));
4135
+ }
4136
+ }
4137
+ if (!primaryError) {
4138
+ throw new Error("Advisor request failed before an error was recorded.");
4139
+ }
4140
+ if (signal?.aborted) {
4141
+ throw primaryError;
4142
+ }
4143
+ return runFallbackAdvisorAttempt(options, primaryModel, primaryError, prepared);
4144
+ };
4145
+ var consultAdvisor = async (ctx, question, signal, onChunk, trigger = "executor-requested", gitContext, draft, includeUntracked, includeTracked, onScout, currentInvocationId, followUp, onFollowUpPayload) => {
3964
4146
  const result = await collectAdvisorResponse({
3965
4147
  ctx,
3966
4148
  currentInvocationId,
3967
4149
  draft,
4150
+ followUp,
3968
4151
  gitContext,
3969
4152
  includeTracked,
3970
4153
  includeUntracked,
@@ -3974,7 +4157,10 @@ var consultAdvisor = async (ctx, question, signal, onChunk, trigger = "executor-
3974
4157
  signal,
3975
4158
  systemPrompt: ADVISOR_SYSTEM
3976
4159
  });
3977
- return { ...result, adviceId: randomUUID2(), trigger };
4160
+ const adviceId = randomUUID2();
4161
+ const { followUpPayload, ...publicResult } = result;
4162
+ onFollowUpPayload?.(adviceId, followUpPayload, followUp?.adviceId);
4163
+ return { ...publicResult, adviceId, trigger };
3978
4164
  };
3979
4165
  var runAdvisorGate = async (ctx, question, trigger = "repeated-tool-call", signal, onChunk, onScout, currentInvocationId) => {
3980
4166
  try {
@@ -4135,10 +4321,10 @@ var hiddenThinkingLabel = (theme) => {
4135
4321
  };
4136
4322
  var renderThinkingMarkdown = (thinking, theme) => piHideThinkingEnabled() ? hiddenThinkingLabel(theme) : new ThinkingMarkdown(thinking, theme);
4137
4323
  var resolveAdvisorRequest = (question) => question?.trim() || undefined;
4138
- var renderAdvisorCallBox = (question, theme) => {
4324
+ var renderAdvisorCallBox = (question, theme, followUp = false) => {
4139
4325
  const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
4140
4326
  const label = theme.fg("customMessageLabel", theme.bold("[advisor]"));
4141
- const title = theme.fg("customMessageText", "Executor → Advisor");
4327
+ const title = theme.fg("customMessageText", followUp ? "Executor → Advisor · Follow-up" : "Executor → Advisor");
4142
4328
  box.addChild(new Text(question ? `${label} ${title}
4143
4329
  ${theme.fg("dim", ` ${question}`)}` : `${label} ${title}`, 0, 0));
4144
4330
  return box;
@@ -4324,6 +4510,59 @@ var renderScoutDetails = (box, scout, expanded, theme) => {
4324
4510
  }
4325
4511
  };
4326
4512
 
4513
+ // src/follow-up.ts
4514
+ var FOLLOW_UP_TTL_MS = 5 * 60 * 1000;
4515
+ var FOLLOW_UP_MAX_DEPTH = 3;
4516
+ var FOLLOW_UP_MAX_TOOL_CALLS = 3;
4517
+ var copy = (value) => structuredClone(value);
4518
+
4519
+ class AdvisorFollowUpCache {
4520
+ #entries = new Map;
4521
+ #now;
4522
+ constructor(now = Date.now) {
4523
+ this.#now = now;
4524
+ }
4525
+ clear() {
4526
+ this.#entries.clear();
4527
+ }
4528
+ advanceToolCall() {
4529
+ const now = this.#now();
4530
+ for (const [adviceId, entry] of this.#entries) {
4531
+ if (now >= entry.expiresAt || entry.toolCallsSince + 1 >= FOLLOW_UP_MAX_TOOL_CALLS) {
4532
+ this.#entries.delete(adviceId);
4533
+ continue;
4534
+ }
4535
+ entry.toolCallsSince += 1;
4536
+ }
4537
+ }
4538
+ capture(adviceId, payload, replacesAdviceId) {
4539
+ if (replacesAdviceId) {
4540
+ this.#entries.delete(replacesAdviceId);
4541
+ }
4542
+ this.#entries.set(adviceId, {
4543
+ ...copy(payload),
4544
+ expiresAt: this.#now() + FOLLOW_UP_TTL_MS,
4545
+ toolCallsSince: 0
4546
+ });
4547
+ }
4548
+ get(adviceId) {
4549
+ const entry = this.#entries.get(adviceId);
4550
+ if (!entry) {
4551
+ return { reason: "the adviceId is unknown or no longer cached" };
4552
+ }
4553
+ if (this.#now() >= entry.expiresAt) {
4554
+ this.#entries.delete(adviceId);
4555
+ return { reason: "the cached payload has expired" };
4556
+ }
4557
+ if (entry.depth >= FOLLOW_UP_MAX_DEPTH) {
4558
+ return {
4559
+ reason: `the follow-up chain is limited to ${FOLLOW_UP_MAX_DEPTH} follow-ups`
4560
+ };
4561
+ }
4562
+ return { payload: copy(entry) };
4563
+ }
4564
+ }
4565
+
4327
4566
  // src/jev/ledger.ts
4328
4567
  var freshJevUsage = () => ({
4329
4568
  cost: 0,
@@ -4545,8 +4784,12 @@ class AdvisorSessionState {
4545
4784
  #consumedCalls = 0;
4546
4785
  #callReservations = new Set;
4547
4786
  #jev = new AdvisorJevLedgerState;
4787
+ #followUps;
4548
4788
  #sessionTurnOrdinal = 0;
4549
4789
  #turnsSinceConsultation = 0;
4790
+ constructor(now) {
4791
+ this.#followUps = new AdvisorFollowUpCache(now);
4792
+ }
4550
4793
  resetTask() {
4551
4794
  this.#repetition = freshRepetition();
4552
4795
  this.#blockedReason = undefined;
@@ -4555,6 +4798,7 @@ class AdvisorSessionState {
4555
4798
  this.#consumedCalls = 0;
4556
4799
  this.#callReservations.clear();
4557
4800
  this.#jev.reset();
4801
+ this.#followUps.clear();
4558
4802
  this.#sessionTurnOrdinal = 0;
4559
4803
  this.#turnsSinceConsultation = 0;
4560
4804
  }
@@ -4619,6 +4863,18 @@ class AdvisorSessionState {
4619
4863
  get consumedCalls() {
4620
4864
  return this.#consumedCalls;
4621
4865
  }
4866
+ advanceFollowUpToolCall() {
4867
+ this.#followUps.advanceToolCall();
4868
+ }
4869
+ clearFollowUps() {
4870
+ this.#followUps.clear();
4871
+ }
4872
+ followUpFor(adviceId) {
4873
+ return this.#followUps.get(adviceId);
4874
+ }
4875
+ captureFollowUp(adviceId, payload, replacesAdviceId) {
4876
+ this.#followUps.capture(adviceId, payload, replacesAdviceId);
4877
+ }
4622
4878
  recordCompletedTurn() {
4623
4879
  this.#sessionTurnOrdinal += 1;
4624
4880
  this.#turnsSinceConsultation += 1;
@@ -4777,6 +5033,7 @@ class AdvisorSessionState {
4777
5033
  `Budget: ${budget}`,
4778
5034
  `Usage: ${formatAdvisorUsageTotals(totals)}`,
4779
5035
  `Markdown advice: ${markdown.length} responses (${this.#ledger.draftConsultations} with drafts)`,
5036
+ `Follow-ups: ${markdown.filter((item) => item.followUp).length}`,
4780
5037
  `Outcome reports: ${this.#ledger.outcomes}`,
4781
5038
  `Gate decisions: ${this.#decisionsLine()}`,
4782
5039
  `Loop matching: normalized tool signatures; ${this.#repetition.interventions} gate intervention${this.#repetition.interventions === 1 ? "" : "s"}`,
@@ -4834,7 +5091,7 @@ class CommandRuntime {
4834
5091
  this.advisorSessionState = dependencies.sessionState ?? sessionStateFor(pi);
4835
5092
  this.herdrActivity = dependencies.herdrActivity ?? herdrAdvisorActivity;
4836
5093
  this.scoutStatus = dependencies.statusManager ?? new ScoutStatusManager(false);
4837
- this.requestAdvisor = dependencies.consult ?? ((ctx, question, signal, onChunk, onScout, gitContext) => consultAdvisor(ctx, question, signal, onChunk, "manual", gitContext, undefined, undefined, undefined, onScout));
5094
+ this.requestAdvisor = dependencies.consult ?? ((ctx, question, signal, onChunk, onScout, gitContext) => consultAdvisor(ctx, question, signal, onChunk, "manual", gitContext, undefined, undefined, undefined, onScout, undefined, undefined, (adviceId, payload, replacesAdviceId) => this.advisorSessionState.captureFollowUp(adviceId, payload, replacesAdviceId)));
4838
5095
  }
4839
5096
  flowEnabled() {
4840
5097
  return this.pi.getActiveTools().includes("ask_advisor");
@@ -4904,6 +5161,7 @@ var prepareActivationModels = async (runtime, ctx, announce, executorOverride, a
4904
5161
  const selection = await selectAdvisorModels(ctx, {
4905
5162
  advisor: advisorOverride || persisted.advisor ? advisorRef : "",
4906
5163
  advisorEffort: advisorEffortRef,
5164
+ advisorFallbackModel: advisorFallbackModelRef,
4907
5165
  executor: executorOverride || plan.pendingExecutor || persisted.executor ? executorRef : "",
4908
5166
  executorEffort: executorEffortRef,
4909
5167
  selectAdvisor: plan.selectAdvisor,
@@ -4913,6 +5171,7 @@ var prepareActivationModels = async (runtime, ctx, announce, executorOverride, a
4913
5171
  return;
4914
5172
  }
4915
5173
  setAdvisorRef(selection.advisor);
5174
+ setAdvisorFallbackModelRef(selection.advisorFallbackModel);
4916
5175
  setAdvisorEffortRef(selection.advisorEffort);
4917
5176
  setExecutorRef(selection.executor);
4918
5177
  setExecutorEffortRef(selection.executorEffort);
@@ -6030,7 +6289,15 @@ var startManualConsultation = async (runtime, ctx, question, controller, scoutSt
6030
6289
  const finishHerdrActivity = runtime.herdrActivity.start();
6031
6290
  let scoutDetails;
6032
6291
  try {
6033
- const { adviceId, agentRulesBytes, markdown, preferenceBytes, usage } = await runtime.requestAdvisor(ctx, question, controller.signal, (thinking, text) => {
6292
+ const {
6293
+ adviceId,
6294
+ agentRulesBytes,
6295
+ followUp,
6296
+ markdown,
6297
+ model,
6298
+ preferenceBytes,
6299
+ usage
6300
+ } = await runtime.requestAdvisor(ctx, question, controller.signal, (thinking, text) => {
6034
6301
  if (controller.signal.aborted) {
6035
6302
  return;
6036
6303
  }
@@ -6055,8 +6322,9 @@ var startManualConsultation = async (runtime, ctx, question, controller, scoutSt
6055
6322
  runtime.advisorSessionState.recordInvocation({
6056
6323
  cost: advisorUsageCost(usage),
6057
6324
  executionEffect: "continued",
6325
+ followUp,
6058
6326
  kind: "markdown",
6059
- model: advisorRef,
6327
+ model: model ?? advisorRef,
6060
6328
  trigger: "manual",
6061
6329
  usage
6062
6330
  });
@@ -6065,8 +6333,10 @@ var startManualConsultation = async (runtime, ctx, question, controller, scoutSt
6065
6333
  }
6066
6334
  runtime.updateAdvisorUsageStatus(ctx);
6067
6335
  const details = {
6068
- advisor: advisorRef,
6336
+ adviceId,
6337
+ advisor: model ?? advisorRef,
6069
6338
  agentRulesBytes,
6339
+ followUp,
6070
6340
  preferenceBytes,
6071
6341
  question,
6072
6342
  text: markdown
@@ -6223,7 +6493,7 @@ var registerModelCommands = (runtime) => {
6223
6493
  handler: (args, ctx) => activateAdvisor(runtime, args, ctx)
6224
6494
  });
6225
6495
  runtime.pi.registerCommand("advisor-models", {
6226
- description: "Select and persist the Executor and Advisor models with reasoning levels",
6496
+ description: "Select and persist the Executor, Advisor, and optional fallback models with reasoning levels",
6227
6497
  handler: async (_args, ctx) => {
6228
6498
  if (!(loadCommandConfig(ctx) && ctx.hasUI)) {
6229
6499
  return;
@@ -6232,6 +6502,7 @@ var registerModelCommands = (runtime) => {
6232
6502
  const selection = await selectAdvisorModels(ctx, {
6233
6503
  advisor: persisted.advisor ? advisorRef : "",
6234
6504
  advisorEffort: advisorEffortRef,
6505
+ advisorFallbackModel: advisorFallbackModelRef,
6235
6506
  executor: runtime.pendingExecutorModelRef ?? (persisted.executor ? executorRef : ""),
6236
6507
  executorEffort: executorEffortRef,
6237
6508
  selectAdvisor: true,
@@ -6242,6 +6513,7 @@ var registerModelCommands = (runtime) => {
6242
6513
  }
6243
6514
  setExecutorRef(selection.executor);
6244
6515
  setAdvisorRef(selection.advisor);
6516
+ setAdvisorFallbackModelRef(selection.advisorFallbackModel);
6245
6517
  setExecutorEffortRef(selection.executorEffort);
6246
6518
  setAdvisorEffortRef(selection.advisorEffort);
6247
6519
  const path = saveConfig(ctx, {
@@ -7288,6 +7560,25 @@ var jevItems = (settings, theme, tui) => [
7288
7560
  ])
7289
7561
  }
7290
7562
  ];
7563
+ var advisorFallbackModelItem = (settings, modelRefs, keybindings, theme, tui) => ({
7564
+ currentValue: settings.fallbackModel ?? FALLBACK_ADVISOR_MODEL_DISABLED,
7565
+ description: "Retry a failed Advisor request once with this optional model; the primary model remains unchanged.",
7566
+ id: "fallbackModel",
7567
+ label: "Fallback Advisor model",
7568
+ submenu: (_currentValue, done) => new SearchableModelSelector({
7569
+ allOptions: [
7570
+ FALLBACK_ADVISOR_MODEL_DISABLED,
7571
+ ...new Set(modelRefs ?? (settings.fallbackModel ? [settings.fallbackModel] : []))
7572
+ ],
7573
+ currentOption: settings.fallbackModel ?? FALLBACK_ADVISOR_MODEL_DISABLED,
7574
+ keybindings: keybindings ?? getKeybindings(),
7575
+ onCancel: done,
7576
+ onSelect: (value) => done(value === FALLBACK_ADVISOR_MODEL_DISABLED ? "" : value),
7577
+ theme,
7578
+ title: "Fallback Advisor model",
7579
+ tui
7580
+ })
7581
+ });
7291
7582
  var advisorModelWhitelistItem = (settings, modelRefs, keybindings, theme, tui) => ({
7292
7583
  currentValue: settings.modelWhitelist?.length ? settings.modelWhitelist.join(", ") : "Any model",
7293
7584
  description: "Only the exact provider/model references listed here may call the Advisor; an empty list allows every model.",
@@ -7309,6 +7600,7 @@ var advisorModelWhitelistItem = (settings, modelRefs, keybindings, theme, tui) =
7309
7600
  });
7310
7601
  var createSettingsItems = ({
7311
7602
  effortLevels,
7603
+ fallbackModel,
7312
7604
  modelWhitelist,
7313
7605
  presets,
7314
7606
  settings,
@@ -7336,7 +7628,8 @@ var createSettingsItems = ({
7336
7628
  id: "alwaysOn",
7337
7629
  label: "Always on",
7338
7630
  values: TOGGLE_VALUES
7339
- }
7631
+ },
7632
+ fallbackModel
7340
7633
  ];
7341
7634
  if (settings.simpleMode) {
7342
7635
  items.push(modelWhitelist, toggle("disableSameModel", "Disable same-model Advisor", "Skip advice when the active Executor and Advisor use the same provider/model; turn off to allow higher-effort same-model reviews.", settings.disableSameModel, true));
@@ -7558,6 +7851,10 @@ var applyCoreMutation = (settings, id, value, presets) => {
7558
7851
  settings.alwaysOn = value === "On";
7559
7852
  return true;
7560
7853
  }
7854
+ case "fallbackModel": {
7855
+ settings.fallbackModel = value === FALLBACK_ADVISOR_MODEL_DISABLED ? undefined : value.trim() || undefined;
7856
+ return true;
7857
+ }
7561
7858
  case "effort": {
7562
7859
  settings.effort = value;
7563
7860
  return true;
@@ -7742,9 +8039,11 @@ class AdvisorSettingsSelector {
7742
8039
  }
7743
8040
  return defaultLabel(text, selected);
7744
8041
  };
8042
+ const fallbackModel = advisorFallbackModelItem(this.settings, this.options.modelRefs, this.options.keybindings, this.options.theme, this.options.tui);
7745
8043
  const modelWhitelist = advisorModelWhitelistItem(this.settings, this.options.modelRefs, this.options.keybindings, this.options.theme, this.options.tui);
7746
8044
  const items = createSettingsItems({
7747
8045
  effortLevels: this.options.effortLevels,
8046
+ fallbackModel,
7748
8047
  modelWhitelist,
7749
8048
  presets: this.presets,
7750
8049
  settings: this.settings,
@@ -7798,6 +8097,7 @@ var applySessionSettings = (settings) => {
7798
8097
  setAdvisorEffortRef(settings.effort === "Default (Model Default)" ? undefined : settings.effort);
7799
8098
  setContextMaxCharsRef(settings.contextMaxChars);
7800
8099
  setAdvisorPlanGateRef(settings.planGate);
8100
+ setAdvisorFallbackModelRef(settings.fallbackModel);
7801
8101
  setAdvisorFailureGateRef(settings.failureGate);
7802
8102
  setAdvisorCompletionGateRef(settings.completionGate);
7803
8103
  setAdvisorDisableSameModelRef(settings.disableSameModel ?? true);
@@ -8099,7 +8399,32 @@ var registerJevTurnGate = (on, registration) => {
8099
8399
  };
8100
8400
 
8101
8401
  // src/tools/register-ask-advisor.ts
8402
+ import { Type as Type2 } from "typebox";
8403
+
8404
+ // src/tools/advisor-output.ts
8102
8405
  import { Type } from "typebox";
8406
+ var advisorOutputSchema = Type.Object({
8407
+ adviceId: Type.Optional(Type.String()),
8408
+ advisor: Type.Optional(Type.String()),
8409
+ followUp: Type.Optional(Type.Boolean()),
8410
+ jev: Type.Optional(Type.Object({
8411
+ kind: Type.Union([Type.Literal("screened"), Type.Literal("repeat")]),
8412
+ reason: Type.String(),
8413
+ skipped: Type.Boolean()
8414
+ })),
8415
+ skipReason: Type.Optional(Type.String()),
8416
+ text: Type.String({
8417
+ description: "Advisor Markdown or consultation skip notice."
8418
+ }),
8419
+ usage: Type.Optional(Type.Object({
8420
+ cacheRead: Type.Optional(Type.Number()),
8421
+ cacheWrite: Type.Optional(Type.Number()),
8422
+ cost: Type.Optional(Type.Number()),
8423
+ input: Type.Optional(Type.Number()),
8424
+ output: Type.Optional(Type.Number()),
8425
+ totalTokens: Type.Optional(Type.Number())
8426
+ }))
8427
+ });
8103
8428
 
8104
8429
  // src/tools/render-advisor-result.ts
8105
8430
  import { getMarkdownTheme as getMarkdownTheme4 } from "@earendil-works/pi-coding-agent";
@@ -8188,6 +8513,9 @@ var renderPartialAdvisorResult = (box, result, expanded, theme, context) => {
8188
8513
  var thinkingPreview = (details) => details?.thinking?.trim() ? `${details.thinking.slice(0, 300)}${details.thinking.length > 300 ? "…" : ""}` : "";
8189
8514
  var finalResultLines = (details, advice, theme) => {
8190
8515
  const lines = [renderAdvisorResponseHeader(hasSoundVerdict(advice), theme)];
8516
+ if (details?.followUp) {
8517
+ lines.push(theme.fg("dim", " Follow-up consultation"));
8518
+ }
8191
8519
  if (details?.advisor) {
8192
8520
  lines.push(theme.fg("dim", ` ${details.advisor}`));
8193
8521
  }
@@ -8255,6 +8583,32 @@ var assertAdvisorModelAccess = (ctx) => {
8255
8583
  throw new Error(accessReason);
8256
8584
  }
8257
8585
  };
8586
+ var resolveFollowUp = (params, session) => {
8587
+ if (!params.followUpTo) {
8588
+ return;
8589
+ }
8590
+ if (!params.question?.trim() || params.draft !== undefined || params.gitContext !== undefined || params.includeTrackedFiles?.length || params.includeUntracked?.length) {
8591
+ throw new Error("Follow-up consultations accept only followUpTo and a non-empty question. Issue a fresh consultation for new context or attachments.");
8592
+ }
8593
+ const cached = session.followUpFor(params.followUpTo);
8594
+ if (!cached.payload) {
8595
+ throw new Error(`Follow-up unavailable for adviceId ${params.followUpTo}: ${cached.reason ?? "the cached payload is unavailable"}. Issue a fresh consultation.`);
8596
+ }
8597
+ return { adviceId: params.followUpTo, payload: cached.payload };
8598
+ };
8599
+ var sameModelNoticeFor = (modelRef) => (modelRef ?? advisorRef) === advisorRef ? sameModelAdvisorNotice : fallbackSameModelAdvisorNotice;
8600
+ var skippedSameModelResult = (id, notice, reservedCalls, session) => {
8601
+ session.releaseCall(id);
8602
+ reservedCalls.delete(id);
8603
+ return {
8604
+ content: [{ text: notice, type: "text" }],
8605
+ details: {
8606
+ skipReason: notice,
8607
+ text: notice
8608
+ },
8609
+ structuredContent: { skipReason: notice, text: notice }
8610
+ };
8611
+ };
8258
8612
  var claimTrackedHandoff = (session, includeTrackedFiles) => {
8259
8613
  if (!includeTrackedFiles?.length) {
8260
8614
  return;
@@ -8275,23 +8629,18 @@ var registerAskAdvisorTool = ({
8275
8629
  session
8276
8630
  }) => {
8277
8631
  pi.registerTool({
8278
- description: "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.",
8632
+ description: "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. Use followUpTo with a new question to continue one cached consultation without rebuilding its redacted context. If the Advisor explicitly names a missing file, you may make a sequential fresh consultation with includeTrackedFiles when enabled and relevant. In Pi Codemode, gather and filter deterministic tool results first, then call tools.ask_advisor. Nested results are not automatically reconstructed: pass only permitted, concise summaries in draft (untrusted, capped at 8 KiB, not independently verified). Draft does not inherit source-tool disclosure policies; use gitContext for patches within the user's allowance.",
8279
8633
  async execute(_id, params, signal, onUpdate, ctx) {
8280
- const skipSameModel = () => {
8281
- session.releaseCall(_id);
8282
- reservedCalls.delete(_id);
8283
- return {
8284
- content: [{ text: sameModelAdvisorNotice, type: "text" }],
8285
- details: {
8286
- skipReason: sameModelAdvisorNotice,
8287
- text: sameModelAdvisorNotice
8288
- }
8289
- };
8290
- };
8634
+ let followUp;
8291
8635
  try {
8292
8636
  loadConfig(ctx);
8293
- if (sameModelAdvisorDisabled(ctx)) {
8294
- return skipSameModel();
8637
+ followUp = resolveFollowUp(params, session);
8638
+ if (followUp) {
8639
+ assertAdvisorFollowUpPrivacy(followUp.payload, ctx);
8640
+ }
8641
+ const modelRef = followUp?.payload.model;
8642
+ if (sameModelAdvisorDisabled(ctx, ctx.model, modelRef)) {
8643
+ return skippedSameModelResult(_id, sameModelNoticeFor(modelRef), reservedCalls, session);
8295
8644
  }
8296
8645
  assertAdvisorModelAccess(ctx);
8297
8646
  } catch (error) {
@@ -8325,20 +8674,22 @@ var registerAskAdvisorTool = ({
8325
8674
  const skipText = screeningSkipText(screening);
8326
8675
  session.releaseCall(_id);
8327
8676
  reservedCalls.delete(_id);
8677
+ const skipped = {
8678
+ jev: {
8679
+ kind: screening.kind,
8680
+ reason: screening.reason,
8681
+ skipped: true
8682
+ },
8683
+ text: skipText
8684
+ };
8328
8685
  return {
8329
8686
  content: [{ text: skipText, type: "text" }],
8330
- details: {
8331
- jev: {
8332
- kind: screening.kind,
8333
- reason: screening.reason,
8334
- skipped: true
8335
- },
8336
- text: skipText
8337
- }
8687
+ details: skipped,
8688
+ structuredContent: skipped
8338
8689
  };
8339
8690
  }
8340
- if (sameModelAdvisorDisabled(ctx)) {
8341
- return skipSameModel();
8691
+ if (sameModelAdvisorDisabled(ctx, ctx.model, followUp?.payload.model)) {
8692
+ return skippedSameModelResult(_id, sameModelNoticeFor(followUp?.payload.model), reservedCalls, session);
8342
8693
  }
8343
8694
  if (!simpleMode && !session.canConsult(getAdvisorMaxCallsPerSession(), _id)) {
8344
8695
  throw new Error("Advisor call budget exhausted for this session.");
@@ -8368,7 +8719,8 @@ var registerAskAdvisorTool = ({
8368
8719
  const result = await requestAdvisor(ctx, resolveAdvisorRequest(params.question), signal, (t, tx) => coalescedUpdate.update({
8369
8720
  content: [{ text: tx, type: "text" }],
8370
8721
  details: {
8371
- advisor: advisorRef,
8722
+ advisor: followUp?.payload.model ?? advisorRef,
8723
+ followUp: Boolean(followUp),
8372
8724
  question: resolveAdvisorRequest(params.question),
8373
8725
  scout: scoutDetails,
8374
8726
  text: tx,
@@ -8379,17 +8731,19 @@ var registerAskAdvisorTool = ({
8379
8731
  coalescedUpdate.update({
8380
8732
  content: [{ text: scoutDetails.text ?? "", type: "text" }],
8381
8733
  details: {
8382
- advisor: advisorRef,
8734
+ advisor: followUp?.payload.model ?? advisorRef,
8735
+ followUp: Boolean(followUp),
8383
8736
  question: resolveAdvisorRequest(params.question),
8384
8737
  scout: scoutDetails
8385
8738
  }
8386
8739
  });
8387
- }, _id);
8740
+ }, _id, followUp, (adviceId, payload, replacesAdviceId) => session.captureFollowUp(adviceId, payload, replacesAdviceId));
8388
8741
  flushUpdate();
8389
8742
  session.issueAdvice(result.adviceId, result.markdown, result.trigger, Boolean(result.draftBytes), normalizedQuestion);
8390
8743
  session.recordInvocation({
8391
8744
  cost: advisorUsageCost(result.usage),
8392
8745
  executionEffect: "continued",
8746
+ followUp: result.followUp,
8393
8747
  kind: "markdown",
8394
8748
  model: result.model,
8395
8749
  trigger: "executor-requested",
@@ -8403,6 +8757,7 @@ var registerAskAdvisorTool = ({
8403
8757
  advisor: result.model,
8404
8758
  agentRulesBytes: result.agentRulesBytes,
8405
8759
  draftBytes: result.draftBytes,
8760
+ followUp: result.followUp,
8406
8761
  imageBytes: result.imageBytes,
8407
8762
  imageCount: result.imageCount,
8408
8763
  imageOmissions: result.imageOmissions,
@@ -8418,16 +8773,26 @@ var registerAskAdvisorTool = ({
8418
8773
  if (usage) {
8419
8774
  details.usage = usage;
8420
8775
  }
8776
+ const structuredContent = {
8777
+ adviceId: result.adviceId,
8778
+ advisor: result.model,
8779
+ followUp: Boolean(result.followUp),
8780
+ text: result.markdown
8781
+ };
8782
+ if (usage) {
8783
+ structuredContent.usage = usage;
8784
+ }
8421
8785
  const response = {
8422
8786
  content: [
8423
8787
  {
8424
- text: `Advisor (${result.model})
8788
+ text: `${result.followUp ? "Advisor follow-up" : "Advisor"} (${result.model})
8425
8789
 
8426
8790
  ${result.markdown}`,
8427
8791
  type: "text"
8428
8792
  }
8429
8793
  ],
8430
- details
8794
+ details,
8795
+ structuredContent
8431
8796
  };
8432
8797
  if (piUsage) {
8433
8798
  response.usage = piUsage;
@@ -8440,7 +8805,7 @@ ${result.markdown}`,
8440
8805
  executionEffect: "continued",
8441
8806
  failure: "provider-error",
8442
8807
  kind: "markdown",
8443
- model: advisorRef,
8808
+ model: followUp?.payload.model ?? advisorRef,
8444
8809
  trigger: "executor-requested"
8445
8810
  });
8446
8811
  updateAdvisorUsageStatus(ctx, session);
@@ -8456,32 +8821,36 @@ ${result.markdown}`,
8456
8821
  },
8457
8822
  label: "Ask Advisor",
8458
8823
  name: "ask_advisor",
8459
- parameters: Type.Object({
8460
- draft: Type.Optional(Type.String({
8824
+ outputSchema: advisorOutputSchema,
8825
+ parameters: Type2.Object({
8826
+ draft: Type2.Optional(Type2.String({
8461
8827
  description: "Concise untrusted draft for plan or completion review; claims are not verification evidence."
8462
8828
  })),
8463
- force: Type.Optional(Type.Boolean({
8829
+ followUpTo: Type2.Optional(Type2.String({
8830
+ description: "Opaque adviceId from a prior successful consultation. Requires a new non-empty question and reuses that consultation's redacted payload; do not combine with draft, Git context, or file attachments."
8831
+ })),
8832
+ force: Type2.Optional(Type2.Boolean({
8464
8833
  description: "Set true only when you judge a decision genuinely material after a consultation was screened out; bypasses screening."
8465
8834
  })),
8466
- gitContext: Type.Optional(Type.Union([Type.Literal("none"), Type.Literal("summary"), Type.Literal("full")], {
8835
+ gitContext: Type2.Optional(Type2.Union([Type2.Literal("none"), Type2.Literal("summary"), Type2.Literal("full")], {
8467
8836
  description: "How much of the working tree to include. Use full when the review depends on the exact code changes, such as a completion review. Use summary for changed file names only, or none when the question is not about the current changes. The user's configured allowance is the ceiling and a larger request is narrowed to it."
8468
8837
  })),
8469
- includeTrackedFiles: Type.Optional(Type.Array(Type.String({
8838
+ includeTrackedFiles: Type2.Optional(Type2.Array(Type2.String({
8470
8839
  description: "Exact tracked repository-relative files to attach after the Advisor explicitly names a file it cannot review. Requires global advisorTrackedFileContent consent; current working-tree contents are sent as untrusted data."
8471
8840
  }))),
8472
- includeUntracked: Type.Optional(Type.Array(Type.String({
8841
+ includeUntracked: Type2.Optional(Type2.Array(Type2.String({
8473
8842
  description: "Exact new repository-relative files to include only when user configuration allows it."
8474
8843
  }))),
8475
- question: Type.Optional(Type.String({
8844
+ question: Type2.Optional(Type2.String({
8476
8845
  description: "The specific question or decision to get advice on. Omit this for normal reviews: the Advisor already has the conversation context."
8477
8846
  }))
8478
8847
  }),
8479
8848
  promptGuidelines: [
8480
- "Call ask_advisor with an empty object for general consultation. For a plan or completion review, include a concise draft naming work, validation, and remaining risks; its claims are not evidence. If the Advisor explicitly says it cannot review a specifically named file, you may make a sequential follow-up call with includeTrackedFiles when the file is relevant, permitted, and worth the shared call budget; do not infer paths or retry automatically."
8849
+ "Call ask_advisor with an empty object for general consultation. For a plan or completion review, include a concise draft naming work, validation, and remaining risks; its claims are not evidence. To drill into an existing answer, pass its adviceId as followUpTo with a new question and no draft, Git context, or attachments. If the Advisor explicitly says it cannot review a specifically named file, you may make a sequential follow-up call with includeTrackedFiles when the file is relevant, permitted, and worth the shared call budget; do not infer paths or retry automatically."
8481
8850
  ],
8482
8851
  promptSnippet: "Consult the Advisor using its existing context; attach a draft for plan or completion review",
8483
8852
  renderCall(args, theme) {
8484
- return renderAdvisorCallBox(args.question?.trim(), theme);
8853
+ return renderAdvisorCallBox(args.question?.trim(), theme, Boolean(args.followUpTo));
8485
8854
  },
8486
8855
  renderResult(result, options, theme, context) {
8487
8856
  return renderAdvisorResult(result, options, theme, context);
@@ -8678,6 +9047,14 @@ ${guidelines.map((rule) => `- ${rule}`).join(`
8678
9047
  `)}`
8679
9048
  } : undefined;
8680
9049
  });
9050
+ pi.on("input", () => {
9051
+ session.clearFollowUps();
9052
+ });
9053
+ pi.on("tool_result", (event) => {
9054
+ if (event.toolName !== "ask_advisor") {
9055
+ session.advanceFollowUpToolCall();
9056
+ }
9057
+ });
8681
9058
  pi.on("tool_call", (event, ctx) => {
8682
9059
  if (session.blocked) {
8683
9060
  return {
@@ -8717,6 +9094,7 @@ ${guidelines.map((rule) => `- ${rule}`).join(`
8717
9094
  });
8718
9095
  pi.on("session_shutdown", (_event, ctx) => {
8719
9096
  reservedCalls.clear();
9097
+ session.clearFollowUps();
8720
9098
  session.clearCallReservations();
8721
9099
  scoutStatus.clear(ctx);
8722
9100
  herdrBlock.clear();
@@ -8728,7 +9106,7 @@ ${guidelines.map((rule) => `- ${rule}`).join(`
8728
9106
 
8729
9107
  // src/tools/register-outcome.ts
8730
9108
  import { Text as Text6 } from "@earendil-works/pi-tui";
8731
- import { Type as Type2 } from "typebox";
9109
+ import { Type as Type3 } from "typebox";
8732
9110
  var registerOutcomeTool = ({
8733
9111
  appendOutcome: appendAdvisorOutcome,
8734
9112
  pi,
@@ -8782,10 +9160,10 @@ var registerOutcomeTool = ({
8782
9160
  },
8783
9161
  label: "Record Advisor Outcome",
8784
9162
  name: "record_advisor_outcome",
8785
- parameters: Type2.Object({
8786
- adoption: Type2.String({ enum: ADOPTIONS }),
8787
- adviceId: Type2.String(),
8788
- validationStatus: Type2.String({ enum: VALIDATIONS })
9163
+ parameters: Type3.Object({
9164
+ adoption: Type3.String({ enum: ADOPTIONS }),
9165
+ adviceId: Type3.String(),
9166
+ validationStatus: Type3.String({ enum: VALIDATIONS })
8789
9167
  }),
8790
9168
  renderCall: () => new Text6("[advisor] Record outcome", 0, 0),
8791
9169
  renderResult: (result) => new Text6(textFrom(result.content), 0, 0)