pi-advisor-flow 0.1.5 → 0.1.7
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 +12 -1
- package/package.json +2 -2
- package/src/commands.ts +68 -2
- package/src/herdr.ts +87 -0
- package/src/tools.ts +25 -18
package/README.md
CHANGED
|
@@ -45,6 +45,10 @@ Enables the Advisor flow. Switches the primary model to the configured Executor
|
|
|
45
45
|
- _Example:_ `/advisor executor=anthropic/claude-sonnet-5 advisor=openai/gpt-5.6-sol`
|
|
46
46
|
- _Context size:_ `/advisor contextMaxChars=30000` uses up to 30,000 characters of the reconstructed conversation for each consultation. `0` disables history; `Number.MAX_SAFE_INTEGER` represents the complete branch. Larger values increase request cost and can exceed the Advisor model's context window.
|
|
47
47
|
|
|
48
|
+
### `/advisor-manual [focus]`
|
|
49
|
+
|
|
50
|
+
Starts an Advisor consultation in parallel without interrupting the Executor's active tool work. An optional `focus` is passed to the Advisor; when it completes, the advice is delivered to the Executor before its next model call. This works while the Executor is mid-turn.
|
|
51
|
+
|
|
48
52
|
### `/advisor-settings`
|
|
49
53
|
|
|
50
54
|
Opens a single keyboard-navigable settings screen. It includes a Claude Code-style context slider with `0`, `10k`, `25k`, `100k`, `200k`, and `ALL`; `0` sends no reconstructed history and `ALL` sends the complete current branch, subject to the Advisor model's context limit.
|
|
@@ -89,6 +93,13 @@ All fields are optional. `executor`, `advisor`, and their effort settings are ma
|
|
|
89
93
|
|
|
90
94
|
Disables the Advisor flow, removing the `ask_advisor` tool from the active session.
|
|
91
95
|
|
|
96
|
+
## Publishing releases
|
|
97
|
+
|
|
98
|
+
Pushing a version tag (`vX.Y.Z`) runs the release workflow. It verifies that the tag matches `package.json`, type-checks, tests, then publishes:
|
|
99
|
+
|
|
100
|
+
- `pi-advisor-flow` to [npm](https://www.npmjs.com/package/pi-advisor-flow)
|
|
101
|
+
- `@philipbrembeck/pi-advisor-flow` to GitHub Packages, which makes the package appear in this repository’s **Packages** sidebar
|
|
102
|
+
|
|
92
103
|
## Local Development
|
|
93
104
|
|
|
94
105
|
`pi-advisor` uses Bun for rapid testing and TypeScript. Standard commands apply:
|
|
@@ -112,5 +123,5 @@ Verify code-splitting correctness and registration logic:
|
|
|
112
123
|
|
|
113
124
|
```bash
|
|
114
125
|
bun test # Run unit tests
|
|
115
|
-
|
|
126
|
+
bun run typecheck # Perform strict TS checks
|
|
116
127
|
```
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-advisor-flow",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
|
-
"url": "
|
|
7
|
+
"url": "https://github.com/philipbrembeck/pi-advisor.git"
|
|
8
8
|
},
|
|
9
9
|
"homepage": "https://github.com/philipbrembeck/pi-advisor",
|
|
10
10
|
"author": "Philip Brembeck",
|
package/src/commands.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { getMarkdownTheme, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Box, Markdown, Text } from "@earendil-works/pi-tui";
|
|
2
3
|
import {
|
|
3
4
|
advisorCollapseResponsesRef, advisorCompletionGateRef, advisorCustomInvocationRef, advisorFailureGateRef, advisorPlanGateRef,
|
|
4
5
|
advisorRef, advisorEffortRef, executorRef, executorEffortRef,
|
|
@@ -7,6 +8,8 @@ import {
|
|
|
7
8
|
contextMaxCharsRef, loadConfig, saveConfig, parseArgs, splitRef,
|
|
8
9
|
} from "./config.js";
|
|
9
10
|
import { AdvisorSettingsSelector, SearchableModelSelector, type AdvisorSettings, type ContextPreset } from "./ui.js";
|
|
11
|
+
import { herdrAdvisorActivity } from "./herdr.js";
|
|
12
|
+
import { adviceForDisplay, consult, renderAdvisorCallBox, resolveAdvisorRequest } from "./tools.js";
|
|
10
13
|
|
|
11
14
|
const EFFORT_LEVELS = ["Default (Model Default)", "off", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
12
15
|
|
|
@@ -19,8 +22,71 @@ const CONTEXT_PRESETS: ContextPreset[] = [
|
|
|
19
22
|
{ label: "ALL", value: Number.MAX_SAFE_INTEGER, description: "The complete reconstructed conversation branch. Cost and model context limits apply." },
|
|
20
23
|
];
|
|
21
24
|
|
|
22
|
-
export const registerCommands = (pi: ExtensionAPI) => {
|
|
25
|
+
export const registerCommands = (pi: ExtensionAPI, dependencies: { consult?: typeof consult } = {}) => {
|
|
23
26
|
const flowEnabled = () => pi.getActiveTools().includes("ask_advisor");
|
|
27
|
+
const requestAdvisor = dependencies.consult ?? consult;
|
|
28
|
+
const manualConsultations = new Set<AbortController>();
|
|
29
|
+
|
|
30
|
+
pi.registerEntryRenderer?.("advisor-manual-call", (entry, _options, theme) => {
|
|
31
|
+
const { question } = (entry.data ?? {}) as { question?: string };
|
|
32
|
+
return renderAdvisorCallBox(question, theme);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
pi.registerMessageRenderer?.("advisor-manual-result", (message, { expanded }, theme) => {
|
|
36
|
+
const details = message.details as { advisor?: string; text?: string } | undefined;
|
|
37
|
+
const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
|
|
38
|
+
box.addChild(new Text(theme.fg("warning", theme.bold("◆ ADVISOR RESPONSE")), 0, 0));
|
|
39
|
+
if (details?.advisor) box.addChild(new Text(theme.fg("dim", ` ${details.advisor}`), 0, 0));
|
|
40
|
+
const advice = details?.text ?? (typeof message.content === "string" ? message.content : "(Advisor returned no advice.)");
|
|
41
|
+
box.addChild(new Markdown(adviceForDisplay(advice, expanded), 0, 0, getMarkdownTheme()));
|
|
42
|
+
return box;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
pi.on("session_shutdown", () => {
|
|
46
|
+
for (const controller of manualConsultations) controller.abort();
|
|
47
|
+
manualConsultations.clear();
|
|
48
|
+
herdrAdvisorActivity.clear();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
pi.registerCommand("advisor-manual", {
|
|
52
|
+
description: "Consult the Advisor in parallel; accepts an optional focused question and fans its response out to the Executor",
|
|
53
|
+
handler: async (args, ctx) => {
|
|
54
|
+
const question = resolveAdvisorRequest(args);
|
|
55
|
+
// A single visible progress surface avoids competing consultations overwriting
|
|
56
|
+
// each other's streamed state. A newer manual request replaces the previous one.
|
|
57
|
+
for (const pending of manualConsultations) pending.abort();
|
|
58
|
+
manualConsultations.clear();
|
|
59
|
+
const controller = new AbortController();
|
|
60
|
+
manualConsultations.add(controller);
|
|
61
|
+
pi.appendEntry?.("advisor-manual-call", { question });
|
|
62
|
+
|
|
63
|
+
herdrAdvisorActivity.start();
|
|
64
|
+
void requestAdvisor(ctx, question, controller.signal)
|
|
65
|
+
.then(({ advice }) => {
|
|
66
|
+
if (controller.signal.aborted) return;
|
|
67
|
+
pi.sendMessage({
|
|
68
|
+
customType: "advisor-manual-result",
|
|
69
|
+
content: `Manual Advisor consultation${question ? ` (${question})` : ""}:\n\n${advice}`,
|
|
70
|
+
display: true,
|
|
71
|
+
details: { advisor: advisorRef, question, text: advice },
|
|
72
|
+
}, {
|
|
73
|
+
// Steer lets the current turn finish its active work; the Executor sees
|
|
74
|
+
// the result before its next model call rather than being interrupted.
|
|
75
|
+
deliverAs: "steer",
|
|
76
|
+
triggerTurn: true,
|
|
77
|
+
});
|
|
78
|
+
})
|
|
79
|
+
.catch((error: unknown) => {
|
|
80
|
+
if (controller.signal.aborted) return;
|
|
81
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
82
|
+
if (ctx.hasUI) ctx.ui.notify(`Advisor consultation failed: ${message}`, "error");
|
|
83
|
+
})
|
|
84
|
+
.finally(() => {
|
|
85
|
+
manualConsultations.delete(controller);
|
|
86
|
+
herdrAdvisorActivity.finish();
|
|
87
|
+
});
|
|
88
|
+
},
|
|
89
|
+
});
|
|
24
90
|
|
|
25
91
|
pi.registerCommand("advisor", {
|
|
26
92
|
description: "Enable the Executor/Advisor flow and switch to the configured Executor model; accepts contextMaxChars=N",
|
package/src/herdr.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import net from "node:net";
|
|
2
|
+
|
|
3
|
+
const SOURCE = "pi-advisor:advisor-activity";
|
|
4
|
+
const HERDR_PI_SOURCE = "herdr:pi";
|
|
5
|
+
let sequence = Date.now() * 1_000;
|
|
6
|
+
|
|
7
|
+
const nextSequence = () => ++sequence;
|
|
8
|
+
|
|
9
|
+
type HerdrRequest = {
|
|
10
|
+
id: string;
|
|
11
|
+
method: "pane.report_metadata";
|
|
12
|
+
params: {
|
|
13
|
+
pane_id: string;
|
|
14
|
+
source: string;
|
|
15
|
+
agent: "pi";
|
|
16
|
+
applies_to_source: string;
|
|
17
|
+
state_labels?: { working: string };
|
|
18
|
+
clear_state_labels?: true;
|
|
19
|
+
seq: number;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type Report = (request: HerdrRequest) => void;
|
|
24
|
+
|
|
25
|
+
const sendToHerdr: Report = (request) => {
|
|
26
|
+
if (process.env.HERDR_ENV !== "1") return;
|
|
27
|
+
const paneId = process.env.HERDR_PANE_ID;
|
|
28
|
+
const socketPath = process.env.HERDR_SOCKET_PATH;
|
|
29
|
+
if (!paneId || !socketPath) return;
|
|
30
|
+
|
|
31
|
+
const endpoint = process.platform === "win32" ? `\\\\.\\pipe\\${socketPath}` : socketPath;
|
|
32
|
+
const socket = net.createConnection(endpoint);
|
|
33
|
+
const timeout = setTimeout(() => socket.destroy(), 500);
|
|
34
|
+
timeout.unref?.();
|
|
35
|
+
socket.once("connect", () => socket.write(`${JSON.stringify(request)}\n`));
|
|
36
|
+
socket.once("data", () => socket.destroy());
|
|
37
|
+
socket.once("error", () => socket.destroy());
|
|
38
|
+
socket.once("close", () => clearTimeout(timeout));
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export class HerdrAdvisorActivity {
|
|
42
|
+
#activeConsultations = 0;
|
|
43
|
+
|
|
44
|
+
constructor(private readonly report: Report = sendToHerdr) {}
|
|
45
|
+
|
|
46
|
+
start() {
|
|
47
|
+
this.#activeConsultations += 1;
|
|
48
|
+
if (this.#activeConsultations === 1) this.safeReport(false);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
finish() {
|
|
52
|
+
if (this.#activeConsultations === 0) return;
|
|
53
|
+
this.#activeConsultations -= 1;
|
|
54
|
+
if (this.#activeConsultations === 0) this.safeReport(true);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
clear() {
|
|
58
|
+
if (this.#activeConsultations === 0) return;
|
|
59
|
+
this.#activeConsultations = 0;
|
|
60
|
+
this.safeReport(true);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private safeReport(clear: boolean) {
|
|
64
|
+
try {
|
|
65
|
+
this.report(this.request(clear));
|
|
66
|
+
} catch {
|
|
67
|
+
// Herdr is optional; an unavailable integration must never affect advice.
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private request(clear: boolean): HerdrRequest {
|
|
72
|
+
return {
|
|
73
|
+
id: `${SOURCE}:${nextSequence()}`,
|
|
74
|
+
method: "pane.report_metadata",
|
|
75
|
+
params: {
|
|
76
|
+
pane_id: process.env.HERDR_PANE_ID ?? "",
|
|
77
|
+
source: SOURCE,
|
|
78
|
+
agent: "pi",
|
|
79
|
+
applies_to_source: HERDR_PI_SOURCE,
|
|
80
|
+
...(clear ? { clear_state_labels: true } : { state_labels: { working: "seeking advice" } }),
|
|
81
|
+
seq: nextSequence(),
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export const herdrAdvisorActivity = new HerdrAdvisorActivity();
|
package/src/tools.ts
CHANGED
|
@@ -7,12 +7,21 @@ import {
|
|
|
7
7
|
advisorRef, advisorEffortRef, contextMaxCharsRef, loadConfig, splitRef,
|
|
8
8
|
} from "./config.js";
|
|
9
9
|
import { recentConversation, textFrom } from "./conversation.js";
|
|
10
|
+
import { herdrAdvisorActivity } from "./herdr.js";
|
|
10
11
|
|
|
11
12
|
export const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
12
13
|
export const resolveAdvisorRequest = (question?: string) => question?.trim() || undefined;
|
|
13
14
|
export const advisorMessageText = (conversation: string, question?: string) =>
|
|
14
15
|
`${conversation ? `<conversation>\n${conversation}\n</conversation>` : ""}${question ? `\n\nTargeted focus:\n${question}` : ""}`;
|
|
15
16
|
|
|
17
|
+
export const renderAdvisorCallBox = (question: string | undefined, theme: any) => {
|
|
18
|
+
const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
|
|
19
|
+
const label = theme.fg("customMessageLabel", theme.bold("[advisor]"));
|
|
20
|
+
const title = theme.fg("customMessageText", "Executor → Advisor");
|
|
21
|
+
box.addChild(new Text(question ? `${label} ${title}\n${theme.fg("dim", ` ${question}`)}` : `${label} ${title}`, 0, 0));
|
|
22
|
+
return box;
|
|
23
|
+
};
|
|
24
|
+
|
|
16
25
|
const COLLAPSED_ADVICE_LINES = 12;
|
|
17
26
|
|
|
18
27
|
export const adviceForDisplay = (advice: string, expanded: boolean) => {
|
|
@@ -112,15 +121,8 @@ export const registerAdvisorTool = (pi: ExtensionAPI) => {
|
|
|
112
121
|
],
|
|
113
122
|
parameters: Type.Object({ question: Type.Optional(Type.String({ description: "The specific question or decision to get advice on. Omit this for normal reviews: the Advisor already has the conversation context." })) }),
|
|
114
123
|
renderShell: "self",
|
|
115
|
-
renderCall(args, theme,
|
|
116
|
-
|
|
117
|
-
box.setBgFn((text) => theme.bg("customMessageBg", text));
|
|
118
|
-
box.clear();
|
|
119
|
-
const request = args.question?.trim();
|
|
120
|
-
const label = theme.fg("customMessageLabel", theme.bold("[advisor]"));
|
|
121
|
-
const title = theme.fg("customMessageText", "Executor → Advisor");
|
|
122
|
-
box.addChild(new Text(request ? `${label} ${title}\n${theme.fg("dim", ` ${request}`)}` : `${label} ${title}`, 0, 0));
|
|
123
|
-
return box;
|
|
124
|
+
renderCall(args, theme, _context) {
|
|
125
|
+
return renderAdvisorCallBox(args.question?.trim(), theme);
|
|
124
126
|
},
|
|
125
127
|
renderResult(result, { isPartial, expanded }, theme, context) {
|
|
126
128
|
const box = context.lastComponent instanceof Box ? context.lastComponent : new Box(1, 1, (text) => theme.bg("customMessageBg", text));
|
|
@@ -158,16 +160,21 @@ export const registerAdvisorTool = (pi: ExtensionAPI) => {
|
|
|
158
160
|
},
|
|
159
161
|
async execute(_id, params, signal, onUpdate, ctx) {
|
|
160
162
|
const question = resolveAdvisorRequest(params.question);
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
163
|
+
herdrAdvisorActivity.start();
|
|
164
|
+
try {
|
|
165
|
+
const { advice, thinkingText } = await consult(ctx, question, signal, (t, tx) => {
|
|
166
|
+
onUpdate?.({
|
|
167
|
+
content: [{ type: "text", text: tx }],
|
|
168
|
+
details: { thinking: t, text: tx, advisor: advisorRef, question },
|
|
169
|
+
});
|
|
165
170
|
});
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}
|
|
171
|
+
return {
|
|
172
|
+
content: [{ type: "text", text: `Advisor (${advisorRef})\n\n${advice}` }],
|
|
173
|
+
details: { thinking: thinkingText, text: advice, advisor: advisorRef, question },
|
|
174
|
+
};
|
|
175
|
+
} finally {
|
|
176
|
+
herdrAdvisorActivity.finish();
|
|
177
|
+
}
|
|
171
178
|
},
|
|
172
179
|
});
|
|
173
180
|
};
|