@juicesharp/rpiv-advisor 0.1.0
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 +38 -0
- package/advisor.ts +616 -0
- package/index.ts +28 -0
- package/package.json +29 -0
package/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# rpiv-advisor
|
|
2
|
+
|
|
3
|
+
Pi extension that registers the `advisor` tool and `/advisor` slash command,
|
|
4
|
+
implementing the advisor-strategy pattern: the executor model can escalate
|
|
5
|
+
decisions to a stronger reviewer model (e.g. Opus), receive guidance, and
|
|
6
|
+
resume.
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
pi install npm:@juicesharp/rpiv-advisor
|
|
11
|
+
|
|
12
|
+
Then restart your Pi session.
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
Configure an advisor model with `/advisor` — the command opens a selector for
|
|
17
|
+
any model registered with Pi's model registry, plus a reasoning-effort picker
|
|
18
|
+
for reasoning-capable models. Selection persists across sessions at
|
|
19
|
+
`~/.config/rpiv-advisor/advisor.json` (chmod 0600).
|
|
20
|
+
|
|
21
|
+
The `advisor` tool is registered at load but excluded from active tools by
|
|
22
|
+
default; selecting a model via `/advisor` enables it. Choose "No advisor" to
|
|
23
|
+
disable.
|
|
24
|
+
|
|
25
|
+
`advisor` takes zero parameters — calling it forwards the full serialized
|
|
26
|
+
conversation branch to the advisor model, which returns guidance (plan,
|
|
27
|
+
correction, or stop signal) that the executor consumes.
|
|
28
|
+
|
|
29
|
+
## Migration from rpiv-pi ≤ 0.3.0
|
|
30
|
+
|
|
31
|
+
If you had an advisor configured while rpiv-pi bundled this tool, your previous
|
|
32
|
+
selection lived at `~/.config/rpiv-pi/advisor.json`. The new plugin reads
|
|
33
|
+
`~/.config/rpiv-advisor/advisor.json` only — run `/advisor` once to re-select
|
|
34
|
+
your model.
|
|
35
|
+
|
|
36
|
+
## License
|
|
37
|
+
|
|
38
|
+
MIT
|
package/advisor.ts
ADDED
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* advisor tool + /advisor command — Advisor-strategy pattern.
|
|
3
|
+
*
|
|
4
|
+
* Lets the executor model consult a stronger advisor model (e.g. Opus) via an
|
|
5
|
+
* in-process completeSimple() call with the full serialized conversation branch
|
|
6
|
+
* as context. Advisor has no tools, never emits user-facing output, and returns
|
|
7
|
+
* guidance (plan, correction, or stop signal) that the executor resumes with.
|
|
8
|
+
*
|
|
9
|
+
* Default state is OFF — the tool is registered at load but a before_agent_start
|
|
10
|
+
* handler strips it from the active tool list each turn while no advisor model
|
|
11
|
+
* is selected. /advisor opens a selector panel (ctx.ui.custom) to pick an
|
|
12
|
+
* advisor model from ctx.modelRegistry.getAvailable() and toggles the tool in
|
|
13
|
+
* via pi.setActiveTools(). Selection is in-memory and resets each session.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
17
|
+
import { dirname, join } from "node:path";
|
|
18
|
+
import { homedir } from "node:os";
|
|
19
|
+
import { completeSimple, supportsXhigh, type Message, type ThinkingLevel } from "@mariozechner/pi-ai";
|
|
20
|
+
import type { Api, Model, StopReason, Usage } from "@mariozechner/pi-ai";
|
|
21
|
+
import {
|
|
22
|
+
DynamicBorder,
|
|
23
|
+
convertToLlm,
|
|
24
|
+
serializeConversation,
|
|
25
|
+
type AgentToolResult,
|
|
26
|
+
type AgentToolUpdateCallback,
|
|
27
|
+
type ExtensionAPI,
|
|
28
|
+
type ExtensionContext,
|
|
29
|
+
type SessionEntry,
|
|
30
|
+
} from "@mariozechner/pi-coding-agent";
|
|
31
|
+
import {
|
|
32
|
+
Container,
|
|
33
|
+
SelectList,
|
|
34
|
+
Spacer,
|
|
35
|
+
Text,
|
|
36
|
+
type SelectItem,
|
|
37
|
+
} from "@mariozechner/pi-tui";
|
|
38
|
+
import { Type } from "@sinclair/typebox";
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Constants
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
export const ADVISOR_TOOL_NAME = "advisor";
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Config file persistence (cross-session)
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
interface AdvisorConfig {
|
|
51
|
+
modelKey?: string;
|
|
52
|
+
effort?: ThinkingLevel;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const ADVISOR_CONFIG_PATH = join(homedir(), ".config", "rpiv-advisor", "advisor.json");
|
|
56
|
+
|
|
57
|
+
function loadAdvisorConfig(): AdvisorConfig {
|
|
58
|
+
if (!existsSync(ADVISOR_CONFIG_PATH)) return {};
|
|
59
|
+
try {
|
|
60
|
+
return JSON.parse(readFileSync(ADVISOR_CONFIG_PATH, "utf-8")) as AdvisorConfig;
|
|
61
|
+
} catch {
|
|
62
|
+
return {};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function saveAdvisorConfig(key: string | undefined, effort: ThinkingLevel | undefined): void {
|
|
67
|
+
const config: AdvisorConfig = {};
|
|
68
|
+
if (key) config.modelKey = key;
|
|
69
|
+
if (effort) config.effort = effort;
|
|
70
|
+
try {
|
|
71
|
+
mkdirSync(dirname(ADVISOR_CONFIG_PATH), { recursive: true });
|
|
72
|
+
writeFileSync(ADVISOR_CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
73
|
+
} catch {
|
|
74
|
+
// write may fail on disk-full or permission errors — best effort only
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
chmodSync(ADVISOR_CONFIG_PATH, 0o600);
|
|
78
|
+
} catch {
|
|
79
|
+
// chmod may fail on some filesystems — best effort only
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function parseModelKey(key: string): { provider: string; modelId: string } | undefined {
|
|
84
|
+
const idx = key.indexOf(":");
|
|
85
|
+
if (idx < 1) return undefined;
|
|
86
|
+
return { provider: key.slice(0, idx), modelId: key.slice(idx + 1) };
|
|
87
|
+
}
|
|
88
|
+
|
|
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
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// Types
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
export interface AdvisorDetails {
|
|
103
|
+
advisorModel?: string;
|
|
104
|
+
effort?: ThinkingLevel;
|
|
105
|
+
usage?: Usage;
|
|
106
|
+
stopReason?: StopReason;
|
|
107
|
+
errorMessage?: string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
// Module state — in-memory, resets each session
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
|
|
114
|
+
let selectedAdvisor: Model<Api> | undefined;
|
|
115
|
+
let selectedAdvisorEffort: ThinkingLevel | undefined;
|
|
116
|
+
|
|
117
|
+
export function getAdvisorModel(): Model<Api> | undefined {
|
|
118
|
+
return selectedAdvisor;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function setAdvisorModel(model: Model<Api> | undefined): void {
|
|
122
|
+
selectedAdvisor = model;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function getAdvisorEffort(): ThinkingLevel | undefined {
|
|
126
|
+
return selectedAdvisorEffort;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function setAdvisorEffort(effort: ThinkingLevel | undefined): void {
|
|
130
|
+
selectedAdvisorEffort = effort;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
// Session restoration — called from index.ts session_start handler
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
export function restoreAdvisorState(ctx: ExtensionContext, pi: ExtensionAPI): void {
|
|
138
|
+
const config = loadAdvisorConfig();
|
|
139
|
+
if (!config.modelKey) return;
|
|
140
|
+
|
|
141
|
+
const parsed = parseModelKey(config.modelKey);
|
|
142
|
+
if (!parsed) return;
|
|
143
|
+
|
|
144
|
+
const model = ctx.modelRegistry.find(parsed.provider, parsed.modelId);
|
|
145
|
+
if (!model) {
|
|
146
|
+
if (ctx.hasUI) {
|
|
147
|
+
ctx.ui.notify(
|
|
148
|
+
`Previously configured advisor model ${config.modelKey} is no longer available`,
|
|
149
|
+
"warning",
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
setAdvisorModel(model);
|
|
156
|
+
if (config.effort) {
|
|
157
|
+
setAdvisorEffort(config.effort);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const active = pi.getActiveTools();
|
|
161
|
+
if (!active.includes(ADVISOR_TOOL_NAME)) {
|
|
162
|
+
pi.setActiveTools([...active, ADVISOR_TOOL_NAME]);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (ctx.hasUI) {
|
|
166
|
+
ctx.ui.notify(
|
|
167
|
+
`Advisor restored: ${model.provider}:${model.id}${config.effort ? `, ${config.effort}` : ""}`,
|
|
168
|
+
"info",
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
// Core execute logic — curate context, call advisor, return structured result
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
function buildErrorResult(
|
|
178
|
+
advisorLabel: string | undefined,
|
|
179
|
+
userText: string,
|
|
180
|
+
errorMessage: string,
|
|
181
|
+
): AgentToolResult<AdvisorDetails> {
|
|
182
|
+
const effort = getAdvisorEffort();
|
|
183
|
+
return {
|
|
184
|
+
content: [{ type: "text", text: userText }],
|
|
185
|
+
details: advisorLabel
|
|
186
|
+
? { advisorModel: advisorLabel, effort, errorMessage }
|
|
187
|
+
: { effort, errorMessage },
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function executeAdvisor(
|
|
192
|
+
ctx: ExtensionContext,
|
|
193
|
+
signal: AbortSignal | undefined,
|
|
194
|
+
onUpdate: AgentToolUpdateCallback<AdvisorDetails> | undefined,
|
|
195
|
+
): Promise<AgentToolResult<AdvisorDetails>> {
|
|
196
|
+
const advisor = getAdvisorModel();
|
|
197
|
+
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
|
+
);
|
|
203
|
+
}
|
|
204
|
+
const advisorLabel = `${advisor.provider}:${advisor.id}`;
|
|
205
|
+
const effort = getAdvisorEffort();
|
|
206
|
+
|
|
207
|
+
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(advisor);
|
|
208
|
+
if (!auth.ok) {
|
|
209
|
+
return buildErrorResult(
|
|
210
|
+
advisorLabel,
|
|
211
|
+
`Advisor (${advisorLabel}) is misconfigured: ${auth.error}`,
|
|
212
|
+
auth.error,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
if (!auth.apiKey) {
|
|
216
|
+
const msg = `no API key for ${advisor.provider}`;
|
|
217
|
+
return buildErrorResult(
|
|
218
|
+
advisorLabel,
|
|
219
|
+
`Advisor (${advisorLabel}) has no API key available.`,
|
|
220
|
+
msg,
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const branch = ctx.sessionManager.getBranch();
|
|
225
|
+
const agentMessages = branch
|
|
226
|
+
.filter((e): e is SessionEntry & { type: "message" } => e.type === "message")
|
|
227
|
+
.map((e) => e.message);
|
|
228
|
+
const conversationText = serializeConversation(convertToLlm(agentMessages));
|
|
229
|
+
|
|
230
|
+
const userMessage: Message = {
|
|
231
|
+
role: "user",
|
|
232
|
+
content: [
|
|
233
|
+
{
|
|
234
|
+
type: "text",
|
|
235
|
+
text: `## Conversation So Far\n\n${conversationText}`,
|
|
236
|
+
},
|
|
237
|
+
],
|
|
238
|
+
timestamp: Date.now(),
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
onUpdate?.({
|
|
242
|
+
content: [{ type: "text", text: `Consulting advisor (${advisorLabel}${effort ? `, ${effort}` : ""})…` }],
|
|
243
|
+
details: { advisorModel: advisorLabel, effort },
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
const response = await completeSimple(
|
|
248
|
+
advisor,
|
|
249
|
+
{ systemPrompt: ADVISOR_SYSTEM_PROMPT, messages: [userMessage] },
|
|
250
|
+
{ apiKey: auth.apiKey, headers: auth.headers, signal, reasoning: effort },
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
if (response.stopReason === "aborted") {
|
|
254
|
+
return {
|
|
255
|
+
content: [
|
|
256
|
+
{ type: "text", text: "Advisor call was cancelled before it completed." },
|
|
257
|
+
],
|
|
258
|
+
details: {
|
|
259
|
+
advisorModel: advisorLabel,
|
|
260
|
+
effort,
|
|
261
|
+
usage: response.usage,
|
|
262
|
+
stopReason: response.stopReason,
|
|
263
|
+
errorMessage: response.errorMessage ?? "aborted",
|
|
264
|
+
},
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (response.stopReason === "error") {
|
|
269
|
+
return {
|
|
270
|
+
content: [
|
|
271
|
+
{
|
|
272
|
+
type: "text",
|
|
273
|
+
text: `Advisor call failed: ${response.errorMessage ?? "unknown error"}`,
|
|
274
|
+
},
|
|
275
|
+
],
|
|
276
|
+
details: {
|
|
277
|
+
advisorModel: advisorLabel,
|
|
278
|
+
effort,
|
|
279
|
+
usage: response.usage,
|
|
280
|
+
stopReason: response.stopReason,
|
|
281
|
+
errorMessage: response.errorMessage,
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const advisorText = response.content
|
|
287
|
+
.filter((c): c is { type: "text"; text: string } => c.type === "text")
|
|
288
|
+
.map((c) => c.text)
|
|
289
|
+
.join("\n")
|
|
290
|
+
.trim();
|
|
291
|
+
|
|
292
|
+
if (!advisorText) {
|
|
293
|
+
return {
|
|
294
|
+
content: [{ type: "text", text: "Advisor returned no text content." }],
|
|
295
|
+
details: {
|
|
296
|
+
advisorModel: advisorLabel,
|
|
297
|
+
effort,
|
|
298
|
+
usage: response.usage,
|
|
299
|
+
stopReason: response.stopReason,
|
|
300
|
+
errorMessage: "empty response",
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
content: [{ type: "text", text: advisorText }],
|
|
307
|
+
details: {
|
|
308
|
+
advisorModel: advisorLabel,
|
|
309
|
+
effort,
|
|
310
|
+
usage: response.usage,
|
|
311
|
+
stopReason: response.stopReason,
|
|
312
|
+
},
|
|
313
|
+
};
|
|
314
|
+
} catch (err) {
|
|
315
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
316
|
+
return buildErrorResult(
|
|
317
|
+
advisorLabel,
|
|
318
|
+
`Advisor call threw: ${message}`,
|
|
319
|
+
message,
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ---------------------------------------------------------------------------
|
|
325
|
+
// Tool registration — zero-param schema, curated description/snippet/guidelines
|
|
326
|
+
// ---------------------------------------------------------------------------
|
|
327
|
+
|
|
328
|
+
const AdvisorParams = Type.Object({});
|
|
329
|
+
|
|
330
|
+
const ADVISOR_DESCRIPTION =
|
|
331
|
+
"Escalate to a stronger reviewer model for guidance. When you need " +
|
|
332
|
+
"stronger judgment — a complex decision, an ambiguous failure, a problem " +
|
|
333
|
+
"you're circling without progress — escalate to the advisor model for " +
|
|
334
|
+
"guidance, then resume. Takes NO parameters — when you call advisor(), " +
|
|
335
|
+
"your entire conversation history is automatically forwarded. The advisor " +
|
|
336
|
+
"sees the task, every tool call you've made, every result you've seen.";
|
|
337
|
+
|
|
338
|
+
const ADVISOR_PROMPT_SNIPPET =
|
|
339
|
+
"Escalate to a stronger reviewer model for guidance when stuck, before substantive work, or before declaring done";
|
|
340
|
+
|
|
341
|
+
const ADVISOR_PROMPT_GUIDELINES: string[] = [
|
|
342
|
+
"Call `advisor` BEFORE substantive work — before writing, before committing to an interpretation, before building on an assumption. Orientation (finding files, fetching a source, seeing what's there) is not substantive work; writing, editing, and declaring an answer are.",
|
|
343
|
+
"Also call `advisor` when you believe the task is complete. BEFORE this call, make your deliverable durable: write the file, save the result, commit the change. The advisor call takes time; if the session ends during it, a durable result persists and an unwritten one doesn't.",
|
|
344
|
+
"Also call `advisor` when stuck — errors recurring, approach not converging, results that don't fit — or when considering a change of approach.",
|
|
345
|
+
"On tasks longer than a few steps, call `advisor` at least once before committing to an approach and once before declaring done. On short reactive tasks where the next action is dictated by tool output you just read, you don't need to keep calling — the advisor adds most of its value on the first call, before the approach crystallizes.",
|
|
346
|
+
"Give the advisor's advice serious weight. If you follow a step and it fails empirically, or you have primary-source evidence that contradicts a specific claim, adapt — a passing self-test is not evidence the advice is wrong, it's evidence your test doesn't check what the advice is checking.",
|
|
347
|
+
"If you've already retrieved data pointing one way and the advisor points another, don't silently switch — surface the conflict in one more `advisor` call (\"I found X, you suggest Y, which constraint breaks the tie?\"). A reconcile call is cheaper than committing to the wrong branch.",
|
|
348
|
+
];
|
|
349
|
+
|
|
350
|
+
export function registerAdvisorTool(pi: ExtensionAPI): void {
|
|
351
|
+
pi.registerTool({
|
|
352
|
+
name: ADVISOR_TOOL_NAME,
|
|
353
|
+
label: "Advisor",
|
|
354
|
+
description: ADVISOR_DESCRIPTION,
|
|
355
|
+
promptSnippet: ADVISOR_PROMPT_SNIPPET,
|
|
356
|
+
promptGuidelines: ADVISOR_PROMPT_GUIDELINES,
|
|
357
|
+
parameters: AdvisorParams,
|
|
358
|
+
|
|
359
|
+
async execute(_toolCallId, _params, signal, onUpdate, ctx) {
|
|
360
|
+
return executeAdvisor(ctx, signal, onUpdate);
|
|
361
|
+
},
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// ---------------------------------------------------------------------------
|
|
366
|
+
// before_agent_start handler — strip advisor from active tools when disabled
|
|
367
|
+
// ---------------------------------------------------------------------------
|
|
368
|
+
|
|
369
|
+
export function registerAdvisorBeforeAgentStart(pi: ExtensionAPI): void {
|
|
370
|
+
pi.on("before_agent_start", async () => {
|
|
371
|
+
if (!getAdvisorModel()) {
|
|
372
|
+
const active = pi.getActiveTools();
|
|
373
|
+
if (active.includes(ADVISOR_TOOL_NAME)) {
|
|
374
|
+
pi.setActiveTools(active.filter((n) => n !== ADVISOR_TOOL_NAME));
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// ---------------------------------------------------------------------------
|
|
381
|
+
// /advisor slash command — opens selector panel for picking the advisor model
|
|
382
|
+
// ---------------------------------------------------------------------------
|
|
383
|
+
|
|
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
|
+
function modelKey(m: { provider: string; id: string }): string {
|
|
406
|
+
return `${m.provider}:${m.id}`;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
export function registerAdvisorCommand(pi: ExtensionAPI): void {
|
|
410
|
+
pi.registerCommand("advisor", {
|
|
411
|
+
description: "Configure the advisor model for the advisor-strategy pattern",
|
|
412
|
+
handler: async (_args, ctx) => {
|
|
413
|
+
if (!ctx.hasUI) {
|
|
414
|
+
ctx.ui.notify("/advisor requires interactive mode", "error");
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const availableModels = ctx.modelRegistry.getAvailable();
|
|
419
|
+
const current = getAdvisorModel();
|
|
420
|
+
const currentKey = current ? modelKey(current) : undefined;
|
|
421
|
+
|
|
422
|
+
const items: SelectItem[] = availableModels.map((m) => {
|
|
423
|
+
const key = modelKey(m);
|
|
424
|
+
const check = key === currentKey ? " ✓" : "";
|
|
425
|
+
return { value: key, label: `${m.name} (${m.provider})${check}` };
|
|
426
|
+
});
|
|
427
|
+
items.push({
|
|
428
|
+
value: NO_ADVISOR_VALUE,
|
|
429
|
+
label: currentKey === undefined ? "No advisor ✓" : "No advisor",
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
const choice = await ctx.ui.custom<string | null>(
|
|
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
|
+
|
|
492
|
+
if (!choice) {
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const activeTools = pi.getActiveTools();
|
|
497
|
+
const activeHas = activeTools.includes(ADVISOR_TOOL_NAME);
|
|
498
|
+
|
|
499
|
+
if (choice === NO_ADVISOR_VALUE) {
|
|
500
|
+
setAdvisorModel(undefined);
|
|
501
|
+
setAdvisorEffort(undefined);
|
|
502
|
+
saveAdvisorConfig(undefined, undefined);
|
|
503
|
+
if (activeHas) {
|
|
504
|
+
pi.setActiveTools(
|
|
505
|
+
activeTools.filter((n) => n !== ADVISOR_TOOL_NAME),
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
ctx.ui.notify("Advisor disabled", "info");
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const picked = availableModels.find((m) => modelKey(m) === choice);
|
|
513
|
+
if (!picked) {
|
|
514
|
+
ctx.ui.notify(`Advisor selection not found: ${choice}`, "error");
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Effort picker — only for reasoning-capable models
|
|
519
|
+
let effortChoice: ThinkingLevel | undefined;
|
|
520
|
+
if (picked.reasoning) {
|
|
521
|
+
const OFF_VALUE = "__off__";
|
|
522
|
+
const baseLevels: ThinkingLevel[] = ["minimal", "low", "medium", "high"];
|
|
523
|
+
const levels = supportsXhigh(picked)
|
|
524
|
+
? [...baseLevels, "xhigh" as ThinkingLevel]
|
|
525
|
+
: baseLevels;
|
|
526
|
+
|
|
527
|
+
const effortItems: SelectItem[] = [
|
|
528
|
+
{ value: OFF_VALUE, label: "off" },
|
|
529
|
+
...levels.map((level) => ({
|
|
530
|
+
value: level,
|
|
531
|
+
label: level === "high" ? `${level} (recommended)` : level,
|
|
532
|
+
})),
|
|
533
|
+
];
|
|
534
|
+
|
|
535
|
+
const effortResult = await ctx.ui.custom<string | null>(
|
|
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
|
+
|
|
598
|
+
if (!effortResult) {
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
effortChoice = effortResult === OFF_VALUE ? undefined : effortResult as ThinkingLevel;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
setAdvisorEffort(effortChoice);
|
|
605
|
+
setAdvisorModel(picked);
|
|
606
|
+
saveAdvisorConfig(modelKey(picked), effortChoice);
|
|
607
|
+
if (!activeHas) {
|
|
608
|
+
pi.setActiveTools([...activeTools, ADVISOR_TOOL_NAME]);
|
|
609
|
+
}
|
|
610
|
+
ctx.ui.notify(
|
|
611
|
+
`Advisor: ${picked.provider}:${picked.id}${effortChoice ? `, ${effortChoice}` : ""}`,
|
|
612
|
+
"info",
|
|
613
|
+
);
|
|
614
|
+
},
|
|
615
|
+
});
|
|
616
|
+
}
|
package/index.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rpiv-advisor — Pi extension
|
|
3
|
+
*
|
|
4
|
+
* Registers the `advisor` tool, `/advisor` command, and the two lifecycle
|
|
5
|
+
* hooks (session_start restore, before_agent_start strip) that together
|
|
6
|
+
* implement the advisor-strategy pattern.
|
|
7
|
+
*
|
|
8
|
+
* Config persists at ~/.config/rpiv-advisor/advisor.json. Tool name
|
|
9
|
+
* preserved verbatim from rpiv-pi@7525a5d.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
13
|
+
import {
|
|
14
|
+
registerAdvisorTool,
|
|
15
|
+
registerAdvisorCommand,
|
|
16
|
+
registerAdvisorBeforeAgentStart,
|
|
17
|
+
restoreAdvisorState,
|
|
18
|
+
} from "./advisor.js";
|
|
19
|
+
|
|
20
|
+
export default function (pi: ExtensionAPI) {
|
|
21
|
+
registerAdvisorTool(pi);
|
|
22
|
+
registerAdvisorCommand(pi);
|
|
23
|
+
registerAdvisorBeforeAgentStart(pi);
|
|
24
|
+
|
|
25
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
26
|
+
restoreAdvisorState(ctx, pi);
|
|
27
|
+
});
|
|
28
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@juicesharp/rpiv-advisor",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Pi extension: advisor-strategy pattern — escalate to a stronger reviewer model",
|
|
5
|
+
"keywords": ["pi-package", "pi-extension", "rpiv", "advisor"],
|
|
6
|
+
"type": "module",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"author": "juicesharp",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/juicesharp/rpiv-advisor.git"
|
|
12
|
+
},
|
|
13
|
+
"homepage": "https://github.com/juicesharp/rpiv-advisor#readme",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/juicesharp/rpiv-advisor/issues"
|
|
16
|
+
},
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"pi": {
|
|
21
|
+
"extensions": ["./index.ts"]
|
|
22
|
+
},
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"@mariozechner/pi-ai": "*",
|
|
25
|
+
"@mariozechner/pi-coding-agent": "*",
|
|
26
|
+
"@mariozechner/pi-tui": "*",
|
|
27
|
+
"@sinclair/typebox": "*"
|
|
28
|
+
}
|
|
29
|
+
}
|