pi-advisor-flow 0.1.3 → 0.1.4
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 +17 -0
- package/package.json +3 -1
- package/src/commands.ts +6 -2
- package/src/config.ts +29 -3
- package/src/tools.ts +10 -13
package/README.md
CHANGED
|
@@ -43,6 +43,7 @@ Once installed, the following commands are available inside the Pi terminal:
|
|
|
43
43
|
Enables the Advisor flow. Switches the primary model to the configured Executor model and registers the `ask_advisor` tool.
|
|
44
44
|
|
|
45
45
|
- _Example:_ `/advisor executor=anthropic/claude-sonnet-5 advisor=openai/gpt-5.6-sol`
|
|
46
|
+
- _Context size:_ `/advisor contextMaxChars=30000` uses up to 30,000 characters of the reconstructed conversation for each consultation. The default is 15,000; the maximum is 1,000,000. Larger values increase request cost and can exceed the advisor model's context window.
|
|
46
47
|
|
|
47
48
|
### `/advisor-models`
|
|
48
49
|
|
|
@@ -53,6 +54,22 @@ Opens an interactive, scrollable fuzzy-search picker in the TUI to choose:
|
|
|
53
54
|
|
|
54
55
|
Saves and persists your configuration to `~/.pi/agent/advisor.json`.
|
|
55
56
|
|
|
57
|
+
### `ask_advisor`
|
|
58
|
+
|
|
59
|
+
The Executor can call `ask_advisor` with an empty object for a general review of the current task and conversation, or provide `question` for targeted feedback. The Advisor is a brief second opinion: the Executor investigates and forms its own candidate direction first, then uses the Advisor to challenge assumptions and validate a consequential next step. It should not delegate the entire plan or task.
|
|
60
|
+
|
|
61
|
+
### Context configuration
|
|
62
|
+
|
|
63
|
+
The selected configuration is saved as `advisor.json` in the Pi agent directory (or an existing trusted project configuration). Set `contextMaxChars` there to increase the reconstructed conversation limit for all consultations:
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"contextMaxChars": 30000
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`contextMaxChars` must be a positive integer up to 1,000,000. Its default is 15,000.
|
|
72
|
+
|
|
56
73
|
### `/advisor-off`
|
|
57
74
|
|
|
58
75
|
Disables the Advisor flow, removing the `ask_advisor` tool from the active session.
|
package/package.json
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-advisor-flow",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+ssh://git@github.com/philipbrembeck/pi-advisor.git"
|
|
8
8
|
},
|
|
9
|
+
"homepage": "https://github.com/philipbrembeck/pi-advisor",
|
|
10
|
+
"author": "Philip Brembeck",
|
|
9
11
|
"description": "Executor/Advisor flow for autonomous pi coding agents",
|
|
10
12
|
"type": "module",
|
|
11
13
|
"main": "extensions/index.ts",
|
package/src/commands.ts
CHANGED
|
@@ -12,10 +12,14 @@ export const registerCommands = (pi: ExtensionAPI) => {
|
|
|
12
12
|
const flowEnabled = () => pi.getActiveTools().includes("ask_advisor");
|
|
13
13
|
|
|
14
14
|
pi.registerCommand("advisor", {
|
|
15
|
-
description: "Enable the Executor/Advisor flow and switch to the configured Executor model",
|
|
15
|
+
description: "Enable the Executor/Advisor flow and switch to the configured Executor model; accepts contextMaxChars=N",
|
|
16
16
|
handler: async (args, ctx) => {
|
|
17
17
|
loadConfig(ctx);
|
|
18
|
-
parseArgs(args);
|
|
18
|
+
const argumentError = parseArgs(args);
|
|
19
|
+
if (argumentError) {
|
|
20
|
+
if (ctx.hasUI) ctx.ui.notify(argumentError, "error");
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
19
23
|
const [provider, modelId] = splitRef(executorRef);
|
|
20
24
|
const executor = ctx.modelRegistry.find(provider, modelId);
|
|
21
25
|
if (!executor) return ctx.hasUI ? ctx.ui.notify(`Executor model not found: ${executorRef}`, "error") : undefined;
|
package/src/config.ts
CHANGED
|
@@ -4,16 +4,22 @@ import { CONFIG_DIR_NAME, type ExtensionContext, getAgentDir } from "@earendil-w
|
|
|
4
4
|
|
|
5
5
|
export const FALLBACK_EXECUTOR = "aikeys/claude-sonnet-5";
|
|
6
6
|
export const FALLBACK_ADVISOR = "aikeys/claude-fable-5";
|
|
7
|
+
export const DEFAULT_CONTEXT_MAX_CHARS = 15_000;
|
|
8
|
+
export const MAX_CONTEXT_MAX_CHARS = 1_000_000;
|
|
7
9
|
|
|
8
10
|
export let executorRef = FALLBACK_EXECUTOR;
|
|
9
11
|
export let advisorRef = FALLBACK_ADVISOR;
|
|
10
12
|
export let executorEffortRef: string | undefined = undefined;
|
|
11
13
|
export let advisorEffortRef: string | undefined = undefined;
|
|
14
|
+
export let contextMaxCharsRef = DEFAULT_CONTEXT_MAX_CHARS;
|
|
12
15
|
|
|
13
16
|
export const setExecutorRef = (ref: string) => { executorRef = ref; };
|
|
14
17
|
export const setAdvisorRef = (ref: string) => { advisorRef = ref; };
|
|
15
18
|
export const setExecutorEffortRef = (effort: string | undefined) => { executorEffortRef = effort; };
|
|
16
19
|
export const setAdvisorEffortRef = (effort: string | undefined) => { advisorEffortRef = effort; };
|
|
20
|
+
export const isValidContextMaxChars = (value: unknown): value is number =>
|
|
21
|
+
typeof value === "number" && Number.isSafeInteger(value) && value > 0 && value <= MAX_CONTEXT_MAX_CHARS;
|
|
22
|
+
export const setContextMaxCharsRef = (value: number) => { contextMaxCharsRef = value; };
|
|
17
23
|
|
|
18
24
|
export const splitRef = (ref: string): [string, string] => {
|
|
19
25
|
const i = ref.indexOf("/");
|
|
@@ -30,6 +36,7 @@ export const loadConfig = (ctx: ExtensionContext) => {
|
|
|
30
36
|
advisorRef = FALLBACK_ADVISOR;
|
|
31
37
|
executorEffortRef = undefined;
|
|
32
38
|
advisorEffortRef = undefined;
|
|
39
|
+
contextMaxCharsRef = DEFAULT_CONTEXT_MAX_CHARS;
|
|
33
40
|
for (const path of configPaths(ctx)) {
|
|
34
41
|
if (!path || !existsSync(path)) continue;
|
|
35
42
|
try {
|
|
@@ -38,11 +45,13 @@ export const loadConfig = (ctx: ExtensionContext) => {
|
|
|
38
45
|
advisor?: string;
|
|
39
46
|
executorEffort?: string;
|
|
40
47
|
advisorEffort?: string;
|
|
48
|
+
contextMaxChars?: number;
|
|
41
49
|
};
|
|
42
50
|
if (config.executor) executorRef = config.executor;
|
|
43
51
|
if (config.advisor) advisorRef = config.advisor;
|
|
44
52
|
if (config.executorEffort) executorEffortRef = config.executorEffort;
|
|
45
53
|
if (config.advisorEffort) advisorEffortRef = config.advisorEffort;
|
|
54
|
+
if (isValidContextMaxChars(config.contextMaxChars)) contextMaxCharsRef = config.contextMaxChars;
|
|
46
55
|
return path;
|
|
47
56
|
} catch {
|
|
48
57
|
// Ignore malformed config and keep looking for a valid fallback.
|
|
@@ -59,15 +68,32 @@ export const saveConfig = (ctx: ExtensionContext) => {
|
|
|
59
68
|
advisor: advisorRef,
|
|
60
69
|
executorEffort: executorEffortRef,
|
|
61
70
|
advisorEffort: advisorEffortRef,
|
|
71
|
+
contextMaxChars: contextMaxCharsRef,
|
|
62
72
|
};
|
|
63
73
|
writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`);
|
|
64
74
|
return path;
|
|
65
75
|
};
|
|
66
76
|
|
|
67
|
-
export const parseArgs = (args: string) => {
|
|
77
|
+
export const parseArgs = (args: string): string | undefined => {
|
|
78
|
+
let nextExecutor = executorRef;
|
|
79
|
+
let nextAdvisor = advisorRef;
|
|
80
|
+
let nextContextMaxChars = contextMaxCharsRef;
|
|
81
|
+
|
|
68
82
|
for (const token of args.trim().split(/\s+/).filter(Boolean)) {
|
|
69
83
|
const [key, value] = token.split("=");
|
|
70
|
-
if (key === "executor" && value)
|
|
71
|
-
if (key === "advisor" && value)
|
|
84
|
+
if (key === "executor" && value) nextExecutor = value;
|
|
85
|
+
if (key === "advisor" && value) nextAdvisor = value;
|
|
86
|
+
if (key === "contextMaxChars") {
|
|
87
|
+
const parsed = Number(value);
|
|
88
|
+
if (!isValidContextMaxChars(parsed)) {
|
|
89
|
+
return `contextMaxChars must be a positive integer no greater than ${MAX_CONTEXT_MAX_CHARS}.`;
|
|
90
|
+
}
|
|
91
|
+
nextContextMaxChars = parsed;
|
|
92
|
+
}
|
|
72
93
|
}
|
|
94
|
+
|
|
95
|
+
executorRef = nextExecutor;
|
|
96
|
+
advisorRef = nextAdvisor;
|
|
97
|
+
contextMaxCharsRef = nextContextMaxChars;
|
|
98
|
+
return undefined;
|
|
73
99
|
};
|
package/src/tools.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { stream, type Message, type AssistantMessage } from "@earendil-works/pi-
|
|
|
2
2
|
import { getMarkdownTheme, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { Box, Markdown, Text } from "@earendil-works/pi-tui";
|
|
4
4
|
import { Type } from "typebox";
|
|
5
|
-
import { advisorRef, advisorEffortRef, loadConfig, splitRef } from "./config.js";
|
|
5
|
+
import { advisorRef, advisorEffortRef, contextMaxCharsRef, loadConfig, splitRef } from "./config.js";
|
|
6
6
|
import { recentConversation, textFrom } from "./conversation.js";
|
|
7
7
|
|
|
8
8
|
export const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
@@ -11,9 +11,9 @@ export const DEFAULT_ADVISOR_REQUEST = "Review the current task and conversation
|
|
|
11
11
|
export const resolveAdvisorRequest = (question?: string) => question?.trim() || DEFAULT_ADVISOR_REQUEST;
|
|
12
12
|
|
|
13
13
|
export const ADVISOR_SYSTEM = [
|
|
14
|
-
"You are the Advisor: a senior engineer
|
|
15
|
-
"You do not act
|
|
16
|
-
"the smallest correct next step
|
|
14
|
+
"You are the Advisor: a senior engineer giving a brief second opinion to an autonomous coding agent.",
|
|
15
|
+
"You do not act or take over planning. Help the Executor validate its own proposed direction:",
|
|
16
|
+
"identify risks, challenge assumptions, and recommend the smallest correct next step. No preamble.",
|
|
17
17
|
].join(" ");
|
|
18
18
|
|
|
19
19
|
export const consult = async (
|
|
@@ -30,7 +30,7 @@ export const consult = async (
|
|
|
30
30
|
if (!auth.ok) throw new Error((auth as { error: string }).error);
|
|
31
31
|
if (!auth.apiKey) throw new Error(`No API key for ${advisorRef}`);
|
|
32
32
|
|
|
33
|
-
const conversation = recentConversation(ctx);
|
|
33
|
+
const conversation = recentConversation(ctx, contextMaxCharsRef);
|
|
34
34
|
const messages: Message[] = [{
|
|
35
35
|
role: "user",
|
|
36
36
|
content: [{ type: "text", text: `${conversation ? `<conversation>\n${conversation}\n</conversation>\n\n` : ""}Request from the Executor:\n${question}` }],
|
|
@@ -74,14 +74,14 @@ export const registerAdvisorTool = (pi: ExtensionAPI) => {
|
|
|
74
74
|
pi.registerTool({
|
|
75
75
|
name: "ask_advisor",
|
|
76
76
|
label: "Ask Advisor",
|
|
77
|
-
description: "Consult the on-demand Advisor model for strategic guidance
|
|
78
|
-
promptSnippet: "Consult the Advisor
|
|
77
|
+
description: "Consult the on-demand Advisor model for strategic guidance. Call with no arguments for a general review of the current task and conversation, or provide question for targeted advice.",
|
|
78
|
+
promptSnippet: "Consult the Advisor for targeted advice, or call with no arguments for a general task and conversation review",
|
|
79
79
|
promptGuidelines: [
|
|
80
80
|
"You MUST call `ask_advisor` in each of these scenarios:",
|
|
81
|
-
"1. PLAN GATE:
|
|
81
|
+
"1. PLAN GATE: Before committing to a materially consequential plan, first investigate and form your own candidate direction. Then call `ask_advisor` to stress-test the decision when multiple approaches exist, requirements are ambiguous, or the decision has architectural, security, data-loss, compatibility, or difficult-to-reverse consequences. Do not delegate the whole plan or task to the Advisor.",
|
|
82
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.",
|
|
83
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
|
-
"
|
|
84
|
+
"Call `ask_advisor` with an empty object for a general review of the current task and conversation. Provide `question` when you want the Advisor to assess a specific assumption, trade-off, or proposed next step.",
|
|
85
85
|
"Do NOT use `ask_advisor` for routine decisions outside these three gates.",
|
|
86
86
|
],
|
|
87
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." })) }),
|
|
@@ -93,10 +93,7 @@ export const registerAdvisorTool = (pi: ExtensionAPI) => {
|
|
|
93
93
|
const request = args.question?.trim();
|
|
94
94
|
const label = theme.fg("customMessageLabel", theme.bold("[advisor]"));
|
|
95
95
|
const title = theme.fg("customMessageText", "Executor → Advisor");
|
|
96
|
-
|
|
97
|
-
? theme.fg("dim", ` ${request}`)
|
|
98
|
-
: theme.fg("dim", " General task review");
|
|
99
|
-
box.addChild(new Text(`${label} ${title}\n${detail}`, 0, 0));
|
|
96
|
+
box.addChild(new Text(request ? `${label} ${title}\n${theme.fg("dim", ` ${request}`)}` : `${label} ${title}`, 0, 0));
|
|
100
97
|
return box;
|
|
101
98
|
},
|
|
102
99
|
renderResult(result, { isPartial }, theme, context) {
|