@juicesharp/rpiv-advisor 0.1.2 → 0.6.1
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/LICENSE +21 -0
- package/README.md +1 -1
- package/advisor-ui.ts +112 -0
- package/advisor.ts +97 -243
- package/index.ts +10 -10
- package/package.json +22 -6
- package/prompts/advisor-system.txt +8 -0
- package/docs/advisor.jpg +0 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 juicesharp
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ implementing the advisor-strategy pattern: the executor model can escalate
|
|
|
5
5
|
decisions to a stronger reviewer model (e.g. Opus), receive guidance, and
|
|
6
6
|
resume.
|
|
7
7
|
|
|
8
|
-

|
|
9
9
|
|
|
10
10
|
## Installation
|
|
11
11
|
|
package/advisor-ui.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* advisor-ui — bordered select-panel builders for the /advisor command.
|
|
3
|
+
*
|
|
4
|
+
* Two public functions (showAdvisorPicker, showEffortPicker) share a private
|
|
5
|
+
* buildSelectPanel helper that owns the bordered-container layout and the
|
|
6
|
+
* SelectList theme wiring.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ThinkingLevel } from "@mariozechner/pi-ai";
|
|
10
|
+
import { DynamicBorder, type ExtensionContext, type Theme } from "@mariozechner/pi-coding-agent";
|
|
11
|
+
import { Container, type SelectItem, SelectList, Spacer, Text } from "@mariozechner/pi-tui";
|
|
12
|
+
|
|
13
|
+
const MAX_VISIBLE_ROWS = 10;
|
|
14
|
+
const NAV_HINT = "↑↓ navigate • enter select • esc cancel";
|
|
15
|
+
|
|
16
|
+
const ADVISOR_HEADER_TITLE = "Advisor Tool";
|
|
17
|
+
const ADVISOR_HEADER_PROSE_1 =
|
|
18
|
+
"When the active model needs stronger judgment — a complex decision, an ambiguous " +
|
|
19
|
+
"failure, a problem it's circling without progress — it escalates to the " +
|
|
20
|
+
"advisor model for guidance, then resumes. The advisor runs server-side " +
|
|
21
|
+
"and uses additional tokens.";
|
|
22
|
+
const ADVISOR_HEADER_PROSE_2 =
|
|
23
|
+
"For certain workloads, pairing a faster model as the main model with a " +
|
|
24
|
+
"more capable one as the advisor gives near-top-tier performance with " +
|
|
25
|
+
"reduced token usage.";
|
|
26
|
+
|
|
27
|
+
const EFFORT_HEADER_TITLE = "Reasoning Level";
|
|
28
|
+
const EFFORT_HEADER_PROSE =
|
|
29
|
+
"Choose the reasoning effort level for the advisor. " +
|
|
30
|
+
"Higher levels produce stronger judgment but use more tokens.";
|
|
31
|
+
|
|
32
|
+
function selectListTheme(theme: Theme) {
|
|
33
|
+
return {
|
|
34
|
+
selectedPrefix: (t: string) => theme.bg("selectedBg", theme.fg("accent", t)),
|
|
35
|
+
selectedText: (t: string) => theme.bg("selectedBg", theme.bold(t)),
|
|
36
|
+
description: (t: string) => theme.fg("muted", t),
|
|
37
|
+
scrollInfo: (t: string) => theme.fg("dim", t),
|
|
38
|
+
noMatch: (t: string) => theme.fg("warning", t),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function buildSelectPanel(theme: Theme, title: string, proseLines: string[], selectList: SelectList): Container {
|
|
43
|
+
const container = new Container();
|
|
44
|
+
const border = () => new DynamicBorder((s: string) => theme.fg("accent", s));
|
|
45
|
+
|
|
46
|
+
container.addChild(border());
|
|
47
|
+
container.addChild(new Spacer(1));
|
|
48
|
+
container.addChild(new Text(theme.fg("accent", theme.bold(title)), 1, 0));
|
|
49
|
+
container.addChild(new Spacer(1));
|
|
50
|
+
for (const line of proseLines) {
|
|
51
|
+
container.addChild(new Text(line, 1, 0));
|
|
52
|
+
container.addChild(new Spacer(1));
|
|
53
|
+
}
|
|
54
|
+
container.addChild(selectList);
|
|
55
|
+
container.addChild(new Spacer(1));
|
|
56
|
+
container.addChild(new Text(theme.fg("dim", NAV_HINT), 1, 0));
|
|
57
|
+
container.addChild(new Spacer(1));
|
|
58
|
+
container.addChild(border());
|
|
59
|
+
return container;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function showAdvisorPicker(ctx: ExtensionContext, items: SelectItem[]): Promise<string | null> {
|
|
63
|
+
return ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
|
|
64
|
+
const selectList = new SelectList(items, Math.min(items.length, MAX_VISIBLE_ROWS), selectListTheme(theme));
|
|
65
|
+
selectList.onSelect = (item) => done(item.value);
|
|
66
|
+
selectList.onCancel = () => done(null);
|
|
67
|
+
|
|
68
|
+
const container = buildSelectPanel(
|
|
69
|
+
theme,
|
|
70
|
+
ADVISOR_HEADER_TITLE,
|
|
71
|
+
[ADVISOR_HEADER_PROSE_1, ADVISOR_HEADER_PROSE_2],
|
|
72
|
+
selectList,
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
render: (w) => container.render(w),
|
|
77
|
+
invalidate: () => container.invalidate(),
|
|
78
|
+
handleInput: (data) => {
|
|
79
|
+
selectList.handleInput(data);
|
|
80
|
+
tui.requestRender();
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function showEffortPicker(
|
|
87
|
+
ctx: ExtensionContext,
|
|
88
|
+
items: SelectItem[],
|
|
89
|
+
currentEffort: ThinkingLevel | undefined,
|
|
90
|
+
defaultEffort: ThinkingLevel,
|
|
91
|
+
): Promise<string | null> {
|
|
92
|
+
return ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
|
|
93
|
+
const selectList = new SelectList(items, Math.min(items.length, MAX_VISIBLE_ROWS), selectListTheme(theme));
|
|
94
|
+
const preferredIdx = currentEffort ? items.findIndex((item) => item.value === currentEffort) : -1;
|
|
95
|
+
selectList.setSelectedIndex(
|
|
96
|
+
preferredIdx >= 0 ? preferredIdx : items.findIndex((item) => item.value === defaultEffort),
|
|
97
|
+
);
|
|
98
|
+
selectList.onSelect = (item) => done(item.value);
|
|
99
|
+
selectList.onCancel = () => done(null);
|
|
100
|
+
|
|
101
|
+
const container = buildSelectPanel(theme, EFFORT_HEADER_TITLE, [EFFORT_HEADER_PROSE], selectList);
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
render: (w) => container.render(w),
|
|
105
|
+
invalidate: () => container.invalidate(),
|
|
106
|
+
handleInput: (data) => {
|
|
107
|
+
selectList.handleInput(data);
|
|
108
|
+
tui.requestRender();
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
});
|
|
112
|
+
}
|
package/advisor.ts
CHANGED
|
@@ -14,34 +14,77 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
17
|
-
import { dirname, join } from "node:path";
|
|
18
17
|
import { homedir } from "node:os";
|
|
19
|
-
import {
|
|
18
|
+
import { dirname, join } from "node:path";
|
|
19
|
+
import { fileURLToPath } from "node:url";
|
|
20
20
|
import type { Api, Model, StopReason, Usage } from "@mariozechner/pi-ai";
|
|
21
|
+
import { completeSimple, type Message, supportsXhigh, type ThinkingLevel } from "@mariozechner/pi-ai";
|
|
21
22
|
import {
|
|
22
|
-
DynamicBorder,
|
|
23
|
-
convertToLlm,
|
|
24
|
-
serializeConversation,
|
|
25
23
|
type AgentToolResult,
|
|
26
24
|
type AgentToolUpdateCallback,
|
|
25
|
+
convertToLlm,
|
|
27
26
|
type ExtensionAPI,
|
|
28
27
|
type ExtensionContext,
|
|
29
28
|
type SessionEntry,
|
|
29
|
+
serializeConversation,
|
|
30
30
|
} from "@mariozechner/pi-coding-agent";
|
|
31
|
-
import {
|
|
32
|
-
Container,
|
|
33
|
-
SelectList,
|
|
34
|
-
Spacer,
|
|
35
|
-
Text,
|
|
36
|
-
type SelectItem,
|
|
37
|
-
} from "@mariozechner/pi-tui";
|
|
31
|
+
import type { SelectItem } from "@mariozechner/pi-tui";
|
|
38
32
|
import { Type } from "@sinclair/typebox";
|
|
33
|
+
import { showAdvisorPicker, showEffortPicker } from "./advisor-ui.js";
|
|
39
34
|
|
|
40
35
|
// ---------------------------------------------------------------------------
|
|
41
|
-
// Constants
|
|
36
|
+
// Constants — grouped by concern, flat named consts (no namespaced objects)
|
|
42
37
|
// ---------------------------------------------------------------------------
|
|
43
38
|
|
|
39
|
+
// Tool identity
|
|
44
40
|
export const ADVISOR_TOOL_NAME = "advisor";
|
|
41
|
+
const TOOL_LABEL = "Advisor";
|
|
42
|
+
|
|
43
|
+
// Persistence
|
|
44
|
+
const CONFIG_DIR = join(homedir(), ".config", "rpiv-advisor");
|
|
45
|
+
const ADVISOR_CONFIG_PATH = join(CONFIG_DIR, "advisor.json");
|
|
46
|
+
const CONFIG_FILE_MODE = 0o600;
|
|
47
|
+
|
|
48
|
+
// Selector sentinels — double-underscore form is collision-proof against real provider:id keys
|
|
49
|
+
const NO_ADVISOR_VALUE = "__no_advisor__";
|
|
50
|
+
const OFF_VALUE = "__off__";
|
|
51
|
+
|
|
52
|
+
// Effort levels
|
|
53
|
+
const BASE_EFFORT_LEVELS: ThinkingLevel[] = ["minimal", "low", "medium", "high"];
|
|
54
|
+
const XHIGH_EFFORT_LEVEL: ThinkingLevel = "xhigh";
|
|
55
|
+
const DEFAULT_EFFORT: ThinkingLevel = "high";
|
|
56
|
+
const RECOMMENDED_EFFORT_SUFFIX = " (recommended)";
|
|
57
|
+
|
|
58
|
+
// UI — labels used by command flow; panel prose/titles live in advisor-ui.ts
|
|
59
|
+
const CHECKMARK = " ✓";
|
|
60
|
+
|
|
61
|
+
// Messages (static)
|
|
62
|
+
const MSG_ADVISOR_DISABLED = "Advisor disabled";
|
|
63
|
+
const MSG_REQUIRES_INTERACTIVE = "/advisor requires interactive mode";
|
|
64
|
+
|
|
65
|
+
// Errors (static)
|
|
66
|
+
const ERR_NO_MODEL = "No advisor model is configured. The user can enable one with the /advisor command.";
|
|
67
|
+
const ERR_CALL_ABORTED = "Advisor call was cancelled before it completed.";
|
|
68
|
+
const ERR_EMPTY_RESPONSE = "Advisor returned no text content.";
|
|
69
|
+
const ERR_NO_MODEL_SELECTED = "no advisor model selected";
|
|
70
|
+
const ERR_EMPTY_RESPONSE_DETAIL = "empty response";
|
|
71
|
+
const ERR_ABORTED_DETAIL = "aborted";
|
|
72
|
+
const ERR_UNKNOWN = "unknown error";
|
|
73
|
+
|
|
74
|
+
// Errors/messages (parameterized)
|
|
75
|
+
const errMisconfigured = (label: string, err: string) => `Advisor (${label}) is misconfigured: ${err}`;
|
|
76
|
+
const errNoApiKey = (label: string) => `Advisor (${label}) has no API key available.`;
|
|
77
|
+
const errNoApiKeyDetail = (provider: string) => `no API key for ${provider}`;
|
|
78
|
+
const errCallFailed = (err: string | undefined) => `Advisor call failed: ${err ?? ERR_UNKNOWN}`;
|
|
79
|
+
const errCallThrew = (msg: string) => `Advisor call threw: ${msg}`;
|
|
80
|
+
const errSelectionNotFound = (choice: string) => `Advisor selection not found: ${choice}`;
|
|
81
|
+
const errModelUnavailable = (key: string) => `Previously configured advisor model ${key} is no longer available`;
|
|
82
|
+
const msgAdvisorEnabled = (label: string, effort: ThinkingLevel | undefined) =>
|
|
83
|
+
`Advisor: ${label}${effort ? `, ${effort}` : ""}`;
|
|
84
|
+
const msgAdvisorRestored = (label: string, effort: ThinkingLevel | undefined) =>
|
|
85
|
+
`Advisor restored: ${label}${effort ? `, ${effort}` : ""}`;
|
|
86
|
+
const msgConsulting = (label: string, effort: ThinkingLevel | undefined) =>
|
|
87
|
+
`Consulting advisor (${label}${effort ? `, ${effort}` : ""})…`;
|
|
45
88
|
|
|
46
89
|
// ---------------------------------------------------------------------------
|
|
47
90
|
// Config file persistence (cross-session)
|
|
@@ -52,8 +95,6 @@ interface AdvisorConfig {
|
|
|
52
95
|
effort?: ThinkingLevel;
|
|
53
96
|
}
|
|
54
97
|
|
|
55
|
-
const ADVISOR_CONFIG_PATH = join(homedir(), ".config", "rpiv-advisor", "advisor.json");
|
|
56
|
-
|
|
57
98
|
function loadAdvisorConfig(): AdvisorConfig {
|
|
58
99
|
if (!existsSync(ADVISOR_CONFIG_PATH)) return {};
|
|
59
100
|
try {
|
|
@@ -69,12 +110,12 @@ function saveAdvisorConfig(key: string | undefined, effort: ThinkingLevel | unde
|
|
|
69
110
|
if (effort) config.effort = effort;
|
|
70
111
|
try {
|
|
71
112
|
mkdirSync(dirname(ADVISOR_CONFIG_PATH), { recursive: true });
|
|
72
|
-
writeFileSync(ADVISOR_CONFIG_PATH, JSON.stringify(config, null, 2)
|
|
113
|
+
writeFileSync(ADVISOR_CONFIG_PATH, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
|
|
73
114
|
} catch {
|
|
74
115
|
// write may fail on disk-full or permission errors — best effort only
|
|
75
116
|
}
|
|
76
117
|
try {
|
|
77
|
-
chmodSync(ADVISOR_CONFIG_PATH,
|
|
118
|
+
chmodSync(ADVISOR_CONFIG_PATH, CONFIG_FILE_MODE);
|
|
78
119
|
} catch {
|
|
79
120
|
// chmod may fail on some filesystems — best effort only
|
|
80
121
|
}
|
|
@@ -86,26 +127,14 @@ function parseModelKey(key: string): { provider: string; modelId: string } | und
|
|
|
86
127
|
return { provider: key.slice(0, idx), modelId: key.slice(idx + 1) };
|
|
87
128
|
}
|
|
88
129
|
|
|
89
|
-
export const ADVISOR_SYSTEM_PROMPT = `You are an advisor model in an advisor-strategy pattern. An executor model is running a task end-to-end — calling tools, reading results, iterating toward a solution. When the executor hits a decision it cannot reasonably solve alone, it consults you for guidance.
|
|
90
|
-
|
|
91
|
-
You read the shared conversation context and return ONE of:
|
|
92
|
-
- a plan (concrete next steps the executor should take),
|
|
93
|
-
- a correction (the executor is going down a wrong path — redirect it),
|
|
94
|
-
- a stop signal (the executor should halt and escalate to the user).
|
|
95
|
-
|
|
96
|
-
You NEVER call tools. You NEVER produce user-facing output. Be concise, directive, and grounded in the shared context. Name files, functions, and line numbers where possible. No preamble, no apologies, no meta-commentary about being an advisor — just the guidance the executor needs.`;
|
|
97
|
-
|
|
98
130
|
// ---------------------------------------------------------------------------
|
|
99
|
-
//
|
|
131
|
+
// System prompt — loaded once at module init from prompts/advisor-system.txt
|
|
100
132
|
// ---------------------------------------------------------------------------
|
|
101
133
|
|
|
102
|
-
export
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
stopReason?: StopReason;
|
|
107
|
-
errorMessage?: string;
|
|
108
|
-
}
|
|
134
|
+
export const ADVISOR_SYSTEM_PROMPT = readFileSync(
|
|
135
|
+
fileURLToPath(new URL("./prompts/advisor-system.txt", import.meta.url)),
|
|
136
|
+
"utf-8",
|
|
137
|
+
).trimEnd();
|
|
109
138
|
|
|
110
139
|
// ---------------------------------------------------------------------------
|
|
111
140
|
// Module state — in-memory, resets each session
|
|
@@ -144,10 +173,7 @@ export function restoreAdvisorState(ctx: ExtensionContext, pi: ExtensionAPI): vo
|
|
|
144
173
|
const model = ctx.modelRegistry.find(parsed.provider, parsed.modelId);
|
|
145
174
|
if (!model) {
|
|
146
175
|
if (ctx.hasUI) {
|
|
147
|
-
ctx.ui.notify(
|
|
148
|
-
`Previously configured advisor model ${config.modelKey} is no longer available`,
|
|
149
|
-
"warning",
|
|
150
|
-
);
|
|
176
|
+
ctx.ui.notify(errModelUnavailable(config.modelKey), "warning");
|
|
151
177
|
}
|
|
152
178
|
return;
|
|
153
179
|
}
|
|
@@ -163,10 +189,7 @@ export function restoreAdvisorState(ctx: ExtensionContext, pi: ExtensionAPI): vo
|
|
|
163
189
|
}
|
|
164
190
|
|
|
165
191
|
if (ctx.hasUI) {
|
|
166
|
-
ctx.ui.notify(
|
|
167
|
-
`Advisor restored: ${model.provider}:${model.id}${config.effort ? `, ${config.effort}` : ""}`,
|
|
168
|
-
"info",
|
|
169
|
-
);
|
|
192
|
+
ctx.ui.notify(msgAdvisorRestored(`${model.provider}:${model.id}`, config.effort), "info");
|
|
170
193
|
}
|
|
171
194
|
}
|
|
172
195
|
|
|
@@ -174,6 +197,14 @@ export function restoreAdvisorState(ctx: ExtensionContext, pi: ExtensionAPI): vo
|
|
|
174
197
|
// Core execute logic — curate context, call advisor, return structured result
|
|
175
198
|
// ---------------------------------------------------------------------------
|
|
176
199
|
|
|
200
|
+
export interface AdvisorDetails {
|
|
201
|
+
advisorModel?: string;
|
|
202
|
+
effort?: ThinkingLevel;
|
|
203
|
+
usage?: Usage;
|
|
204
|
+
stopReason?: StopReason;
|
|
205
|
+
errorMessage?: string;
|
|
206
|
+
}
|
|
207
|
+
|
|
177
208
|
function buildErrorResult(
|
|
178
209
|
advisorLabel: string | undefined,
|
|
179
210
|
userText: string,
|
|
@@ -182,9 +213,7 @@ function buildErrorResult(
|
|
|
182
213
|
const effort = getAdvisorEffort();
|
|
183
214
|
return {
|
|
184
215
|
content: [{ type: "text", text: userText }],
|
|
185
|
-
details: advisorLabel
|
|
186
|
-
? { advisorModel: advisorLabel, effort, errorMessage }
|
|
187
|
-
: { effort, errorMessage },
|
|
216
|
+
details: advisorLabel ? { advisorModel: advisorLabel, effort, errorMessage } : { effort, errorMessage },
|
|
188
217
|
};
|
|
189
218
|
}
|
|
190
219
|
|
|
@@ -195,30 +224,17 @@ async function executeAdvisor(
|
|
|
195
224
|
): Promise<AgentToolResult<AdvisorDetails>> {
|
|
196
225
|
const advisor = getAdvisorModel();
|
|
197
226
|
if (!advisor) {
|
|
198
|
-
return buildErrorResult(
|
|
199
|
-
undefined,
|
|
200
|
-
"No advisor model is configured. The user can enable one with the /advisor command.",
|
|
201
|
-
"no advisor model selected",
|
|
202
|
-
);
|
|
227
|
+
return buildErrorResult(undefined, ERR_NO_MODEL, ERR_NO_MODEL_SELECTED);
|
|
203
228
|
}
|
|
204
229
|
const advisorLabel = `${advisor.provider}:${advisor.id}`;
|
|
205
230
|
const effort = getAdvisorEffort();
|
|
206
231
|
|
|
207
232
|
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(advisor);
|
|
208
233
|
if (!auth.ok) {
|
|
209
|
-
return buildErrorResult(
|
|
210
|
-
advisorLabel,
|
|
211
|
-
`Advisor (${advisorLabel}) is misconfigured: ${auth.error}`,
|
|
212
|
-
auth.error,
|
|
213
|
-
);
|
|
234
|
+
return buildErrorResult(advisorLabel, errMisconfigured(advisorLabel, auth.error), auth.error);
|
|
214
235
|
}
|
|
215
236
|
if (!auth.apiKey) {
|
|
216
|
-
|
|
217
|
-
return buildErrorResult(
|
|
218
|
-
advisorLabel,
|
|
219
|
-
`Advisor (${advisorLabel}) has no API key available.`,
|
|
220
|
-
msg,
|
|
221
|
-
);
|
|
237
|
+
return buildErrorResult(advisorLabel, errNoApiKey(advisorLabel), errNoApiKeyDetail(advisor.provider));
|
|
222
238
|
}
|
|
223
239
|
|
|
224
240
|
const branch = ctx.sessionManager.getBranch();
|
|
@@ -239,7 +255,7 @@ async function executeAdvisor(
|
|
|
239
255
|
};
|
|
240
256
|
|
|
241
257
|
onUpdate?.({
|
|
242
|
-
content: [{ type: "text", text:
|
|
258
|
+
content: [{ type: "text", text: msgConsulting(advisorLabel, effort) }],
|
|
243
259
|
details: { advisorModel: advisorLabel, effort },
|
|
244
260
|
});
|
|
245
261
|
|
|
@@ -252,27 +268,20 @@ async function executeAdvisor(
|
|
|
252
268
|
|
|
253
269
|
if (response.stopReason === "aborted") {
|
|
254
270
|
return {
|
|
255
|
-
content: [
|
|
256
|
-
{ type: "text", text: "Advisor call was cancelled before it completed." },
|
|
257
|
-
],
|
|
271
|
+
content: [{ type: "text", text: ERR_CALL_ABORTED }],
|
|
258
272
|
details: {
|
|
259
273
|
advisorModel: advisorLabel,
|
|
260
274
|
effort,
|
|
261
275
|
usage: response.usage,
|
|
262
276
|
stopReason: response.stopReason,
|
|
263
|
-
errorMessage: response.errorMessage ??
|
|
277
|
+
errorMessage: response.errorMessage ?? ERR_ABORTED_DETAIL,
|
|
264
278
|
},
|
|
265
279
|
};
|
|
266
280
|
}
|
|
267
281
|
|
|
268
282
|
if (response.stopReason === "error") {
|
|
269
283
|
return {
|
|
270
|
-
content: [
|
|
271
|
-
{
|
|
272
|
-
type: "text",
|
|
273
|
-
text: `Advisor call failed: ${response.errorMessage ?? "unknown error"}`,
|
|
274
|
-
},
|
|
275
|
-
],
|
|
284
|
+
content: [{ type: "text", text: errCallFailed(response.errorMessage) }],
|
|
276
285
|
details: {
|
|
277
286
|
advisorModel: advisorLabel,
|
|
278
287
|
effort,
|
|
@@ -291,13 +300,13 @@ async function executeAdvisor(
|
|
|
291
300
|
|
|
292
301
|
if (!advisorText) {
|
|
293
302
|
return {
|
|
294
|
-
content: [{ type: "text", text:
|
|
303
|
+
content: [{ type: "text", text: ERR_EMPTY_RESPONSE }],
|
|
295
304
|
details: {
|
|
296
305
|
advisorModel: advisorLabel,
|
|
297
306
|
effort,
|
|
298
307
|
usage: response.usage,
|
|
299
308
|
stopReason: response.stopReason,
|
|
300
|
-
errorMessage:
|
|
309
|
+
errorMessage: ERR_EMPTY_RESPONSE_DETAIL,
|
|
301
310
|
},
|
|
302
311
|
};
|
|
303
312
|
}
|
|
@@ -313,11 +322,7 @@ async function executeAdvisor(
|
|
|
313
322
|
};
|
|
314
323
|
} catch (err) {
|
|
315
324
|
const message = err instanceof Error ? err.message : String(err);
|
|
316
|
-
return buildErrorResult(
|
|
317
|
-
advisorLabel,
|
|
318
|
-
`Advisor call threw: ${message}`,
|
|
319
|
-
message,
|
|
320
|
-
);
|
|
325
|
+
return buildErrorResult(advisorLabel, errCallThrew(message), message);
|
|
321
326
|
}
|
|
322
327
|
}
|
|
323
328
|
|
|
@@ -350,7 +355,7 @@ const ADVISOR_PROMPT_GUIDELINES: string[] = [
|
|
|
350
355
|
export function registerAdvisorTool(pi: ExtensionAPI): void {
|
|
351
356
|
pi.registerTool({
|
|
352
357
|
name: ADVISOR_TOOL_NAME,
|
|
353
|
-
label:
|
|
358
|
+
label: TOOL_LABEL,
|
|
354
359
|
description: ADVISOR_DESCRIPTION,
|
|
355
360
|
promptSnippet: ADVISOR_PROMPT_SNIPPET,
|
|
356
361
|
promptGuidelines: ADVISOR_PROMPT_GUIDELINES,
|
|
@@ -381,27 +386,6 @@ export function registerAdvisorBeforeAgentStart(pi: ExtensionAPI): void {
|
|
|
381
386
|
// /advisor slash command — opens selector panel for picking the advisor model
|
|
382
387
|
// ---------------------------------------------------------------------------
|
|
383
388
|
|
|
384
|
-
const ADVISOR_HEADER_TITLE = "Advisor Tool";
|
|
385
|
-
|
|
386
|
-
const ADVISOR_HEADER_PROSE_1 =
|
|
387
|
-
"When the active model needs stronger judgment — a complex decision, an ambiguous " +
|
|
388
|
-
"failure, a problem it's circling without progress — it escalates to the " +
|
|
389
|
-
"advisor model for guidance, then resumes. The advisor runs server-side " +
|
|
390
|
-
"and uses additional tokens.";
|
|
391
|
-
|
|
392
|
-
const ADVISOR_HEADER_PROSE_2 =
|
|
393
|
-
"For certain workloads, pairing a faster model as the main model with a " +
|
|
394
|
-
"more capable one as the advisor gives near-top-tier performance with " +
|
|
395
|
-
"reduced token usage.";
|
|
396
|
-
|
|
397
|
-
const NO_ADVISOR_VALUE = "__no_advisor__";
|
|
398
|
-
|
|
399
|
-
const EFFORT_HEADER_TITLE = "Reasoning Level";
|
|
400
|
-
|
|
401
|
-
const EFFORT_HEADER_PROSE =
|
|
402
|
-
"Choose the reasoning effort level for the advisor. " +
|
|
403
|
-
"Higher levels produce stronger judgment but use more tokens.";
|
|
404
|
-
|
|
405
389
|
function modelKey(m: { provider: string; id: string }): string {
|
|
406
390
|
return `${m.provider}:${m.id}`;
|
|
407
391
|
}
|
|
@@ -411,7 +395,7 @@ export function registerAdvisorCommand(pi: ExtensionAPI): void {
|
|
|
411
395
|
description: "Configure the advisor model for the advisor-strategy pattern",
|
|
412
396
|
handler: async (_args, ctx) => {
|
|
413
397
|
if (!ctx.hasUI) {
|
|
414
|
-
ctx.ui.notify(
|
|
398
|
+
ctx.ui.notify(MSG_REQUIRES_INTERACTIVE, "error");
|
|
415
399
|
return;
|
|
416
400
|
}
|
|
417
401
|
|
|
@@ -421,74 +405,15 @@ export function registerAdvisorCommand(pi: ExtensionAPI): void {
|
|
|
421
405
|
|
|
422
406
|
const items: SelectItem[] = availableModels.map((m) => {
|
|
423
407
|
const key = modelKey(m);
|
|
424
|
-
const check = key === currentKey ?
|
|
408
|
+
const check = key === currentKey ? CHECKMARK : "";
|
|
425
409
|
return { value: key, label: `${m.name} (${m.provider})${check}` };
|
|
426
410
|
});
|
|
427
411
|
items.push({
|
|
428
412
|
value: NO_ADVISOR_VALUE,
|
|
429
|
-
label: currentKey === undefined ?
|
|
413
|
+
label: currentKey === undefined ? `No advisor${CHECKMARK}` : "No advisor",
|
|
430
414
|
});
|
|
431
415
|
|
|
432
|
-
const choice = await ctx
|
|
433
|
-
(tui, theme, _kb, done) => {
|
|
434
|
-
const container = new Container();
|
|
435
|
-
|
|
436
|
-
container.addChild(
|
|
437
|
-
new DynamicBorder((s: string) => theme.fg("accent", s)),
|
|
438
|
-
);
|
|
439
|
-
container.addChild(new Spacer(1));
|
|
440
|
-
container.addChild(
|
|
441
|
-
new Text(
|
|
442
|
-
theme.fg("accent", theme.bold(ADVISOR_HEADER_TITLE)),
|
|
443
|
-
1,
|
|
444
|
-
0,
|
|
445
|
-
),
|
|
446
|
-
);
|
|
447
|
-
container.addChild(new Spacer(1));
|
|
448
|
-
container.addChild(new Text(ADVISOR_HEADER_PROSE_1, 1, 0));
|
|
449
|
-
container.addChild(new Spacer(1));
|
|
450
|
-
container.addChild(new Text(ADVISOR_HEADER_PROSE_2, 1, 0));
|
|
451
|
-
container.addChild(new Spacer(1));
|
|
452
|
-
|
|
453
|
-
const selectList = new SelectList(
|
|
454
|
-
items,
|
|
455
|
-
Math.min(items.length, 10),
|
|
456
|
-
{
|
|
457
|
-
selectedPrefix: (t) => theme.bg("selectedBg", theme.fg("accent", t)),
|
|
458
|
-
selectedText: (t) => theme.bg("selectedBg", theme.bold(t)),
|
|
459
|
-
description: (t) => theme.fg("muted", t),
|
|
460
|
-
scrollInfo: (t) => theme.fg("dim", t),
|
|
461
|
-
noMatch: (t) => theme.fg("warning", t),
|
|
462
|
-
},
|
|
463
|
-
);
|
|
464
|
-
selectList.onSelect = (item) => done(item.value);
|
|
465
|
-
selectList.onCancel = () => done(null);
|
|
466
|
-
container.addChild(selectList);
|
|
467
|
-
|
|
468
|
-
container.addChild(new Spacer(1));
|
|
469
|
-
container.addChild(
|
|
470
|
-
new Text(
|
|
471
|
-
theme.fg("dim", "↑↓ navigate • enter select • esc cancel"),
|
|
472
|
-
1,
|
|
473
|
-
0,
|
|
474
|
-
),
|
|
475
|
-
);
|
|
476
|
-
container.addChild(new Spacer(1));
|
|
477
|
-
container.addChild(
|
|
478
|
-
new DynamicBorder((s: string) => theme.fg("accent", s)),
|
|
479
|
-
);
|
|
480
|
-
|
|
481
|
-
return {
|
|
482
|
-
render: (w) => container.render(w),
|
|
483
|
-
invalidate: () => container.invalidate(),
|
|
484
|
-
handleInput: (data) => {
|
|
485
|
-
selectList.handleInput(data);
|
|
486
|
-
tui.requestRender();
|
|
487
|
-
},
|
|
488
|
-
};
|
|
489
|
-
},
|
|
490
|
-
);
|
|
491
|
-
|
|
416
|
+
const choice = await showAdvisorPicker(ctx, items);
|
|
492
417
|
if (!choice) {
|
|
493
418
|
return;
|
|
494
419
|
}
|
|
@@ -501,104 +426,36 @@ export function registerAdvisorCommand(pi: ExtensionAPI): void {
|
|
|
501
426
|
setAdvisorEffort(undefined);
|
|
502
427
|
saveAdvisorConfig(undefined, undefined);
|
|
503
428
|
if (activeHas) {
|
|
504
|
-
pi.setActiveTools(
|
|
505
|
-
activeTools.filter((n) => n !== ADVISOR_TOOL_NAME),
|
|
506
|
-
);
|
|
429
|
+
pi.setActiveTools(activeTools.filter((n) => n !== ADVISOR_TOOL_NAME));
|
|
507
430
|
}
|
|
508
|
-
ctx.ui.notify(
|
|
431
|
+
ctx.ui.notify(MSG_ADVISOR_DISABLED, "info");
|
|
509
432
|
return;
|
|
510
433
|
}
|
|
511
434
|
|
|
512
435
|
const picked = availableModels.find((m) => modelKey(m) === choice);
|
|
513
436
|
if (!picked) {
|
|
514
|
-
ctx.ui.notify(
|
|
437
|
+
ctx.ui.notify(errSelectionNotFound(choice), "error");
|
|
515
438
|
return;
|
|
516
439
|
}
|
|
517
440
|
|
|
518
441
|
// Effort picker — only for reasoning-capable models
|
|
519
442
|
let effortChoice: ThinkingLevel | undefined;
|
|
520
443
|
if (picked.reasoning) {
|
|
521
|
-
const
|
|
522
|
-
const baseLevels: ThinkingLevel[] = ["minimal", "low", "medium", "high"];
|
|
523
|
-
const levels = supportsXhigh(picked)
|
|
524
|
-
? [...baseLevels, "xhigh" as ThinkingLevel]
|
|
525
|
-
: baseLevels;
|
|
444
|
+
const levels = supportsXhigh(picked) ? [...BASE_EFFORT_LEVELS, XHIGH_EFFORT_LEVEL] : BASE_EFFORT_LEVELS;
|
|
526
445
|
|
|
527
446
|
const effortItems: SelectItem[] = [
|
|
528
447
|
{ value: OFF_VALUE, label: "off" },
|
|
529
448
|
...levels.map((level) => ({
|
|
530
449
|
value: level,
|
|
531
|
-
label: level ===
|
|
450
|
+
label: level === DEFAULT_EFFORT ? `${level}${RECOMMENDED_EFFORT_SUFFIX}` : level,
|
|
532
451
|
})),
|
|
533
452
|
];
|
|
534
453
|
|
|
535
|
-
const effortResult = await ctx
|
|
536
|
-
(tui, theme, _kb, done) => {
|
|
537
|
-
const container = new Container();
|
|
538
|
-
|
|
539
|
-
container.addChild(
|
|
540
|
-
new DynamicBorder((s: string) => theme.fg("accent", s)),
|
|
541
|
-
);
|
|
542
|
-
container.addChild(new Spacer(1));
|
|
543
|
-
container.addChild(
|
|
544
|
-
new Text(
|
|
545
|
-
theme.fg("accent", theme.bold(EFFORT_HEADER_TITLE)),
|
|
546
|
-
1,
|
|
547
|
-
0,
|
|
548
|
-
),
|
|
549
|
-
);
|
|
550
|
-
container.addChild(new Spacer(1));
|
|
551
|
-
container.addChild(new Text(EFFORT_HEADER_PROSE, 1, 0));
|
|
552
|
-
container.addChild(new Spacer(1));
|
|
553
|
-
|
|
554
|
-
const selectList = new SelectList(
|
|
555
|
-
effortItems,
|
|
556
|
-
Math.min(effortItems.length, 10),
|
|
557
|
-
{
|
|
558
|
-
selectedPrefix: (t) => theme.bg("selectedBg", theme.fg("accent", t)),
|
|
559
|
-
selectedText: (t) => theme.bg("selectedBg", theme.bold(t)),
|
|
560
|
-
description: (t) => theme.fg("muted", t),
|
|
561
|
-
scrollInfo: (t) => theme.fg("dim", t),
|
|
562
|
-
noMatch: (t) => theme.fg("warning", t),
|
|
563
|
-
},
|
|
564
|
-
);
|
|
565
|
-
const currentEffort = getAdvisorEffort();
|
|
566
|
-
const defaultIdx = currentEffort
|
|
567
|
-
? effortItems.findIndex((item) => item.value === currentEffort)
|
|
568
|
-
: -1;
|
|
569
|
-
selectList.setSelectedIndex(defaultIdx >= 0 ? defaultIdx : effortItems.findIndex((item) => item.value === "high"));
|
|
570
|
-
selectList.onSelect = (item) => done(item.value);
|
|
571
|
-
selectList.onCancel = () => done(null);
|
|
572
|
-
container.addChild(selectList);
|
|
573
|
-
|
|
574
|
-
container.addChild(new Spacer(1));
|
|
575
|
-
container.addChild(
|
|
576
|
-
new Text(
|
|
577
|
-
theme.fg("dim", "↑↓ navigate • enter select • esc cancel"),
|
|
578
|
-
1,
|
|
579
|
-
0,
|
|
580
|
-
),
|
|
581
|
-
);
|
|
582
|
-
container.addChild(new Spacer(1));
|
|
583
|
-
container.addChild(
|
|
584
|
-
new DynamicBorder((s: string) => theme.fg("accent", s)),
|
|
585
|
-
);
|
|
586
|
-
|
|
587
|
-
return {
|
|
588
|
-
render: (w) => container.render(w),
|
|
589
|
-
invalidate: () => container.invalidate(),
|
|
590
|
-
handleInput: (data) => {
|
|
591
|
-
selectList.handleInput(data);
|
|
592
|
-
tui.requestRender();
|
|
593
|
-
},
|
|
594
|
-
};
|
|
595
|
-
},
|
|
596
|
-
);
|
|
597
|
-
|
|
454
|
+
const effortResult = await showEffortPicker(ctx, effortItems, getAdvisorEffort(), DEFAULT_EFFORT);
|
|
598
455
|
if (!effortResult) {
|
|
599
456
|
return;
|
|
600
457
|
}
|
|
601
|
-
effortChoice = effortResult === OFF_VALUE ? undefined : effortResult as ThinkingLevel;
|
|
458
|
+
effortChoice = effortResult === OFF_VALUE ? undefined : (effortResult as ThinkingLevel);
|
|
602
459
|
}
|
|
603
460
|
|
|
604
461
|
setAdvisorEffort(effortChoice);
|
|
@@ -607,10 +464,7 @@ export function registerAdvisorCommand(pi: ExtensionAPI): void {
|
|
|
607
464
|
if (!activeHas) {
|
|
608
465
|
pi.setActiveTools([...activeTools, ADVISOR_TOOL_NAME]);
|
|
609
466
|
}
|
|
610
|
-
ctx.ui.notify(
|
|
611
|
-
`Advisor: ${picked.provider}:${picked.id}${effortChoice ? `, ${effortChoice}` : ""}`,
|
|
612
|
-
"info",
|
|
613
|
-
);
|
|
467
|
+
ctx.ui.notify(msgAdvisorEnabled(modelKey(picked), effortChoice), "info");
|
|
614
468
|
},
|
|
615
469
|
});
|
|
616
470
|
}
|
package/index.ts
CHANGED
|
@@ -11,18 +11,18 @@
|
|
|
11
11
|
|
|
12
12
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
13
13
|
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
registerAdvisorBeforeAgentStart,
|
|
15
|
+
registerAdvisorCommand,
|
|
16
|
+
registerAdvisorTool,
|
|
17
|
+
restoreAdvisorState,
|
|
18
18
|
} from "./advisor.js";
|
|
19
19
|
|
|
20
20
|
export default function (pi: ExtensionAPI) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
registerAdvisorTool(pi);
|
|
22
|
+
registerAdvisorCommand(pi);
|
|
23
|
+
registerAdvisorBeforeAgentStart(pi);
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
26
|
+
restoreAdvisorState(ctx, pi);
|
|
27
|
+
});
|
|
28
28
|
}
|
package/package.json
CHANGED
|
@@ -1,24 +1,40 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juicesharp/rpiv-advisor",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Pi extension: advisor-strategy pattern — escalate to a stronger reviewer model",
|
|
5
|
-
"keywords": [
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi-package",
|
|
7
|
+
"pi-extension",
|
|
8
|
+
"rpiv",
|
|
9
|
+
"advisor"
|
|
10
|
+
],
|
|
6
11
|
"type": "module",
|
|
7
12
|
"license": "MIT",
|
|
8
13
|
"author": "juicesharp",
|
|
9
14
|
"repository": {
|
|
10
15
|
"type": "git",
|
|
11
|
-
"url": "git+https://github.com/juicesharp/rpiv-
|
|
16
|
+
"url": "git+https://github.com/juicesharp/rpiv-mono.git",
|
|
17
|
+
"directory": "packages/rpiv-advisor"
|
|
12
18
|
},
|
|
13
|
-
"homepage": "https://github.com/juicesharp/rpiv-advisor#readme",
|
|
19
|
+
"homepage": "https://github.com/juicesharp/rpiv-mono/tree/main/packages/rpiv-advisor#readme",
|
|
14
20
|
"bugs": {
|
|
15
|
-
"url": "https://github.com/juicesharp/rpiv-
|
|
21
|
+
"url": "https://github.com/juicesharp/rpiv-mono/issues"
|
|
16
22
|
},
|
|
17
23
|
"publishConfig": {
|
|
18
24
|
"access": "public"
|
|
19
25
|
},
|
|
26
|
+
"files": [
|
|
27
|
+
"index.ts",
|
|
28
|
+
"advisor.ts",
|
|
29
|
+
"advisor-ui.ts",
|
|
30
|
+
"prompts/",
|
|
31
|
+
"README.md",
|
|
32
|
+
"LICENSE"
|
|
33
|
+
],
|
|
20
34
|
"pi": {
|
|
21
|
-
"extensions": [
|
|
35
|
+
"extensions": [
|
|
36
|
+
"./index.ts"
|
|
37
|
+
]
|
|
22
38
|
},
|
|
23
39
|
"peerDependencies": {
|
|
24
40
|
"@mariozechner/pi-ai": "*",
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
You are an advisor model in an advisor-strategy pattern. An executor model is running a task end-to-end — calling tools, reading results, iterating toward a solution. When the executor hits a decision it cannot reasonably solve alone, it consults you for guidance.
|
|
2
|
+
|
|
3
|
+
You read the shared conversation context and return ONE of:
|
|
4
|
+
- a plan (concrete next steps the executor should take),
|
|
5
|
+
- a correction (the executor is going down a wrong path — redirect it),
|
|
6
|
+
- a stop signal (the executor should halt and escalate to the user).
|
|
7
|
+
|
|
8
|
+
You NEVER call tools. You NEVER produce user-facing output. Be concise, directive, and grounded in the shared context. Name files, functions, and line numbers where possible. No preamble, no apologies, no meta-commentary about being an advisor — just the guidance the executor needs.
|
package/docs/advisor.jpg
DELETED
|
Binary file
|