@juicesharp/rpiv-advisor 1.6.0 → 1.7.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.
Files changed (3) hide show
  1. package/advisor.ts +95 -14
  2. package/index.ts +5 -3
  3. package/package.json +1 -1
package/advisor.ts CHANGED
@@ -22,10 +22,10 @@ import { completeSimple, getSupportedThinkingLevels, type Message, type Thinking
22
22
  import {
23
23
  type AgentToolResult,
24
24
  type AgentToolUpdateCallback,
25
+ buildSessionContext,
25
26
  convertToLlm,
26
27
  type ExtensionAPI,
27
28
  type ExtensionContext,
28
- type SessionEntry,
29
29
  type ToolInfo,
30
30
  } from "@earendil-works/pi-coding-agent";
31
31
  import type { SelectItem } from "@earendil-works/pi-tui";
@@ -84,6 +84,10 @@ const msgAdvisorEnabled = (label: string, effort: ThinkingLevel | undefined) =>
84
84
  `Advisor: ${label}${effort ? `, ${effort}` : ""}`;
85
85
  const msgAdvisorRestored = (label: string, effort: ThinkingLevel | undefined) =>
86
86
  `Advisor restored: ${label}${effort ? `, ${effort}` : ""}`;
87
+ const msgAdvisorRestoredInactive = (label: string, effort: ThinkingLevel | undefined) =>
88
+ `Advisor restored: ${label}${effort ? `, ${effort}` : ""} (inactive for current executor)`;
89
+ const msgAdvisorEnabledInactive = (label: string, effort: ThinkingLevel | undefined) =>
90
+ `Advisor: ${label}${effort ? `, ${effort}` : ""} (inactive for current executor)`;
87
91
  const msgConsulting = (label: string, effort: ThinkingLevel | undefined) =>
88
92
  `Consulting advisor (${label}${effort ? `, ${effort}` : ""})…`;
89
93
 
@@ -100,6 +104,7 @@ interface AdvisorConfig {
100
104
  modelKey?: string;
101
105
  effort?: ThinkingLevel;
102
106
  guidance?: GuidanceFields;
107
+ disabledForModels?: string[];
103
108
  }
104
109
 
105
110
  export function loadAdvisorConfig(): AdvisorConfig {
@@ -128,6 +133,11 @@ function validateGuidanceFields(fields: unknown): GuidanceFields {
128
133
  return result;
129
134
  }
130
135
 
136
+ function validateDisabledForModels(value: unknown): string[] {
137
+ if (!Array.isArray(value)) return [];
138
+ return value.filter((entry): entry is string => typeof entry === "string" && entry.length > 0);
139
+ }
140
+
131
141
  export function saveAdvisorConfig(key: string | undefined, effort: ThinkingLevel | undefined): void {
132
142
  const existing = loadAdvisorConfig();
133
143
  const config: AdvisorConfig = { ...existing };
@@ -297,12 +307,21 @@ export function setAdvisorEffort(effort: ThinkingLevel | undefined): void {
297
307
  selectedAdvisorEffort = effort;
298
308
  }
299
309
 
310
+ let disabledForModelsCache: string[] = [];
311
+
312
+ export function setDisabledForModels(models: string[]): void {
313
+ disabledForModelsCache = models;
314
+ }
315
+
300
316
  // ---------------------------------------------------------------------------
301
317
  // Session restoration — called from index.ts session_start handler
302
318
  // ---------------------------------------------------------------------------
303
319
 
304
320
  export function restoreAdvisorState(ctx: ExtensionContext, pi: ExtensionAPI): void {
305
321
  const config = loadAdvisorConfig();
322
+
323
+ setDisabledForModels(validateDisabledForModels(config.disabledForModels));
324
+
306
325
  if (!config.modelKey) return;
307
326
 
308
327
  const parsed = parseModelKey(config.modelKey);
@@ -321,6 +340,14 @@ export function restoreAdvisorState(ctx: ExtensionContext, pi: ExtensionAPI): vo
321
340
  setAdvisorEffort(config.effort);
322
341
  }
323
342
 
343
+ if (isExecutorBlocked(ctx)) {
344
+ if (ctx.hasUI) {
345
+ const advisorLabel = `${model.provider}:${model.id}`;
346
+ ctx.ui.notify(msgAdvisorRestoredInactive(advisorLabel, config.effort), "info");
347
+ }
348
+ return;
349
+ }
350
+
324
351
  const active = pi.getActiveTools();
325
352
  if (!active.includes(ADVISOR_TOOL_NAME)) {
326
353
  pi.setActiveTools([...active, ADVISOR_TOOL_NAME]);
@@ -377,14 +404,16 @@ async function executeAdvisor(
377
404
  }
378
405
 
379
406
  // Live-read every call — advisor runs mid-turn so any message_end snapshot
380
- // is always one turn stale. convertToLlm is pass-through for user/assistant/
381
- // toolResult (messages.js:111-114), so element refs are stable across calls
382
- // via the session store — content-stable output without a snapshot layer.
383
- const branch = ctx.sessionManager.getBranch();
384
- const agentMessages = branch
385
- .filter((e): e is SessionEntry & { type: "message" } => e.type === "message")
386
- .map((e) => e.message);
387
- const branchMessages = ensureUserTailForAdvisor(stripInflightAdvisorCall(convertToLlm(agentMessages)));
407
+ // is always one turn stale. buildSessionContext() preserves Pi's resolved
408
+ // LLM context, including compaction summaries and branch summaries, instead
409
+ // of replaying raw pre-compaction branch messages. convertToLlm is
410
+ // pass-through for user/assistant/toolResult (messages.js:111-114), so
411
+ // element refs are stable across calls via the session store.
412
+ const { messages: sessionMessages } = buildSessionContext(
413
+ ctx.sessionManager.getEntries(),
414
+ ctx.sessionManager.getLeafId(),
415
+ );
416
+ const branchMessages = ensureUserTailForAdvisor(stripInflightAdvisorCall(convertToLlm(sessionMessages)));
388
417
  const inventoryMessage = getInventoryMessage(pi.getAllTools());
389
418
  const messages: Message[] = inventoryMessage ? [inventoryMessage, ...branchMessages] : branchMessages;
390
419
 
@@ -509,8 +538,9 @@ export function registerAdvisorTool(pi: ExtensionAPI): void {
509
538
  // ---------------------------------------------------------------------------
510
539
 
511
540
  export function registerAdvisorBeforeAgentStart(pi: ExtensionAPI): void {
512
- pi.on("before_agent_start", async () => {
513
- if (!getAdvisorModel()) {
541
+ pi.on("before_agent_start", async (_event, ctx) => {
542
+ const shouldStrip = !getAdvisorModel() || isExecutorBlocked(ctx);
543
+ if (shouldStrip) {
514
544
  const active = pi.getActiveTools();
515
545
  if (active.includes(ADVISOR_TOOL_NAME)) {
516
546
  pi.setActiveTools(active.filter((n) => n !== ADVISOR_TOOL_NAME));
@@ -519,6 +549,38 @@ export function registerAdvisorBeforeAgentStart(pi: ExtensionAPI): void {
519
549
  });
520
550
  }
521
551
 
552
+ // ---------------------------------------------------------------------------
553
+ // model_select handler — mid-session model switches strip/re-add advisor
554
+ // ---------------------------------------------------------------------------
555
+
556
+ export function registerModelSelectHandler(pi: ExtensionAPI): void {
557
+ pi.on("model_select", async (event, ctx) => {
558
+ // session_start restore path is owned by restoreAdvisorState — it already
559
+ // activates the tool and notifies. Skipping "restore" here prevents a
560
+ // duplicate notification on initial model load.
561
+ if (event.source === "restore") return;
562
+
563
+ const advisor = getAdvisorModel();
564
+ if (!advisor) return;
565
+
566
+ const blocked = isModelBlocked(event.model);
567
+ const active = pi.getActiveTools();
568
+ const hasTool = active.includes(ADVISOR_TOOL_NAME);
569
+
570
+ if (blocked && hasTool) {
571
+ pi.setActiveTools(active.filter((n) => n !== ADVISOR_TOOL_NAME));
572
+ if (ctx.hasUI) {
573
+ ctx.ui.notify(`Advisor disabled for ${modelKey(event.model)}`, "info");
574
+ }
575
+ } else if (!blocked && !hasTool) {
576
+ pi.setActiveTools([...active, ADVISOR_TOOL_NAME]);
577
+ if (ctx.hasUI) {
578
+ ctx.ui.notify(msgAdvisorRestored(modelKey(advisor), getAdvisorEffort()), "info");
579
+ }
580
+ }
581
+ });
582
+ }
583
+
522
584
  // ---------------------------------------------------------------------------
523
585
  // /advisor slash command — opens selector panel for picking the advisor model
524
586
  // ---------------------------------------------------------------------------
@@ -527,6 +589,15 @@ function modelKey(m: { provider: string; id: string }): string {
527
589
  return `${m.provider}:${m.id}`;
528
590
  }
529
591
 
592
+ function isModelBlocked(model: Model<Api> | undefined): boolean {
593
+ if (!model) return false;
594
+ return disabledForModelsCache.includes(modelKey(model));
595
+ }
596
+
597
+ function isExecutorBlocked(ctx: ExtensionContext): boolean {
598
+ return isModelBlocked(ctx?.model);
599
+ }
600
+
530
601
  export function registerAdvisorCommand(pi: ExtensionAPI): void {
531
602
  pi.registerCommand("advisor", {
532
603
  description: "Configure the advisor model for the advisor-strategy pattern",
@@ -600,10 +671,20 @@ export function registerAdvisorCommand(pi: ExtensionAPI): void {
600
671
  setAdvisorEffort(effortChoice);
601
672
  setAdvisorModel(picked);
602
673
  saveAdvisorConfig(modelKey(picked), effortChoice);
603
- if (!activeHas) {
604
- pi.setActiveTools([...activeTools, ADVISOR_TOOL_NAME]);
674
+
675
+ // Re-read after the effort-picker await — the snapshot taken before
676
+ // `showEffortPicker` is stale once execution yields.
677
+ const activeToolsNow = pi.getActiveTools();
678
+ const activeHasNow = activeToolsNow.includes(ADVISOR_TOOL_NAME);
679
+ const blocked = isExecutorBlocked(ctx);
680
+ if (!activeHasNow && !blocked) {
681
+ pi.setActiveTools([...activeToolsNow, ADVISOR_TOOL_NAME]);
682
+ }
683
+ if (blocked) {
684
+ ctx.ui.notify(msgAdvisorEnabledInactive(modelKey(picked), effortChoice), "info");
685
+ } else {
686
+ ctx.ui.notify(msgAdvisorEnabled(modelKey(picked), effortChoice), "info");
605
687
  }
606
- ctx.ui.notify(msgAdvisorEnabled(modelKey(picked), effortChoice), "info");
607
688
  },
608
689
  });
609
690
  }
package/index.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * rpiv-advisor — Pi extension
3
3
  *
4
- * Registers the `advisor` tool, `/advisor` command, and the two lifecycle
5
- * hooks (session_start restore, before_agent_start strip) that together
6
- * implement the advisor-strategy pattern.
4
+ * Registers the `advisor` tool, `/advisor` command, and the three lifecycle
5
+ * hooks (session_start restore, before_agent_start strip, model_select
6
+ * re-evaluation) that together implement the advisor-strategy pattern.
7
7
  *
8
8
  * Config persists at ~/.config/rpiv-advisor/advisor.json. Tool name
9
9
  * preserved verbatim from rpiv-pi@7525a5d.
@@ -14,6 +14,7 @@ import {
14
14
  registerAdvisorBeforeAgentStart,
15
15
  registerAdvisorCommand,
16
16
  registerAdvisorTool,
17
+ registerModelSelectHandler,
17
18
  restoreAdvisorState,
18
19
  } from "./advisor.js";
19
20
 
@@ -21,6 +22,7 @@ export default function (pi: ExtensionAPI) {
21
22
  registerAdvisorTool(pi);
22
23
  registerAdvisorCommand(pi);
23
24
  registerAdvisorBeforeAgentStart(pi);
25
+ registerModelSelectHandler(pi);
24
26
 
25
27
  pi.on("session_start", async (_event, ctx) => {
26
28
  restoreAdvisorState(ctx, pi);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juicesharp/rpiv-advisor",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "description": "Pi extension. A second opinion the model can request from a stronger reviewer model before it acts.",
5
5
  "keywords": [
6
6
  "pi-package",