pi-advisor-flow 0.1.1 → 0.1.3

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/README.md +4 -0
  2. package/package.json +8 -3
  3. package/src/tools.ts +31 -22
package/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # pi-advisor
2
2
 
3
+ <div align="center">
4
+ <img src="https://raw.githubusercontent.com/philipbrembeck/pi-advisor/refs/heads/main/assets/screenshot.png" alt="Pi Advisor Flow Screenshot" width="600">
5
+ </div>
6
+
3
7
  An on-demand Advisor model flow for autonomous **Pi** coding agents.
4
8
 
5
9
  This extension introduces a strategic "Executor/Advisor" workflow, inspired by Claudes [Advisor](https://code.claude.com/docs/en/advisor).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-advisor-flow",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -11,7 +11,11 @@
11
11
  "main": "extensions/index.ts",
12
12
  "types": "extensions/index.ts",
13
13
  "keywords": [
14
- "pi-package"
14
+ "pi-package",
15
+ "pi-extension",
16
+ "pi-coding-agent",
17
+ "advisor",
18
+ "pi-advisor"
15
19
  ],
16
20
  "files": [
17
21
  "extensions",
@@ -21,7 +25,8 @@
21
25
  "pi": {
22
26
  "extensions": [
23
27
  "./extensions/index.ts"
24
- ]
28
+ ],
29
+ "image": "https://raw.githubusercontent.com/philipbrembeck/pi-advisor/refs/heads/main/assets/hero.png"
25
30
  },
26
31
  "scripts": {
27
32
  "test": "bun test",
package/src/tools.ts CHANGED
@@ -1,11 +1,14 @@
1
1
  import { stream, type Message, type AssistantMessage } from "@earendil-works/pi-ai/compat";
2
- import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
3
- import { Box, Text } from "@earendil-works/pi-tui";
2
+ import { getMarkdownTheme, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
3
+ import { Box, Markdown, Text } from "@earendil-works/pi-tui";
4
4
  import { Type } from "typebox";
5
5
  import { advisorRef, advisorEffortRef, loadConfig, splitRef } from "./config.js";
6
6
  import { recentConversation, textFrom } from "./conversation.js";
7
7
 
8
8
  export const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
9
+ export const DEFAULT_ADVISOR_REQUEST = "Review the current task and conversation context. Identify the highest-risk assumption, the smallest correct next step, and any important validation.";
10
+
11
+ export const resolveAdvisorRequest = (question?: string) => question?.trim() || DEFAULT_ADVISOR_REQUEST;
9
12
 
10
13
  export const ADVISOR_SYSTEM = [
11
14
  "You are the Advisor: a senior engineer consulted by an autonomous coding agent.",
@@ -30,7 +33,7 @@ export const consult = async (
30
33
  const conversation = recentConversation(ctx);
31
34
  const messages: Message[] = [{
32
35
  role: "user",
33
- content: [{ type: "text", text: `${conversation ? `<conversation>\n${conversation}\n</conversation>\n\n` : ""}Question from the Executor:\n${question}` }],
36
+ content: [{ type: "text", text: `${conversation ? `<conversation>\n${conversation}\n</conversation>\n\n` : ""}Request from the Executor:\n${question}` }],
34
37
  timestamp: Date.now(),
35
38
  }];
36
39
 
@@ -71,31 +74,34 @@ export const registerAdvisorTool = (pi: ExtensionAPI) => {
71
74
  pi.registerTool({
72
75
  name: "ask_advisor",
73
76
  label: "Ask Advisor",
74
- description: "Consult the on-demand Advisor model for strategic guidance, a second opinion, or a sanity check.",
75
- promptSnippet: "Consult the Advisor model for a second opinion",
77
+ description: "Consult the on-demand Advisor model for strategic guidance, a second opinion, a sanity check, or general contextual advice.",
78
+ promptSnippet: "Consult the Advisor model for a second opinion or general contextual advice",
76
79
  promptGuidelines: [
77
80
  "You MUST call `ask_advisor` in each of these scenarios:",
78
81
  "1. PLAN GATE: You MUST call `ask_advisor` BEFORE selecting or implementing a plan when multiple materially different approaches exist, requirements are ambiguous, or the decision has architectural, security, data-loss, compatibility, or difficult-to-reverse consequences.",
79
82
  "2. FAILURE GATE: You MUST call `ask_advisor` after two consecutive materially equivalent failed attempts, when an attempted fix recreates an earlier failure, or when two consecutive actions produce no measurable progress. Do NOT attempt another materially equivalent fix before consulting.",
80
83
  "3. COMPLETION GATE: You MUST call `ask_advisor` with the goal, changed files, key decisions, tests performed, results, and remaining risks BEFORE declaring success or calling `goal_complete`. You MAY skip this only for demonstrably trivial, low-risk work with no meaningful trade-offs or failures.",
84
+ "For a context-based review without a specific question, call `ask_advisor` with an empty object.",
81
85
  "Do NOT use `ask_advisor` for routine decisions outside these three gates.",
82
86
  ],
83
- parameters: Type.Object({ question: Type.String({ description: "The specific question or decision to get advice on." }) }),
87
+ parameters: Type.Object({ question: Type.Optional(Type.String({ description: "The specific question or decision to get advice on. Omit for a general contextual review." })) }),
84
88
  renderShell: "self",
85
89
  renderCall(args, theme, context) {
86
- const box = context.lastComponent instanceof Box ? context.lastComponent : new Box(1, 1);
87
- box.setBgFn((text) => `\x1b[48;2;25;32;45m${text}\x1b[0m`);
90
+ const box = context.lastComponent instanceof Box ? context.lastComponent : new Box(1, 1, (text) => theme.bg("customMessageBg", text));
91
+ box.setBgFn((text) => theme.bg("customMessageBg", text));
88
92
  box.clear();
89
- if (!context.state.timerId) {
90
- context.state.timerId = setInterval(() => context.invalidate(), 80);
91
- }
92
- const frame = SPINNER_FRAMES[Math.floor(Date.now() / 80) % SPINNER_FRAMES.length];
93
- box.addChild(new Text(`${theme.fg("warning", theme.bold(`◆ ADVISOR ${frame}`))} ${theme.fg("dim", `· ${args.question}`)}`, 0, 0));
93
+ const request = args.question?.trim();
94
+ const label = theme.fg("customMessageLabel", theme.bold("[advisor]"));
95
+ const title = theme.fg("customMessageText", "Executor → Advisor");
96
+ const detail = request
97
+ ? theme.fg("dim", ` ${request}`)
98
+ : theme.fg("dim", " General task review");
99
+ box.addChild(new Text(`${label} ${title}\n${detail}`, 0, 0));
94
100
  return box;
95
101
  },
96
102
  renderResult(result, { isPartial }, theme, context) {
97
- const box = context.lastComponent instanceof Box ? context.lastComponent : new Box(1, 1);
98
- box.setBgFn((text) => `\x1b[48;2;25;32;45m${text}\x1b[0m`);
103
+ const box = context.lastComponent instanceof Box ? context.lastComponent : new Box(1, 1, (text) => theme.bg("customMessageBg", text));
104
+ box.setBgFn((text) => theme.bg("customMessageBg", text));
99
105
  box.clear();
100
106
  if (isPartial) {
101
107
  if (!context.state.timerId) {
@@ -108,33 +114,36 @@ export const registerAdvisorTool = (pi: ExtensionAPI) => {
108
114
  const snippet = d.thinking.length > 200 ? d.thinking.slice(-200) : d.thinking;
109
115
  lines.push(theme.fg("thinkingText", ` 💭 ${snippet.replace(/\n/g, " ")}`));
110
116
  }
111
- if (d?.text) lines.push(theme.fg("text", d.text));
112
117
  box.addChild(new Text(lines.join("\n"), 0, 0));
118
+ if (d?.text) box.addChild(new Markdown(d.text, 0, 0, getMarkdownTheme()));
113
119
  } else {
114
120
  if (context.state.timerId) {
115
121
  clearInterval(context.state.timerId);
116
122
  delete context.state.timerId;
117
123
  }
118
- const d = result.details as { thinking?: string; text?: string } | undefined;
119
- const lines: string[] = [theme.fg("warning", theme.bold("◆ ADVISOR"))];
124
+ const d = result.details as { thinking?: string; text?: string; advisor?: string } | undefined;
125
+ const lines: string[] = [theme.fg("warning", theme.bold("◆ ADVISOR RESPONSE"))];
126
+ if (d?.advisor) lines.push(theme.fg("dim", ` ${d.advisor}`));
120
127
  if (d?.thinking) {
121
128
  lines.push(theme.fg("thinkingText", ` 💭 ${d.thinking.replace(/\n/g, " ").slice(0, 300)}${d.thinking.length > 300 ? "…" : ""}`));
122
129
  }
123
- lines.push(textFrom(result.content) || "(Advisor returned no advice.)");
130
+ const advice = d?.text || textFrom(result.content) || "(Advisor returned no advice.)";
124
131
  box.addChild(new Text(lines.join("\n"), 0, 0));
132
+ box.addChild(new Markdown(advice, 0, 0, getMarkdownTheme()));
125
133
  }
126
134
  return box;
127
135
  },
128
136
  async execute(_id, params, signal, onUpdate, ctx) {
129
- const { advice, thinkingText } = await consult(ctx, params.question, signal, (t, tx) => {
137
+ const question = resolveAdvisorRequest(params.question);
138
+ const { advice, thinkingText } = await consult(ctx, question, signal, (t, tx) => {
130
139
  onUpdate?.({
131
140
  content: [{ type: "text", text: tx }],
132
- details: { thinking: t, text: tx, advisor: advisorRef, question: params.question },
141
+ details: { thinking: t, text: tx, advisor: advisorRef, question },
133
142
  });
134
143
  });
135
144
  return {
136
145
  content: [{ type: "text", text: `Advisor (${advisorRef})\n\n${advice}` }],
137
- details: { thinking: thinkingText, text: advice, advisor: advisorRef, question: params.question },
146
+ details: { thinking: thinkingText, text: advice, advisor: advisorRef, question },
138
147
  };
139
148
  },
140
149
  });