@ian-pascoe/pi-advisor 0.5.0 → 0.7.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 +52 -16
- package/package.json +7 -7
- package/skills/pi-advisor/SKILL.md +7 -3
- package/src/advisor-command.ts +26 -47
- package/src/advisor-contract.ts +70 -4
- package/src/advisor-evidence.ts +157 -0
- package/src/advisor-menu.ts +54 -253
- package/src/advisor-observer.ts +514 -117
- package/src/advisor-rendering.ts +231 -112
- package/src/advisor-session.ts +14 -3
- package/src/advisor-settings.ts +80 -115
- package/src/index.ts +31 -12
package/src/advisor-menu.ts
CHANGED
|
@@ -1,23 +1,17 @@
|
|
|
1
|
+
import { getSettingsListTheme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { SettingsList, type Component, type SettingItem } from "@earendil-works/pi-tui";
|
|
1
3
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
SettingsList,
|
|
14
|
-
type Component,
|
|
15
|
-
type SelectItem,
|
|
16
|
-
type SettingItem,
|
|
17
|
-
type TUI,
|
|
18
|
-
type TuiMouseEvent,
|
|
19
|
-
type TuiMouseEventResult,
|
|
20
|
-
} from "@earendil-works/pi-tui";
|
|
4
|
+
cycleDisplay,
|
|
5
|
+
EditorChooser,
|
|
6
|
+
effectiveWithSource,
|
|
7
|
+
errorText,
|
|
8
|
+
ModelPicker,
|
|
9
|
+
nextCycleValue,
|
|
10
|
+
scopeRow,
|
|
11
|
+
SettingsMenu,
|
|
12
|
+
ValueInput,
|
|
13
|
+
type SettingsMenuUi,
|
|
14
|
+
} from "@ian-pascoe/pi-utils/settings-menu";
|
|
21
15
|
import { Value } from "typebox/value";
|
|
22
16
|
import { formatAdvisorOption, type AdvisorRenderTheme } from "./advisor-rendering.js";
|
|
23
17
|
import {
|
|
@@ -61,11 +55,8 @@ export interface AdvisorMenuHost {
|
|
|
61
55
|
}
|
|
62
56
|
|
|
63
57
|
/** Native UI collaborators supplied by `ctx.ui.custom`. */
|
|
64
|
-
export interface AdvisorMenuUi {
|
|
65
|
-
tui: TUI;
|
|
66
|
-
keybindings: KeybindingsManager;
|
|
58
|
+
export interface AdvisorMenuUi extends SettingsMenuUi {
|
|
67
59
|
theme: AdvisorRenderTheme;
|
|
68
|
-
externalEditorCommand?: string;
|
|
69
60
|
}
|
|
70
61
|
|
|
71
62
|
const thinkingLevels = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
@@ -99,6 +90,13 @@ const descriptions = {
|
|
|
99
90
|
maxToolCalls: "Investigative tool calls per Review",
|
|
100
91
|
maxCorrectiveTurns: "Automatic Corrective Turns per request",
|
|
101
92
|
maxFindingsPerReview: "Findings accepted from one Review (1–32)",
|
|
93
|
+
maxNitsPerRequest: "Nits delivered per request; further Nits are dropped (0 delivers none)",
|
|
94
|
+
seedBudgetTokens:
|
|
95
|
+
"Token budget for the Context Seed; auto is a quarter of the Advisor model's context window, at most 100k",
|
|
96
|
+
reviewEvery:
|
|
97
|
+
"When Reviews run: every turn, every N turns, or once per request; a tool error reviews at once",
|
|
98
|
+
maxSessionTokens:
|
|
99
|
+
"Advisor Session size that triggers native compaction; auto is half the Advisor model's context window, at most 200k",
|
|
102
100
|
} satisfies Record<keyof AdvisorOptions, string>;
|
|
103
101
|
const inheritRow = "\u0000inherit";
|
|
104
102
|
const inputHints = {
|
|
@@ -107,13 +105,11 @@ const inputHints = {
|
|
|
107
105
|
maxToolCalls: "a number, or inherit",
|
|
108
106
|
maxCorrectiveTurns: "a number, or inherit",
|
|
109
107
|
maxFindingsPerReview: "a number from 1 to 32, or inherit",
|
|
108
|
+
maxNitsPerRequest: "a number, or inherit",
|
|
109
|
+
seedBudgetTokens: "a token count, auto, or inherit",
|
|
110
|
+
reviewEvery: "turn, request, a number of turns, or inherit",
|
|
111
|
+
maxSessionTokens: "a token count, auto, or inherit",
|
|
110
112
|
} as const;
|
|
111
|
-
const listActions = [
|
|
112
|
-
"tui.select.up",
|
|
113
|
-
"tui.select.down",
|
|
114
|
-
"tui.select.confirm",
|
|
115
|
-
"tui.select.cancel",
|
|
116
|
-
] as const;
|
|
117
113
|
|
|
118
114
|
/** Convert one typed or selected menu value into a validated change. */
|
|
119
115
|
function parseAdvisorMenuValue(
|
|
@@ -133,9 +129,18 @@ function parseAdvisorMenuValue(
|
|
|
133
129
|
return { catchUpThreshold: value === "off" ? value : Number(value) };
|
|
134
130
|
case "reviewTimeoutMs":
|
|
135
131
|
return { reviewTimeoutMs: Math.round(Number(value) * 1_000) };
|
|
132
|
+
case "seedBudgetTokens":
|
|
133
|
+
return { seedBudgetTokens: value === "auto" ? value : Number(value) };
|
|
134
|
+
case "maxSessionTokens":
|
|
135
|
+
return { maxSessionTokens: value === "auto" ? value : Number(value) };
|
|
136
|
+
case "reviewEvery":
|
|
137
|
+
return {
|
|
138
|
+
reviewEvery: value === "turn" || value === "request" ? value : Number(value),
|
|
139
|
+
};
|
|
136
140
|
case "maxToolCalls":
|
|
137
141
|
case "maxCorrectiveTurns":
|
|
138
142
|
case "maxFindingsPerReview":
|
|
143
|
+
case "maxNitsPerRequest":
|
|
139
144
|
return { [key]: Number(value) };
|
|
140
145
|
default:
|
|
141
146
|
return { [key]: value };
|
|
@@ -144,188 +149,34 @@ function parseAdvisorMenuValue(
|
|
|
144
149
|
return { action: "set", key, patch: parseAdvisorOptions(patch, scope) };
|
|
145
150
|
}
|
|
146
151
|
|
|
147
|
-
function errorText(cause: unknown): string {
|
|
148
|
-
return cause instanceof Error ? cause.message : String(cause);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/** Single-line value entry with inline validation. */
|
|
152
|
-
class ValueInput implements Component {
|
|
153
|
-
private readonly input: Input;
|
|
154
|
-
private error: string | undefined;
|
|
155
|
-
|
|
156
|
-
constructor(
|
|
157
|
-
private readonly title: string,
|
|
158
|
-
private readonly hint: string,
|
|
159
|
-
private readonly theme: AdvisorRenderTheme,
|
|
160
|
-
submit: (text: string) => void,
|
|
161
|
-
cancel: () => void,
|
|
162
|
-
) {
|
|
163
|
-
this.input = new Input({ placeholder: hint });
|
|
164
|
-
this.input.focused = true;
|
|
165
|
-
this.input.onSubmit = (text) => {
|
|
166
|
-
try {
|
|
167
|
-
submit(text);
|
|
168
|
-
} catch (cause) {
|
|
169
|
-
this.error = errorText(cause);
|
|
170
|
-
}
|
|
171
|
-
};
|
|
172
|
-
this.input.onEscape = cancel;
|
|
173
|
-
}
|
|
174
|
-
handleInput(data: string): void {
|
|
175
|
-
this.error = undefined;
|
|
176
|
-
this.input.handleInput(data);
|
|
177
|
-
}
|
|
178
|
-
render(width: number): string[] {
|
|
179
|
-
return [
|
|
180
|
-
this.theme.bold(this.title),
|
|
181
|
-
...this.input.render(width),
|
|
182
|
-
this.error ? this.theme.fg("error", `✖ ${this.error}`) : this.theme.fg("dim", this.hint),
|
|
183
|
-
];
|
|
184
|
-
}
|
|
185
|
-
invalidate(): void {
|
|
186
|
-
this.input.invalidate();
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/** Fuzzy-searchable model list with an inherit choice. */
|
|
191
|
-
class ModelPicker implements Component {
|
|
192
|
-
private readonly input = new Input({ placeholder: "type to search" });
|
|
193
|
-
private list: SelectList;
|
|
194
|
-
|
|
195
|
-
constructor(
|
|
196
|
-
private readonly models: readonly string[],
|
|
197
|
-
private readonly choose: (value: string) => void,
|
|
198
|
-
private readonly cancel: () => void,
|
|
199
|
-
) {
|
|
200
|
-
this.input.focused = true;
|
|
201
|
-
this.list = this.createList("");
|
|
202
|
-
}
|
|
203
|
-
private createList(query: string): SelectList {
|
|
204
|
-
const items: SelectItem[] = ["inherit", ...this.models].map((value) => ({
|
|
205
|
-
value,
|
|
206
|
-
label: value,
|
|
207
|
-
}));
|
|
208
|
-
const list = new SelectList(
|
|
209
|
-
query ? fuzzyFilter(items, query, (item) => item.value) : items,
|
|
210
|
-
10,
|
|
211
|
-
getSelectListTheme(),
|
|
212
|
-
);
|
|
213
|
-
list.onSelect = (item) => this.choose(item.value);
|
|
214
|
-
list.onCancel = this.cancel;
|
|
215
|
-
return list;
|
|
216
|
-
}
|
|
217
|
-
handleInput(data: string): void {
|
|
218
|
-
const bindings = getKeybindings();
|
|
219
|
-
if (listActions.some((action) => bindings.matches(data, action))) {
|
|
220
|
-
this.list.handleInput(data);
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
this.input.handleInput(data);
|
|
224
|
-
this.list = this.createList(this.input.getValue());
|
|
225
|
-
}
|
|
226
|
-
render(width: number): string[] {
|
|
227
|
-
return [...this.input.render(width), ...this.list.render(width)];
|
|
228
|
-
}
|
|
229
|
-
invalidate(): void {
|
|
230
|
-
this.input.invalidate();
|
|
231
|
-
this.list.invalidate();
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/** Choose between editing the prompt in Pi's editor component and inheriting it. */
|
|
236
|
-
class PromptChooser implements Component {
|
|
237
|
-
private readonly list: SelectList;
|
|
238
|
-
private editor: ExtensionEditorComponent | undefined;
|
|
239
|
-
private error: string | undefined;
|
|
240
|
-
|
|
241
|
-
/** `submit` throws a user-facing message for an invalid prompt; the editor stays open. */
|
|
242
|
-
constructor(
|
|
243
|
-
ui: AdvisorMenuUi,
|
|
244
|
-
prompt: string,
|
|
245
|
-
submit: (text: string) => void,
|
|
246
|
-
inherit: () => void,
|
|
247
|
-
cancel: () => void,
|
|
248
|
-
) {
|
|
249
|
-
this.list = new SelectList(
|
|
250
|
-
[
|
|
251
|
-
{ value: "edit", label: "Edit…" },
|
|
252
|
-
{ value: "inherit", label: "inherit" },
|
|
253
|
-
],
|
|
254
|
-
2,
|
|
255
|
-
getSelectListTheme(),
|
|
256
|
-
);
|
|
257
|
-
this.list.onCancel = cancel;
|
|
258
|
-
this.list.onSelect = (item) => {
|
|
259
|
-
if (item.value === "inherit") {
|
|
260
|
-
inherit();
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
this.editor = new ExtensionEditorComponent(
|
|
264
|
-
ui.tui,
|
|
265
|
-
ui.keybindings,
|
|
266
|
-
"Advisor Prompt",
|
|
267
|
-
prompt,
|
|
268
|
-
(text) => {
|
|
269
|
-
try {
|
|
270
|
-
submit(text);
|
|
271
|
-
} catch (cause) {
|
|
272
|
-
this.error = ui.theme.fg("error", `✖ ${errorText(cause)}`);
|
|
273
|
-
}
|
|
274
|
-
},
|
|
275
|
-
cancel,
|
|
276
|
-
undefined,
|
|
277
|
-
ui.externalEditorCommand,
|
|
278
|
-
);
|
|
279
|
-
this.editor.focused = true;
|
|
280
|
-
};
|
|
281
|
-
}
|
|
282
|
-
handleInput(data: string): void {
|
|
283
|
-
this.error = undefined;
|
|
284
|
-
(this.editor ?? this.list).handleInput(data);
|
|
285
|
-
}
|
|
286
|
-
render(width: number): string[] {
|
|
287
|
-
const lines = (this.editor ?? this.list).render(width);
|
|
288
|
-
return this.error ? [...lines, this.error] : lines;
|
|
289
|
-
}
|
|
290
|
-
invalidate(): void {
|
|
291
|
-
(this.editor ?? this.list).invalidate();
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
152
|
/** Menu row ids that are not Advisor options. */
|
|
296
153
|
const actionRows = { resume: "resume", scope: "scope" } as const;
|
|
297
154
|
|
|
298
155
|
/** `/advisor` settings menu built from Pi's native settings list. */
|
|
299
|
-
export class AdvisorSettingsMenu
|
|
156
|
+
export class AdvisorSettingsMenu extends SettingsMenu {
|
|
300
157
|
private scope: AdvisorSettingScope = "session";
|
|
301
158
|
private view: AdvisorMenuView;
|
|
302
159
|
private rows: SettingItem[] = [];
|
|
303
|
-
private list: SettingsList;
|
|
304
|
-
private error: string | undefined;
|
|
305
160
|
private lastRow: string = actionRows.scope;
|
|
306
|
-
/** Edits run one at a time so each reads the result of the previous one. */
|
|
307
|
-
private pending: Promise<void> = Promise.resolve();
|
|
308
161
|
private submenuOpen = false;
|
|
309
162
|
private rebuildPending = false;
|
|
310
|
-
private readonly border: DynamicBorder;
|
|
311
163
|
|
|
312
164
|
constructor(
|
|
313
165
|
private readonly host: AdvisorMenuHost,
|
|
314
|
-
|
|
315
|
-
|
|
166
|
+
protected override readonly ui: AdvisorMenuUi,
|
|
167
|
+
done: () => void,
|
|
316
168
|
) {
|
|
169
|
+
super("Advisor settings", ui, done);
|
|
317
170
|
this.view = host.view();
|
|
318
|
-
this.
|
|
319
|
-
this.list = this.createList();
|
|
171
|
+
this.createList();
|
|
320
172
|
}
|
|
321
173
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
return this.pending;
|
|
174
|
+
protected headline(): readonly string[] {
|
|
175
|
+
return this.view.headline;
|
|
325
176
|
}
|
|
326
177
|
|
|
327
178
|
/** Re-read the host after external state changes, such as a Review starting. */
|
|
328
|
-
refresh(): void {
|
|
179
|
+
override refresh(): void {
|
|
329
180
|
const paused = this.view.paused;
|
|
330
181
|
try {
|
|
331
182
|
this.view = this.host.view();
|
|
@@ -345,8 +196,7 @@ export class AdvisorSettingsMenu implements Component {
|
|
|
345
196
|
|
|
346
197
|
private rebuild(): void {
|
|
347
198
|
this.rebuildPending = false;
|
|
348
|
-
this.
|
|
349
|
-
this.list.selectItem(this.view.paused ? actionRows.resume : this.lastRow);
|
|
199
|
+
this.createList().selectItem(this.view.paused ? actionRows.resume : this.lastRow);
|
|
350
200
|
}
|
|
351
201
|
|
|
352
202
|
private createList(): SettingsList {
|
|
@@ -356,13 +206,7 @@ export class AdvisorSettingsMenu implements Component {
|
|
|
356
206
|
...advisorOptionKeys,
|
|
357
207
|
];
|
|
358
208
|
this.rows = ids.map((id) => this.createRow(id));
|
|
359
|
-
return
|
|
360
|
-
this.rows,
|
|
361
|
-
this.rows.length,
|
|
362
|
-
getSettingsListTheme(),
|
|
363
|
-
(id, value) => this.change(id, value),
|
|
364
|
-
this.done,
|
|
365
|
-
);
|
|
209
|
+
return this.setList(this.rows, (id, value) => this.change(id, value));
|
|
366
210
|
}
|
|
367
211
|
|
|
368
212
|
private createRow(id: string): SettingItem {
|
|
@@ -374,14 +218,7 @@ export class AdvisorSettingsMenu implements Component {
|
|
|
374
218
|
values: ["retry"],
|
|
375
219
|
description: "Retry the Paused Advisor with its current settings",
|
|
376
220
|
};
|
|
377
|
-
if (id === actionRows.scope)
|
|
378
|
-
return {
|
|
379
|
-
id,
|
|
380
|
-
label: "Scope",
|
|
381
|
-
currentValue: this.scope,
|
|
382
|
-
values: [...this.view.scopes],
|
|
383
|
-
description: "Where edits are written",
|
|
384
|
-
};
|
|
221
|
+
if (id === actionRows.scope) return scopeRow(this.scope, this.view.scopes);
|
|
385
222
|
return this.optionRow(advisorOptionKey(id));
|
|
386
223
|
}
|
|
387
224
|
|
|
@@ -450,8 +287,9 @@ export class AdvisorSettingsMenu implements Component {
|
|
|
450
287
|
submenu: this.submenu(
|
|
451
288
|
key,
|
|
452
289
|
(close) =>
|
|
453
|
-
new
|
|
290
|
+
new EditorChooser(
|
|
454
291
|
this.ui,
|
|
292
|
+
"Advisor Prompt",
|
|
455
293
|
settings.prompt ?? "",
|
|
456
294
|
(text) => {
|
|
457
295
|
// Throws for an invalid prompt, keeping the editor open.
|
|
@@ -540,12 +378,14 @@ export class AdvisorSettingsMenu implements Component {
|
|
|
540
378
|
|
|
541
379
|
/** This scope's own value, or what it inherits; notes when another scope overrides it. */
|
|
542
380
|
private cycleDisplay(key: CycleKey): string {
|
|
543
|
-
const own = cycleValue(key, this.view.authored[this.scope] ?? {});
|
|
544
381
|
const effective = cycleValue(key, this.view.settings);
|
|
545
382
|
const source = this.view.sources[key] ?? "default";
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
383
|
+
return cycleDisplay({
|
|
384
|
+
own: cycleValue(key, this.view.authored[this.scope] ?? {}),
|
|
385
|
+
inEffect: effective === undefined ? "observed agent" : effectiveWithSource(effective, source),
|
|
386
|
+
source,
|
|
387
|
+
scope: this.scope,
|
|
388
|
+
});
|
|
549
389
|
}
|
|
550
390
|
|
|
551
391
|
private change(id: string, value: string): void {
|
|
@@ -566,8 +406,7 @@ export class AdvisorSettingsMenu implements Component {
|
|
|
566
406
|
// text is not one of the values, so ignore its suggestion.
|
|
567
407
|
const values = cycleValues[key];
|
|
568
408
|
const own = cycleValue(key, this.view.authored[this.scope] ?? {}) ?? "inherit";
|
|
569
|
-
const next =
|
|
570
|
-
values[(values.findIndex((option) => option === own) + 1) % values.length] ?? "inherit";
|
|
409
|
+
const next = nextCycleValue(values, own) ?? "inherit";
|
|
571
410
|
let change: AdvisorChange;
|
|
572
411
|
try {
|
|
573
412
|
change = parseAdvisorMenuValue(key, next, this.scope);
|
|
@@ -588,42 +427,4 @@ export class AdvisorSettingsMenu implements Component {
|
|
|
588
427
|
const scope = this.scope;
|
|
589
428
|
this.run(() => this.host.apply(scope, change));
|
|
590
429
|
}
|
|
591
|
-
|
|
592
|
-
private run(task: () => Promise<void>): void {
|
|
593
|
-
this.error = undefined;
|
|
594
|
-
this.pending = this.pending
|
|
595
|
-
.then(task)
|
|
596
|
-
.catch((cause: unknown) => {
|
|
597
|
-
this.error = errorText(cause);
|
|
598
|
-
})
|
|
599
|
-
.finally(() => {
|
|
600
|
-
this.refresh();
|
|
601
|
-
this.ui.tui.requestRender();
|
|
602
|
-
});
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
handleInput(data: string): void {
|
|
606
|
-
this.list.handleInput(data);
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
handleMouse(event: TuiMouseEvent): TuiMouseEventResult | undefined {
|
|
610
|
-
return this.list.handleMouse(event);
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
render(width: number): string[] {
|
|
614
|
-
const { theme } = this.ui;
|
|
615
|
-
return [
|
|
616
|
-
...this.border.render(width),
|
|
617
|
-
` ${theme.bold("Advisor settings")}`,
|
|
618
|
-
...this.view.headline.map((line) => ` ${line}`),
|
|
619
|
-
"",
|
|
620
|
-
...this.list.render(width),
|
|
621
|
-
...(this.error ? [theme.fg("error", ` ✖ ${this.error}`)] : []),
|
|
622
|
-
...this.border.render(width),
|
|
623
|
-
];
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
invalidate(): void {
|
|
627
|
-
this.list.invalidate();
|
|
628
|
-
}
|
|
629
430
|
}
|