@fiale-plus/pi-rogue-advisor 0.1.9 → 0.1.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fiale-plus/pi-rogue-advisor",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "description": "Pi-Rogue advisor extension for Pi — multi-model support, SOTA model suggestion, cache-aware session advisory.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,6 +1,14 @@
1
1
  import { describe, expect, it, vi } from "vitest";
2
2
  import { completeSimple } from "@earendil-works/pi-ai";
3
- import { normalizeAdvisorConfig, shouldRunCheckin, type AdvisorConfig, completeWithHigherAdvisorModel, completeWithModelFallback } from "./extension.js";
3
+ import {
4
+ buildAdvisorCheckinPrompt,
5
+ completeWithHigherAdvisorModel,
6
+ completeWithModelFallback,
7
+ contentText,
8
+ normalizeAdvisorConfig,
9
+ shouldRunCheckin,
10
+ type AdvisorConfig,
11
+ } from "./extension.js";
4
12
 
5
13
  vi.mock("@earendil-works/pi-ai", async () => {
6
14
  const actual = await vi.importActual<typeof import("@earendil-works/pi-ai")>("@earendil-works/pi-ai");
@@ -72,6 +80,14 @@ describe("AdvisorConfig", () => {
72
80
  });
73
81
  });
74
82
 
83
+ describe("advisor message extraction", () => {
84
+ it("extracts nested structured content without object string leakage", () => {
85
+ expect(contentText({ content: [{ type: "text", text: "done" }] })).toBe("done");
86
+ expect(contentText([{ type: "toolResult", content: [{ type: "text", text: "ok" }] }])).toBe("ok");
87
+ expect(contentText({ arbitrary: "shape" })).toBe("");
88
+ });
89
+ });
90
+
75
91
  describe("mid-hour check-ins", () => {
76
92
  it("does not run immediately after session start", () => {
77
93
  const cfg = normalizeAdvisorConfig({ checkins: "mid-hour", checkinIntervalMinutes: 30 });
@@ -110,6 +126,26 @@ describe("mid-hour check-ins", () => {
110
126
  })),
111
127
  ).toBe("queued mid-session check-in");
112
128
  });
129
+
130
+ it("keeps check-in guidance anchored to the active goal", () => {
131
+ const prompt = buildAdvisorCheckinPrompt(
132
+ "loop_tick",
133
+ [
134
+ "Orchestration:",
135
+ "- Goal: active — Autoresearch: solve advisor weaknesses",
136
+ "- Autoresearch: active — solve advisor weaknesses; cycles=1, doneAttempts=0",
137
+ "- Loop: active every 5m — Run one autoresearch cycle toward the active goal.",
138
+ ].join("\n"),
139
+ "Task: solve advisor weaknesses\nNotes:\n- found shallow mid-hour feedback",
140
+ );
141
+
142
+ expect(prompt).toContain("alignment reviewer");
143
+ expect(prompt).toContain("Do not create a new task");
144
+ expect(prompt).toContain("preserve its research question");
145
+ expect(prompt).toContain("solving the named weakness");
146
+ expect(prompt).toContain("Nudge: <one concrete next action that continues the active goal>");
147
+ expect(prompt).toContain("found shallow mid-hour feedback");
148
+ });
113
149
  });
114
150
 
115
151
 
package/src/extension.ts CHANGED
@@ -267,9 +267,16 @@ function renderAdvisorHint(message: any, options: { expanded?: boolean }, theme:
267
267
  return box;
268
268
  }
269
269
 
270
- /** Extract readable text from message content (handles both string and content-block arrays) */
271
- function contentText(content: unknown): string {
270
+ /** Extract readable text from message content (handles strings, blocks, and nested message payloads). */
271
+ export function contentText(content: unknown): string {
272
272
  if (typeof content === "string") return content.trim();
273
+ if (content && typeof content === "object" && !Array.isArray(content)) {
274
+ const obj = content as Record<string, unknown>;
275
+ if (typeof obj.text === "string") return obj.text.trim();
276
+ if (obj.content !== undefined) return contentText(obj.content);
277
+ if (obj.message !== undefined) return contentText(obj.message);
278
+ return "";
279
+ }
273
280
  if (!Array.isArray(content)) return String(content ?? "").trim();
274
281
  const parts: string[] = [];
275
282
  for (const item of content) {
@@ -278,6 +285,14 @@ function contentText(content: unknown): string {
278
285
  const obj = item as Record<string, unknown>;
279
286
  if (obj.type === "text" && typeof obj.text === "string") parts.push(obj.text);
280
287
  else if (typeof obj.text === "string") parts.push(obj.text);
288
+ else if (obj.content !== undefined) {
289
+ const nested = contentText(obj.content);
290
+ if (nested) parts.push(nested);
291
+ }
292
+ else if (obj.message !== undefined) {
293
+ const nested = contentText(obj.message);
294
+ if (nested) parts.push(nested);
295
+ }
281
296
  }
282
297
  return parts.join("\n").replace(/\s+/g, " ").trim();
283
298
  }
@@ -333,13 +348,27 @@ function orchestrationSnapshotText(ctx: any): string {
333
348
  : "no active goal";
334
349
  return [
335
350
  "Orchestration:",
336
- `- Goal: ${goalActive ? `active — ${truncate(snapshot.goal, 140)}` : "off"}`,
337
- `- Autoresearch: ${researchActive ? `active — cycles=${snapshot.research.cycles ?? 0}, doneAttempts=${snapshot.research.doneAttempts ?? 0}${snapshot.research.lastResult ? `, last=${snapshot.research.lastResult}` : ""}` : "off"}`,
338
- `- Loop: ${loopActive ? `active every ${snapshot.loop.interval || "?"} — ${truncate(snapshot.loop.instruction || "", 120)}` : "off"}`,
351
+ `- Goal: ${goalActive ? `active — ${truncate(snapshot.goal, 360)}` : "off"}`,
352
+ `- Autoresearch: ${researchActive ? `active — ${truncate(snapshot.research.instruction || "", 240)}; cycles=${snapshot.research.cycles ?? 0}, doneAttempts=${snapshot.research.doneAttempts ?? 0}${snapshot.research.lastResult ? `, last=${snapshot.research.lastResult}` : ""}` : "off"}`,
353
+ `- Loop: ${loopActive ? `active every ${snapshot.loop.interval || "?"} — ${truncate(snapshot.loop.instruction || "", 260)}` : "off"}`,
339
354
  `- Status: ${status}`,
340
355
  ].join("\n");
341
356
  }
342
357
 
358
+ export function buildAdvisorCheckinPrompt(source: string, orchestration: string, sessionBrief: string): string {
359
+ return [
360
+ `Mid-session check-in (${source})`,
361
+ "Role: alignment reviewer for the active work. Do not create a new task, research direction, benchmark, script, artifact, or model switch unless the active goal explicitly asks for it.",
362
+ "Stay anchored to the active goal/autoresearch/loop. If autoresearch is active, preserve its research question and judge whether the latest work is gathering evidence toward that question.",
363
+ "Bad nudge examples: research the existence of weaknesses instead of solving the named weakness; create a script/report about weaknesses when the goal is to fix advisor behavior; swap to a shallower research mode.",
364
+ "Return exactly two short lines:",
365
+ "Status: on_track|stuck|off_track - <why, tied to the active goal>",
366
+ "Nudge: <one concrete next action that continues the active goal>",
367
+ orchestration,
368
+ sessionBrief ? `Session brief:\n${sessionBrief}` : "",
369
+ ].filter(Boolean).join("\n\n");
370
+ }
371
+
343
372
  function setPiRogueStatus(ctx: any, config = loadConfig(), state = loadState()): void {
344
373
  const normalized = normalizeAdvisorConfig(config);
345
374
  const checkin = normalized.checkins === "off" ? "checkins off" : `checkins ${normalized.checkinIntervalMinutes}m`;
@@ -406,26 +435,19 @@ async function maybeAdvisorCheckin(pi: ExtensionAPI, ctx: any, source: string):
406
435
 
407
436
  checkinLocks.add(key);
408
437
  try {
438
+ const prompt = buildAdvisorCheckinPrompt(source, orchestrationSnapshotText(ctx), brief(state));
409
439
  const completed = await completeWithHigherAdvisorModel(
410
440
  ctx,
411
441
  config,
412
- [
413
- `Mid-session check-in (${source}): briefly assess whether the current session is on track, stuck, or missing a higher-leverage next step.`,
414
- orchestrationSnapshotText(ctx),
415
- "If a goal exists but autoresearch/loop progression is off, call out the setup gap. Do not start or change orchestration; return one concrete nudge.",
416
- ].join("\n\n"),
442
+ prompt,
417
443
  [
418
444
  {
419
445
  role: "user",
420
- content: [
421
- `Mid-session check-in (${source}): briefly assess whether the current session is on track, stuck, or missing a higher-leverage next step.`,
422
- orchestrationSnapshotText(ctx),
423
- "If a goal exists but autoresearch/loop progression is off, call out the setup gap. Do not start or change orchestration; return one concrete nudge.",
424
- ].join("\n\n"),
446
+ content: prompt,
425
447
  timestamp: new Date().toISOString(),
426
448
  },
427
449
  ],
428
- { maxTokens: 600, reasoning: "medium" as ThinkingLevel },
450
+ { maxTokens: 260, reasoning: "low" as ThinkingLevel },
429
451
  );
430
452
  if (!completed) return false;
431
453
 
@@ -585,15 +607,17 @@ async function doReview(pi: ExtensionAPI, ctx: any, trigger: string, delta: stri
585
607
  const gatePrediction = binaryGatePredict(reviewInput.text);
586
608
  let reviewRoute = reviewHeuristic;
587
609
  if (gatePrediction && gatePrediction.confidence >= 0.55 && !reviewHeuristic.safety) {
588
- const binLabel = gatePrediction.decision === "continue" ? "continue" as const : "escalate_to_advisor" as const;
610
+ const gateContinues = gatePrediction.decision === "continue";
589
611
  reviewRoute = {
590
612
  ...reviewHeuristic,
613
+ label: gateContinues ? "abstain" : reviewHeuristic.label,
614
+ confidence: gatePrediction.confidence,
591
615
  source: "model",
592
- reason: gatePrediction.decision === "continue"
616
+ reason: gateContinues
593
617
  ? "local gate predicts continue"
594
618
  : "local gate predicts review",
595
- review: binLabel === "continue" ? "off" as const : reviewHeuristic.review,
596
- escalate: binLabel === "escalate_to_advisor",
619
+ review: gateContinues ? "off" as const : reviewHeuristic.review,
620
+ escalate: gateContinues ? false : reviewHeuristic.escalate,
597
621
  };
598
622
  }
599
623
  appendRouteLog(reviewRoute);