pi-advisor-flow 0.1.4 → 0.1.5
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 +19 -4
- package/package.json +1 -1
- package/src/commands.ts +54 -3
- package/src/config.ts +68 -10
- package/src/conversation.ts +1 -0
- package/src/tools.ts +45 -19
- package/src/ui.ts +143 -1
package/README.md
CHANGED
|
@@ -43,7 +43,13 @@ 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.
|
|
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
|
+
|
|
48
|
+
### `/advisor-settings`
|
|
49
|
+
|
|
50
|
+
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.
|
|
51
|
+
|
|
52
|
+
It also configures Advisor reasoning effort, whether long Advisor responses collapse to a short preview (`Ctrl+O` expands them), and each built-in invocation gate independently (consequential plans, repeated failures, and completion review). Response collapsing is off by default. You can add one custom natural-language invocation rule. Settings persist in `advisor.json`.
|
|
47
53
|
|
|
48
54
|
### `/advisor-models`
|
|
49
55
|
|
|
@@ -60,15 +66,24 @@ The Executor can call `ask_advisor` with an empty object for a general review of
|
|
|
60
66
|
|
|
61
67
|
### Context configuration
|
|
62
68
|
|
|
63
|
-
The selected configuration is saved as `advisor.json` in the Pi agent directory (or an existing trusted project configuration).
|
|
69
|
+
The selected configuration is saved as `advisor.json` in the Pi agent directory (or an existing trusted project configuration). `/advisor-models` and `/advisor-settings` share this file:
|
|
64
70
|
|
|
65
71
|
```json
|
|
66
72
|
{
|
|
67
|
-
"
|
|
73
|
+
"executor": "aikeys/claude-sonnet-5",
|
|
74
|
+
"advisor": "aikeys/claude-fable-5",
|
|
75
|
+
"executorEffort": "high",
|
|
76
|
+
"advisorEffort": "high",
|
|
77
|
+
"contextMaxChars": 25000,
|
|
78
|
+
"advisorPlanGate": true,
|
|
79
|
+
"advisorFailureGate": true,
|
|
80
|
+
"advisorCompletionGate": true,
|
|
81
|
+
"advisorCollapseResponses": false,
|
|
82
|
+
"advisorCustomInvocation": "before changing a production deployment"
|
|
68
83
|
}
|
|
69
84
|
```
|
|
70
85
|
|
|
71
|
-
`contextMaxChars` must be a
|
|
86
|
+
All fields are optional. `executor`, `advisor`, and their effort settings are managed by `/advisor-models`. `/advisor-settings` manages `advisorEffort`, `contextMaxChars`, the three gate booleans, `advisorCollapseResponses`, and `advisorCustomInvocation`. `contextMaxChars` must be a non-negative safe integer: its default is 15,000, `0` omits history, and `9007199254740991` means ALL.
|
|
72
87
|
|
|
73
88
|
### `/advisor-off`
|
|
74
89
|
|
package/package.json
CHANGED
package/src/commands.ts
CHANGED
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import {
|
|
3
|
+
advisorCollapseResponsesRef, advisorCompletionGateRef, advisorCustomInvocationRef, advisorFailureGateRef, advisorPlanGateRef,
|
|
3
4
|
advisorRef, advisorEffortRef, executorRef, executorEffortRef,
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
setAdvisorCollapseResponsesRef, setAdvisorCompletionGateRef, setAdvisorCustomInvocationRef, setAdvisorFailureGateRef, setAdvisorPlanGateRef,
|
|
6
|
+
setAdvisorRef, setAdvisorEffortRef, setContextMaxCharsRef, setExecutorRef, setExecutorEffortRef,
|
|
7
|
+
contextMaxCharsRef, loadConfig, saveConfig, parseArgs, splitRef,
|
|
6
8
|
} from "./config.js";
|
|
7
|
-
import { SearchableModelSelector } from "./ui.js";
|
|
9
|
+
import { AdvisorSettingsSelector, SearchableModelSelector, type AdvisorSettings, type ContextPreset } from "./ui.js";
|
|
8
10
|
|
|
9
11
|
const EFFORT_LEVELS = ["Default (Model Default)", "off", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
10
12
|
|
|
13
|
+
const CONTEXT_PRESETS: ContextPreset[] = [
|
|
14
|
+
{ label: "0", value: 0, description: "No conversation history. The Advisor receives only its standing instructions." },
|
|
15
|
+
{ label: "10k", value: 10_000, description: "The most recent 10,000 characters of the current branch." },
|
|
16
|
+
{ label: "25k", value: 25_000, description: "The most recent 25,000 characters of the current branch." },
|
|
17
|
+
{ label: "100k", value: 100_000, description: "The most recent 100,000 characters of the current branch." },
|
|
18
|
+
{ label: "200k", value: 200_000, description: "The most recent 200,000 characters of the current branch." },
|
|
19
|
+
{ label: "ALL", value: Number.MAX_SAFE_INTEGER, description: "The complete reconstructed conversation branch. Cost and model context limits apply." },
|
|
20
|
+
];
|
|
21
|
+
|
|
11
22
|
export const registerCommands = (pi: ExtensionAPI) => {
|
|
12
23
|
const flowEnabled = () => pi.getActiveTools().includes("ask_advisor");
|
|
13
24
|
|
|
@@ -64,6 +75,46 @@ export const registerCommands = (pi: ExtensionAPI) => {
|
|
|
64
75
|
},
|
|
65
76
|
});
|
|
66
77
|
|
|
78
|
+
pi.registerCommand("advisor-settings", {
|
|
79
|
+
description: "Configure Advisor context and reasoning effort",
|
|
80
|
+
handler: async (_args, ctx) => {
|
|
81
|
+
loadConfig(ctx);
|
|
82
|
+
if (!ctx.hasUI) return;
|
|
83
|
+
|
|
84
|
+
const initial: AdvisorSettings = {
|
|
85
|
+
contextMaxChars: contextMaxCharsRef,
|
|
86
|
+
effort: advisorEffortRef,
|
|
87
|
+
planGate: advisorPlanGateRef,
|
|
88
|
+
failureGate: advisorFailureGateRef,
|
|
89
|
+
completionGate: advisorCompletionGateRef,
|
|
90
|
+
collapseResponses: advisorCollapseResponsesRef,
|
|
91
|
+
customRule: advisorCustomInvocationRef,
|
|
92
|
+
};
|
|
93
|
+
const settings = await ctx.ui.custom<AdvisorSettings | undefined>((tui, theme, _keybindings, done) =>
|
|
94
|
+
new AdvisorSettingsSelector({
|
|
95
|
+
tui,
|
|
96
|
+
theme,
|
|
97
|
+
presets: CONTEXT_PRESETS,
|
|
98
|
+
effortLevels: EFFORT_LEVELS,
|
|
99
|
+
initial,
|
|
100
|
+
onSave: done,
|
|
101
|
+
onCancel: () => done(undefined),
|
|
102
|
+
})
|
|
103
|
+
);
|
|
104
|
+
if (!settings) return;
|
|
105
|
+
|
|
106
|
+
setAdvisorEffortRef(settings.effort === "Default (Model Default)" ? undefined : settings.effort);
|
|
107
|
+
setContextMaxCharsRef(settings.contextMaxChars);
|
|
108
|
+
setAdvisorPlanGateRef(settings.planGate);
|
|
109
|
+
setAdvisorFailureGateRef(settings.failureGate);
|
|
110
|
+
setAdvisorCompletionGateRef(settings.completionGate);
|
|
111
|
+
setAdvisorCollapseResponsesRef(settings.collapseResponses);
|
|
112
|
+
setAdvisorCustomInvocationRef(settings.customRule);
|
|
113
|
+
const path = saveConfig(ctx);
|
|
114
|
+
ctx.ui.notify(`Saved Advisor settings to ${path}`, "info");
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
|
|
67
118
|
pi.registerCommand("advisor-off", {
|
|
68
119
|
description: "Disable on-demand Advisor calls; keep the current model",
|
|
69
120
|
handler: async (_args, ctx) => {
|
package/src/config.ts
CHANGED
|
@@ -5,21 +5,32 @@ import { CONFIG_DIR_NAME, type ExtensionContext, getAgentDir } from "@earendil-w
|
|
|
5
5
|
export const FALLBACK_EXECUTOR = "aikeys/claude-sonnet-5";
|
|
6
6
|
export const FALLBACK_ADVISOR = "aikeys/claude-fable-5";
|
|
7
7
|
export const DEFAULT_CONTEXT_MAX_CHARS = 15_000;
|
|
8
|
-
|
|
8
|
+
// MAX_SAFE_INTEGER represents the complete reconstructed branch (the ALL preset).
|
|
9
|
+
export const MAX_CONTEXT_MAX_CHARS = Number.MAX_SAFE_INTEGER;
|
|
9
10
|
|
|
10
11
|
export let executorRef = FALLBACK_EXECUTOR;
|
|
11
12
|
export let advisorRef = FALLBACK_ADVISOR;
|
|
12
13
|
export let executorEffortRef: string | undefined = undefined;
|
|
13
14
|
export let advisorEffortRef: string | undefined = undefined;
|
|
14
15
|
export let contextMaxCharsRef = DEFAULT_CONTEXT_MAX_CHARS;
|
|
16
|
+
export let advisorPlanGateRef = true;
|
|
17
|
+
export let advisorFailureGateRef = true;
|
|
18
|
+
export let advisorCompletionGateRef = true;
|
|
19
|
+
export let advisorCustomInvocationRef: string | undefined = undefined;
|
|
20
|
+
export let advisorCollapseResponsesRef = false;
|
|
15
21
|
|
|
16
22
|
export const setExecutorRef = (ref: string) => { executorRef = ref; };
|
|
17
23
|
export const setAdvisorRef = (ref: string) => { advisorRef = ref; };
|
|
18
24
|
export const setExecutorEffortRef = (effort: string | undefined) => { executorEffortRef = effort; };
|
|
19
25
|
export const setAdvisorEffortRef = (effort: string | undefined) => { advisorEffortRef = effort; };
|
|
20
26
|
export const isValidContextMaxChars = (value: unknown): value is number =>
|
|
21
|
-
typeof value === "number" && Number.isSafeInteger(value) && value
|
|
27
|
+
typeof value === "number" && Number.isSafeInteger(value) && value >= 0 && value <= MAX_CONTEXT_MAX_CHARS;
|
|
22
28
|
export const setContextMaxCharsRef = (value: number) => { contextMaxCharsRef = value; };
|
|
29
|
+
export const setAdvisorPlanGateRef = (enabled: boolean) => { advisorPlanGateRef = enabled; };
|
|
30
|
+
export const setAdvisorFailureGateRef = (enabled: boolean) => { advisorFailureGateRef = enabled; };
|
|
31
|
+
export const setAdvisorCompletionGateRef = (enabled: boolean) => { advisorCompletionGateRef = enabled; };
|
|
32
|
+
export const setAdvisorCustomInvocationRef = (rule: string | undefined) => { advisorCustomInvocationRef = rule?.trim() || undefined; };
|
|
33
|
+
export const setAdvisorCollapseResponsesRef = (enabled: boolean) => { advisorCollapseResponsesRef = enabled; };
|
|
23
34
|
|
|
24
35
|
export const splitRef = (ref: string): [string, string] => {
|
|
25
36
|
const i = ref.indexOf("/");
|
|
@@ -31,27 +42,60 @@ export const configPaths = (ctx: ExtensionContext) => [
|
|
|
31
42
|
join(getAgentDir(), "advisor.json"),
|
|
32
43
|
];
|
|
33
44
|
|
|
45
|
+
type AdvisorConfig = {
|
|
46
|
+
executor?: string;
|
|
47
|
+
advisor?: string;
|
|
48
|
+
executorEffort?: string;
|
|
49
|
+
advisorEffort?: string;
|
|
50
|
+
contextMaxChars?: number;
|
|
51
|
+
advisorPlanGate?: boolean;
|
|
52
|
+
advisorFailureGate?: boolean;
|
|
53
|
+
advisorCompletionGate?: boolean;
|
|
54
|
+
advisorCustomInvocation?: string;
|
|
55
|
+
advisorCollapseResponses?: boolean;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const isValidConfig = (value: unknown): value is AdvisorConfig => {
|
|
59
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
60
|
+
const config = value as Record<string, unknown>;
|
|
61
|
+
return (config.executor === undefined || typeof config.executor === "string")
|
|
62
|
+
&& (config.advisor === undefined || typeof config.advisor === "string")
|
|
63
|
+
&& (config.executorEffort === undefined || typeof config.executorEffort === "string")
|
|
64
|
+
&& (config.advisorEffort === undefined || typeof config.advisorEffort === "string")
|
|
65
|
+
&& (config.contextMaxChars === undefined || isValidContextMaxChars(config.contextMaxChars))
|
|
66
|
+
&& (config.advisorPlanGate === undefined || typeof config.advisorPlanGate === "boolean")
|
|
67
|
+
&& (config.advisorFailureGate === undefined || typeof config.advisorFailureGate === "boolean")
|
|
68
|
+
&& (config.advisorCompletionGate === undefined || typeof config.advisorCompletionGate === "boolean")
|
|
69
|
+
&& (config.advisorCustomInvocation === undefined || typeof config.advisorCustomInvocation === "string")
|
|
70
|
+
&& (config.advisorCollapseResponses === undefined || typeof config.advisorCollapseResponses === "boolean");
|
|
71
|
+
};
|
|
72
|
+
|
|
34
73
|
export const loadConfig = (ctx: ExtensionContext) => {
|
|
35
74
|
executorRef = FALLBACK_EXECUTOR;
|
|
36
75
|
advisorRef = FALLBACK_ADVISOR;
|
|
37
76
|
executorEffortRef = undefined;
|
|
38
77
|
advisorEffortRef = undefined;
|
|
39
78
|
contextMaxCharsRef = DEFAULT_CONTEXT_MAX_CHARS;
|
|
79
|
+
advisorPlanGateRef = true;
|
|
80
|
+
advisorFailureGateRef = true;
|
|
81
|
+
advisorCompletionGateRef = true;
|
|
82
|
+
advisorCustomInvocationRef = undefined;
|
|
83
|
+
advisorCollapseResponsesRef = false;
|
|
40
84
|
for (const path of configPaths(ctx)) {
|
|
41
85
|
if (!path || !existsSync(path)) continue;
|
|
42
86
|
try {
|
|
43
|
-
const config = JSON.parse(readFileSync(path, "utf8"))
|
|
44
|
-
|
|
45
|
-
advisor?: string;
|
|
46
|
-
executorEffort?: string;
|
|
47
|
-
advisorEffort?: string;
|
|
48
|
-
contextMaxChars?: number;
|
|
49
|
-
};
|
|
87
|
+
const config = JSON.parse(readFileSync(path, "utf8"));
|
|
88
|
+
if (!isValidConfig(config)) throw new TypeError("Invalid advisor configuration");
|
|
50
89
|
if (config.executor) executorRef = config.executor;
|
|
51
90
|
if (config.advisor) advisorRef = config.advisor;
|
|
52
91
|
if (config.executorEffort) executorEffortRef = config.executorEffort;
|
|
53
92
|
if (config.advisorEffort) advisorEffortRef = config.advisorEffort;
|
|
54
93
|
if (isValidContextMaxChars(config.contextMaxChars)) contextMaxCharsRef = config.contextMaxChars;
|
|
94
|
+
if (typeof config.advisorPlanGate === "boolean") advisorPlanGateRef = config.advisorPlanGate;
|
|
95
|
+
if (typeof config.advisorFailureGate === "boolean") advisorFailureGateRef = config.advisorFailureGate;
|
|
96
|
+
if (typeof config.advisorCompletionGate === "boolean") advisorCompletionGateRef = config.advisorCompletionGate;
|
|
97
|
+
if (typeof config.advisorCustomInvocation === "string") advisorCustomInvocationRef = config.advisorCustomInvocation || undefined;
|
|
98
|
+
if (typeof config.advisorCollapseResponses === "boolean") advisorCollapseResponsesRef = config.advisorCollapseResponses;
|
|
55
99
|
return path;
|
|
56
100
|
} catch {
|
|
57
101
|
// Ignore malformed config and keep looking for a valid fallback.
|
|
@@ -63,12 +107,26 @@ export const loadConfig = (ctx: ExtensionContext) => {
|
|
|
63
107
|
export const saveConfig = (ctx: ExtensionContext) => {
|
|
64
108
|
const project = join(ctx.cwd, CONFIG_DIR_NAME, "advisor.json");
|
|
65
109
|
const path = ctx.isProjectTrusted() && existsSync(project) ? project : join(getAgentDir(), "advisor.json");
|
|
110
|
+
// Preserve settings owned by future versions or other tools that share this file.
|
|
111
|
+
let existing: Record<string, unknown> = {};
|
|
112
|
+
try {
|
|
113
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
114
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) existing = parsed as Record<string, unknown>;
|
|
115
|
+
} catch {
|
|
116
|
+
// A missing or malformed file is safely replaced with the current settings.
|
|
117
|
+
}
|
|
66
118
|
const data = {
|
|
119
|
+
...existing,
|
|
67
120
|
executor: executorRef,
|
|
68
121
|
advisor: advisorRef,
|
|
69
122
|
executorEffort: executorEffortRef,
|
|
70
123
|
advisorEffort: advisorEffortRef,
|
|
71
124
|
contextMaxChars: contextMaxCharsRef,
|
|
125
|
+
advisorPlanGate: advisorPlanGateRef,
|
|
126
|
+
advisorFailureGate: advisorFailureGateRef,
|
|
127
|
+
advisorCompletionGate: advisorCompletionGateRef,
|
|
128
|
+
advisorCustomInvocation: advisorCustomInvocationRef,
|
|
129
|
+
advisorCollapseResponses: advisorCollapseResponsesRef,
|
|
72
130
|
};
|
|
73
131
|
writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`);
|
|
74
132
|
return path;
|
|
@@ -86,7 +144,7 @@ export const parseArgs = (args: string): string | undefined => {
|
|
|
86
144
|
if (key === "contextMaxChars") {
|
|
87
145
|
const parsed = Number(value);
|
|
88
146
|
if (!isValidContextMaxChars(parsed)) {
|
|
89
|
-
return `contextMaxChars must be a
|
|
147
|
+
return `contextMaxChars must be a non-negative integer no greater than ${MAX_CONTEXT_MAX_CHARS}.`;
|
|
90
148
|
}
|
|
91
149
|
nextContextMaxChars = parsed;
|
|
92
150
|
}
|
package/src/conversation.ts
CHANGED
|
@@ -14,6 +14,7 @@ export const textFrom = (content: unknown): string => {
|
|
|
14
14
|
};
|
|
15
15
|
|
|
16
16
|
export const recentConversation = (ctx: ExtensionContext, maxChars = 15000): string => {
|
|
17
|
+
if (maxChars === 0) return "";
|
|
17
18
|
const entries: string[] = [];
|
|
18
19
|
for (const entry of ctx.sessionManager.getBranch()) {
|
|
19
20
|
if (entry.type === "message") {
|
package/src/tools.ts
CHANGED
|
@@ -2,23 +2,47 @@ 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 {
|
|
5
|
+
import {
|
|
6
|
+
advisorCollapseResponsesRef, advisorCompletionGateRef, advisorCustomInvocationRef, advisorFailureGateRef, advisorPlanGateRef,
|
|
7
|
+
advisorRef, advisorEffortRef, contextMaxCharsRef, loadConfig, splitRef,
|
|
8
|
+
} from "./config.js";
|
|
6
9
|
import { recentConversation, textFrom } from "./conversation.js";
|
|
7
10
|
|
|
8
11
|
export const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
9
|
-
export const
|
|
12
|
+
export const resolveAdvisorRequest = (question?: string) => question?.trim() || undefined;
|
|
13
|
+
export const advisorMessageText = (conversation: string, question?: string) =>
|
|
14
|
+
`${conversation ? `<conversation>\n${conversation}\n</conversation>` : ""}${question ? `\n\nTargeted focus:\n${question}` : ""}`;
|
|
10
15
|
|
|
11
|
-
|
|
16
|
+
const COLLAPSED_ADVICE_LINES = 12;
|
|
17
|
+
|
|
18
|
+
export const adviceForDisplay = (advice: string, expanded: boolean) => {
|
|
19
|
+
if (!advisorCollapseResponsesRef || expanded) return advice;
|
|
20
|
+
const lines = advice.split("\n");
|
|
21
|
+
if (lines.length <= COLLAPSED_ADVICE_LINES) return advice;
|
|
22
|
+
return `${lines.slice(0, COLLAPSED_ADVICE_LINES).join("\n")}\n\n… (${lines.length - COLLAPSED_ADVICE_LINES} more lines, Ctrl+O to expand)`;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const advisorInvocationGuidelines = () => {
|
|
26
|
+
const guidelines: string[] = [];
|
|
27
|
+
if (advisorPlanGateRef) guidelines.push("Before committing to a materially consequential plan, use ask_advisor after investigating and forming your own candidate direction. Use it to stress-test consequential architectural, security, data-loss, compatibility, or difficult-to-reverse decisions. Do not delegate the entire plan or task.");
|
|
28
|
+
if (advisorFailureGateRef) guidelines.push("Use ask_advisor after two consecutive materially equivalent failed attempts, when a fix recreates an earlier failure, or after two actions produce no measurable progress. Do not make another materially equivalent attempt before consulting.");
|
|
29
|
+
if (advisorCompletionGateRef) guidelines.push("Before declaring success, use ask_advisor to review the goal, changed files, key decisions, tests, results, and remaining risks. Skip this only for demonstrably trivial, low-risk work.");
|
|
30
|
+
if (advisorCustomInvocationRef) guidelines.push(`Also use ask_advisor when: ${advisorCustomInvocationRef}`);
|
|
31
|
+
if (guidelines.length > 0) guidelines.push("Call ask_advisor with an empty object by default. Do not invent a question merely to request a review: the Advisor already receives context. Include question only for a genuinely specific assumption or trade-off.");
|
|
32
|
+
return guidelines;
|
|
33
|
+
};
|
|
12
34
|
|
|
13
35
|
export const ADVISOR_SYSTEM = [
|
|
14
36
|
"You are the Advisor: a senior engineer giving a brief second opinion to an autonomous coding agent.",
|
|
15
|
-
"You
|
|
16
|
-
"
|
|
37
|
+
"You already have the relevant reconstructed conversation context. No question or other input from the Executor is needed for a general review.",
|
|
38
|
+
"When no targeted focus is supplied, proactively review the task, risks, proposed direction, and validation from the context. Do not ask the Executor for a question, clarification, more input, or confirmation.",
|
|
39
|
+
"The context may be truncated, so state any material uncertainty and make the best recommendation you can from what is present.",
|
|
40
|
+
"You do not act or take over planning. Identify risks, challenge assumptions, and recommend the smallest correct next step. No preamble.",
|
|
17
41
|
].join(" ");
|
|
18
42
|
|
|
19
43
|
export const consult = async (
|
|
20
44
|
ctx: ExtensionContext,
|
|
21
|
-
question
|
|
45
|
+
question?: string,
|
|
22
46
|
signal?: AbortSignal,
|
|
23
47
|
onChunk?: (thinking: string, text: string) => void,
|
|
24
48
|
) => {
|
|
@@ -33,7 +57,7 @@ export const consult = async (
|
|
|
33
57
|
const conversation = recentConversation(ctx, contextMaxCharsRef);
|
|
34
58
|
const messages: Message[] = [{
|
|
35
59
|
role: "user",
|
|
36
|
-
content: [{ type: "text", text:
|
|
60
|
+
content: [{ type: "text", text: advisorMessageText(conversation, question) }],
|
|
37
61
|
timestamp: Date.now(),
|
|
38
62
|
}];
|
|
39
63
|
|
|
@@ -71,20 +95,22 @@ export const consult = async (
|
|
|
71
95
|
};
|
|
72
96
|
|
|
73
97
|
export const registerAdvisorTool = (pi: ExtensionAPI) => {
|
|
98
|
+
pi.on("before_agent_start", (_event, ctx) => {
|
|
99
|
+
if (!pi.getActiveTools().includes("ask_advisor")) return;
|
|
100
|
+
loadConfig(ctx);
|
|
101
|
+
const guidelines = advisorInvocationGuidelines();
|
|
102
|
+
return guidelines.length > 0 ? { systemPrompt: `${ctx.getSystemPrompt()}\n\nAdvisor invocation settings:\n${guidelines.map((rule) => `- ${rule}`).join("\n")}` } : undefined;
|
|
103
|
+
});
|
|
104
|
+
|
|
74
105
|
pi.registerTool({
|
|
75
106
|
name: "ask_advisor",
|
|
76
107
|
label: "Ask Advisor",
|
|
77
|
-
description: "Consult the on-demand Advisor model for strategic guidance. Call with
|
|
78
|
-
promptSnippet: "Consult the Advisor
|
|
108
|
+
description: "Consult the on-demand Advisor model for strategic guidance. Call with an empty object for a context-aware review; add question only for a genuinely targeted focus.",
|
|
109
|
+
promptSnippet: "Consult the Advisor using its existing context; omit question unless a specific focus is necessary",
|
|
79
110
|
promptGuidelines: [
|
|
80
|
-
"
|
|
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
|
-
"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
|
-
"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
|
-
"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
|
-
"Do NOT use `ask_advisor` for routine decisions outside these three gates.",
|
|
111
|
+
"Call ask_advisor with an empty object by default. Do not invent a question merely to request a review: the Advisor already receives the context. Include question only for a genuinely specific assumption or trade-off.",
|
|
86
112
|
],
|
|
87
|
-
parameters: Type.Object({ question: Type.Optional(Type.String({ description: "The specific question or decision to get advice on. Omit for
|
|
113
|
+
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." })) }),
|
|
88
114
|
renderShell: "self",
|
|
89
115
|
renderCall(args, theme, context) {
|
|
90
116
|
const box = context.lastComponent instanceof Box ? context.lastComponent : new Box(1, 1, (text) => theme.bg("customMessageBg", text));
|
|
@@ -96,7 +122,7 @@ export const registerAdvisorTool = (pi: ExtensionAPI) => {
|
|
|
96
122
|
box.addChild(new Text(request ? `${label} ${title}\n${theme.fg("dim", ` ${request}`)}` : `${label} ${title}`, 0, 0));
|
|
97
123
|
return box;
|
|
98
124
|
},
|
|
99
|
-
renderResult(result, { isPartial }, theme, context) {
|
|
125
|
+
renderResult(result, { isPartial, expanded }, theme, context) {
|
|
100
126
|
const box = context.lastComponent instanceof Box ? context.lastComponent : new Box(1, 1, (text) => theme.bg("customMessageBg", text));
|
|
101
127
|
box.setBgFn((text) => theme.bg("customMessageBg", text));
|
|
102
128
|
box.clear();
|
|
@@ -112,7 +138,7 @@ export const registerAdvisorTool = (pi: ExtensionAPI) => {
|
|
|
112
138
|
lines.push(theme.fg("thinkingText", ` 💭 ${snippet.replace(/\n/g, " ")}`));
|
|
113
139
|
}
|
|
114
140
|
box.addChild(new Text(lines.join("\n"), 0, 0));
|
|
115
|
-
if (d?.text) box.addChild(new Markdown(d.text, 0, 0, getMarkdownTheme()));
|
|
141
|
+
if (d?.text) box.addChild(new Markdown(adviceForDisplay(d.text, Boolean(expanded)), 0, 0, getMarkdownTheme()));
|
|
116
142
|
} else {
|
|
117
143
|
if (context.state.timerId) {
|
|
118
144
|
clearInterval(context.state.timerId);
|
|
@@ -126,7 +152,7 @@ export const registerAdvisorTool = (pi: ExtensionAPI) => {
|
|
|
126
152
|
}
|
|
127
153
|
const advice = d?.text || textFrom(result.content) || "(Advisor returned no advice.)";
|
|
128
154
|
box.addChild(new Text(lines.join("\n"), 0, 0));
|
|
129
|
-
box.addChild(new Markdown(advice, 0, 0, getMarkdownTheme()));
|
|
155
|
+
box.addChild(new Markdown(adviceForDisplay(advice, Boolean(expanded)), 0, 0, getMarkdownTheme()));
|
|
130
156
|
}
|
|
131
157
|
return box;
|
|
132
158
|
},
|
package/src/ui.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Input, fuzzyFilter, type Component, type Focusable } from "@earendil-works/pi-tui";
|
|
1
|
+
import { Input, Key, matchesKey, fuzzyFilter, truncateToWidth, type Component, type Focusable } from "@earendil-works/pi-tui";
|
|
2
2
|
import { Box, Text } from "@earendil-works/pi-tui";
|
|
3
3
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
|
|
@@ -93,3 +93,145 @@ export class SearchableModelSelector implements Component, Focusable {
|
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
|
+
|
|
97
|
+
export type ContextPreset = { label: string; value: number; description: string };
|
|
98
|
+
|
|
99
|
+
export type AdvisorSettings = {
|
|
100
|
+
contextMaxChars: number;
|
|
101
|
+
effort?: string;
|
|
102
|
+
planGate: boolean;
|
|
103
|
+
failureGate: boolean;
|
|
104
|
+
completionGate: boolean;
|
|
105
|
+
collapseResponses: boolean;
|
|
106
|
+
customRule?: string;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export class AdvisorSettingsSelector implements Component, Focusable {
|
|
110
|
+
private selectedRow = 0;
|
|
111
|
+
private contextIndex: number;
|
|
112
|
+
private effortIndex: number;
|
|
113
|
+
private settings: AdvisorSettings;
|
|
114
|
+
private customInput = new Input();
|
|
115
|
+
private editingCustom = false;
|
|
116
|
+
private _focused = false;
|
|
117
|
+
|
|
118
|
+
public get focused() { return this._focused; }
|
|
119
|
+
public set focused(value: boolean) {
|
|
120
|
+
this._focused = value;
|
|
121
|
+
this.customInput.focused = value && this.editingCustom;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
constructor(private options: {
|
|
125
|
+
tui: any;
|
|
126
|
+
theme: Theme;
|
|
127
|
+
presets: ContextPreset[];
|
|
128
|
+
effortLevels: string[];
|
|
129
|
+
initial: AdvisorSettings;
|
|
130
|
+
onSave: (settings: AdvisorSettings) => void;
|
|
131
|
+
onCancel: () => void;
|
|
132
|
+
}) {
|
|
133
|
+
this.settings = { ...options.initial };
|
|
134
|
+
this.contextIndex = Math.max(0, options.presets.findIndex((preset) => preset.value === this.settings.contextMaxChars));
|
|
135
|
+
this.effortIndex = Math.max(0, options.effortLevels.indexOf(this.settings.effort || "Default (Model Default)"));
|
|
136
|
+
this.customInput.onSubmit = (value) => {
|
|
137
|
+
this.settings.customRule = value.trim() || undefined;
|
|
138
|
+
this.editingCustom = false;
|
|
139
|
+
this.customInput.focused = false;
|
|
140
|
+
this.options.tui.requestRender();
|
|
141
|
+
};
|
|
142
|
+
this.customInput.onEscape = () => {
|
|
143
|
+
this.editingCustom = false;
|
|
144
|
+
this.customInput.focused = false;
|
|
145
|
+
this.options.tui.requestRender();
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
invalidate(): void { this.options.tui.requestRender(); }
|
|
150
|
+
|
|
151
|
+
private currentContext() { return this.options.presets[this.contextIndex]; }
|
|
152
|
+
private row(label: string, value: string, index: number) {
|
|
153
|
+
const { theme } = this.options;
|
|
154
|
+
const prefix = index === this.selectedRow ? theme.fg("accent", "›") : " ";
|
|
155
|
+
const text = `${prefix} ${label.padEnd(28)} ${value}`;
|
|
156
|
+
return index === this.selectedRow ? theme.fg("accent", theme.bold(text)) : theme.fg("text", text);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
render(width: number): string[] {
|
|
160
|
+
const { theme, presets } = this.options;
|
|
161
|
+
const trackWidth = Math.max(24, Math.min(60, width - 4));
|
|
162
|
+
const positions = presets.map((_, index) => Math.round(index * (trackWidth - 1) / (presets.length - 1)));
|
|
163
|
+
const track = Array.from({ length: trackWidth }, () => "─");
|
|
164
|
+
track[positions[this.contextIndex]] = "▲";
|
|
165
|
+
const labels = Array.from({ length: trackWidth }, () => " ");
|
|
166
|
+
for (let index = 0; index < presets.length; index++) {
|
|
167
|
+
const label = presets[index].label;
|
|
168
|
+
const start = Math.max(0, Math.min(trackWidth - label.length, positions[index] - Math.floor(label.length / 2)));
|
|
169
|
+
for (let char = 0; char < label.length; char++) labels[start + char] = label[char];
|
|
170
|
+
}
|
|
171
|
+
const heading = `Recent history${" ".repeat(Math.max(1, trackWidth - "Recent history".length - "Full branch".length))}Full branch`;
|
|
172
|
+
const onOff = (value: boolean) => value ? "On" : "Off";
|
|
173
|
+
const rows = [
|
|
174
|
+
this.row("Context window", this.currentContext().label, 0),
|
|
175
|
+
this.row("Advisor reasoning", this.options.effortLevels[this.effortIndex], 1),
|
|
176
|
+
this.row("Plan gate", onOff(this.settings.planGate), 2),
|
|
177
|
+
this.row("Failure gate", onOff(this.settings.failureGate), 3),
|
|
178
|
+
this.row("Completion gate", onOff(this.settings.completionGate), 4),
|
|
179
|
+
this.row("Collapse long responses", onOff(this.settings.collapseResponses), 5),
|
|
180
|
+
this.row("Custom invocation", this.settings.customRule || "None", 6),
|
|
181
|
+
];
|
|
182
|
+
if (this.editingCustom) rows.push(` ${this.customInput.render(Math.max(10, width - 6))[0] || ""}`);
|
|
183
|
+
rows.push(this.row("Save changes", "", 7));
|
|
184
|
+
return [
|
|
185
|
+
theme.fg("accent", theme.bold(" Advisor settings")),
|
|
186
|
+
"",
|
|
187
|
+
` ${theme.fg("muted", heading)}`,
|
|
188
|
+
` ${theme.fg("muted", track.join(""))}`,
|
|
189
|
+
` ${theme.fg("text", labels.join(""))}`,
|
|
190
|
+
"",
|
|
191
|
+
...rows.map((line) => ` ${line}`),
|
|
192
|
+
"",
|
|
193
|
+
` ${theme.fg("muted", "↑/↓ select · ←/→ adjust · Enter edits or saves · Esc cancels")}`,
|
|
194
|
+
].map((line) => truncateToWidth(line, width));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
handleInput(keyData: string): void {
|
|
198
|
+
const { tui } = this.options;
|
|
199
|
+
if (this.editingCustom) {
|
|
200
|
+
this.customInput.handleInput(keyData);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (matchesKey(keyData, Key.up)) {
|
|
204
|
+
this.selectedRow = Math.max(0, this.selectedRow - 1);
|
|
205
|
+
} else if (matchesKey(keyData, Key.down)) {
|
|
206
|
+
this.selectedRow = Math.min(7, this.selectedRow + 1);
|
|
207
|
+
} else if (matchesKey(keyData, Key.left)) {
|
|
208
|
+
this.adjust(-1);
|
|
209
|
+
} else if (matchesKey(keyData, Key.right)) {
|
|
210
|
+
this.adjust(1);
|
|
211
|
+
} else if (matchesKey(keyData, Key.enter)) {
|
|
212
|
+
if (this.selectedRow === 6) {
|
|
213
|
+
this.editingCustom = true;
|
|
214
|
+
this.customInput.setValue(this.settings.customRule || "");
|
|
215
|
+
this.customInput.focused = this.focused;
|
|
216
|
+
tui.requestRender();
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (this.selectedRow === 7) return this.options.onSave({ ...this.settings, contextMaxChars: this.currentContext().value, effort: this.options.effortLevels[this.effortIndex] });
|
|
220
|
+
this.adjust(1);
|
|
221
|
+
} else if (matchesKey(keyData, Key.escape)) {
|
|
222
|
+
return this.options.onCancel();
|
|
223
|
+
} else return;
|
|
224
|
+
tui.requestRender();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private adjust(direction: number) {
|
|
228
|
+
switch (this.selectedRow) {
|
|
229
|
+
case 0: this.contextIndex = Math.max(0, Math.min(this.options.presets.length - 1, this.contextIndex + direction)); break;
|
|
230
|
+
case 1: this.effortIndex = Math.max(0, Math.min(this.options.effortLevels.length - 1, this.effortIndex + direction)); break;
|
|
231
|
+
case 2: this.settings.planGate = !this.settings.planGate; break;
|
|
232
|
+
case 3: this.settings.failureGate = !this.settings.failureGate; break;
|
|
233
|
+
case 4: this.settings.completionGate = !this.settings.completionGate; break;
|
|
234
|
+
case 5: this.settings.collapseResponses = !this.settings.collapseResponses; break;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|