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.
- package/README.md +4 -0
- package/package.json +8 -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.
|
|
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
|
|
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` : ""}
|
|
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,
|
|
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) =>
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const
|
|
93
|
-
|
|
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) =>
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
146
|
+
details: { thinking: thinkingText, text: advice, advisor: advisorRef, question },
|
|
138
147
|
};
|
|
139
148
|
},
|
|
140
149
|
});
|