@ian-pascoe/pi-advisor 0.4.0 → 0.6.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 +57 -14
- package/package.json +2 -2
- package/skills/pi-advisor/SKILL.md +7 -3
- package/src/advisor-command.ts +14 -8
- package/src/advisor-contract.ts +99 -0
- package/src/advisor-evidence.ts +389 -0
- package/src/advisor-menu.ts +649 -0
- package/src/advisor-observer.ts +558 -154
- package/src/advisor-rendering.ts +414 -0
- package/src/advisor-session.ts +14 -3
- package/src/advisor-settings.ts +83 -14
- package/src/index.ts +224 -46
package/src/index.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import * as piSdk from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
AgentSession,
|
|
4
|
+
ExtensionAPI,
|
|
5
|
+
ExtensionContext,
|
|
6
|
+
ExtensionUIContext,
|
|
7
|
+
} from "@earendil-works/pi-coding-agent";
|
|
3
8
|
import { isDeepStrictEqual } from "node:util";
|
|
4
|
-
import { Text } from "@earendil-works/pi-tui";
|
|
5
9
|
import { Type } from "typebox";
|
|
6
10
|
import { Value } from "typebox/value";
|
|
7
11
|
import { discoverPiAgentSession } from "@ian-pascoe/pi-utils/pi-agent-session-discovery";
|
|
@@ -13,8 +17,27 @@ import {
|
|
|
13
17
|
writeAdvisorSettings,
|
|
14
18
|
type AdvisorLayers,
|
|
15
19
|
type AdvisorChange,
|
|
20
|
+
type AdvisorAppliedChange,
|
|
16
21
|
type AdvisorConfig,
|
|
22
|
+
type AdvisorSettingScope,
|
|
17
23
|
} from "./advisor-settings.js";
|
|
24
|
+
import {
|
|
25
|
+
AdvisorSettingsMenu,
|
|
26
|
+
type AdvisorMenuHost,
|
|
27
|
+
type AdvisorScopedOptions,
|
|
28
|
+
} from "./advisor-menu.js";
|
|
29
|
+
import {
|
|
30
|
+
advisorFooterText,
|
|
31
|
+
advisorStatusHeadline,
|
|
32
|
+
renderAdvisorAskCall,
|
|
33
|
+
renderAdvisorAskResult,
|
|
34
|
+
renderAdvisorChildEntry,
|
|
35
|
+
renderAdvisorIntervention,
|
|
36
|
+
renderAdvisorStatus,
|
|
37
|
+
type AdvisorActivity,
|
|
38
|
+
type AdvisorRenderTheme,
|
|
39
|
+
type AdvisorStatusEntry,
|
|
40
|
+
} from "./advisor-rendering.js";
|
|
18
41
|
import { completeAdvisorCommandArguments, parseAdvisorCommand } from "./advisor-command.js";
|
|
19
42
|
import { AdvisorObserver } from "./advisor-observer.js";
|
|
20
43
|
import { isAdvisorSession, type AdvisorResourceInputs } from "./advisor-session.js";
|
|
@@ -46,6 +69,9 @@ const childRequestSchema = Type.Object({
|
|
|
46
69
|
/** Review an observed session without changing its tools or standing instructions. */
|
|
47
70
|
export default function advisor(pi: ExtensionAPI): void {
|
|
48
71
|
let warn: (message: string) => void = () => {};
|
|
72
|
+
let footer: ExtensionUIContext | undefined;
|
|
73
|
+
/** The open settings menu, refreshed on state changes and closed on session changes. */
|
|
74
|
+
let activeMenu: { refresh: () => void; close: () => void } | undefined;
|
|
49
75
|
let observed: AgentSession | undefined;
|
|
50
76
|
let error: string | undefined;
|
|
51
77
|
let layers: AdvisorLayers = { global: {}, project: {} };
|
|
@@ -60,10 +86,31 @@ export default function advisor(pi: ExtensionAPI): void {
|
|
|
60
86
|
attachChild,
|
|
61
87
|
);
|
|
62
88
|
|
|
63
|
-
pi.registerEntryRenderer(
|
|
64
|
-
|
|
65
|
-
|
|
89
|
+
pi.registerEntryRenderer("pi-advisor-child", (entry, { expanded }, theme) =>
|
|
90
|
+
renderAdvisorChildEntry(entry.data, expanded, theme),
|
|
91
|
+
);
|
|
92
|
+
pi.registerEntryRenderer("pi-advisor-status", (entry, { expanded }, theme) =>
|
|
93
|
+
renderAdvisorStatus(entry.data, expanded, theme),
|
|
66
94
|
);
|
|
95
|
+
pi.registerMessageRenderer("pi-advisor", (message, options, theme) =>
|
|
96
|
+
renderAdvisorIntervention(message.details, options, theme),
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
/** Show the current Advisor state in the open settings menu and the footer. */
|
|
100
|
+
function publishState(): void {
|
|
101
|
+
activeMenu?.refresh();
|
|
102
|
+
if (!footer) return;
|
|
103
|
+
const root: AdvisorActivity | undefined =
|
|
104
|
+
observer?.status ?? (observed && error ? { state: "paused", backlog: 0 } : undefined);
|
|
105
|
+
const watched = [...children.values()].flatMap((child) =>
|
|
106
|
+
child.observer ? [child.observer.status] : [],
|
|
107
|
+
);
|
|
108
|
+
try {
|
|
109
|
+
footer.setStatus("advisor", advisorFooterText(root, watched, footer.theme));
|
|
110
|
+
} catch {
|
|
111
|
+
// A replaced session's UI is stale; status entries remain authoritative.
|
|
112
|
+
}
|
|
113
|
+
}
|
|
67
114
|
|
|
68
115
|
function setAskToolAvailable(available: boolean): void {
|
|
69
116
|
if (available && !askToolRegistered) {
|
|
@@ -80,6 +127,13 @@ export default function advisor(pi: ExtensionAPI): void {
|
|
|
80
127
|
openWorldHint: false,
|
|
81
128
|
},
|
|
82
129
|
executionMode: "sequential",
|
|
130
|
+
renderCall: (args, theme, context) => renderAdvisorAskCall(args, context.expanded, theme),
|
|
131
|
+
renderResult: (result, options, theme, context) =>
|
|
132
|
+
renderAdvisorAskResult(
|
|
133
|
+
result.content.map((item) => (item.type === "text" ? item.text : "")).join(""),
|
|
134
|
+
{ expanded: options.expanded, isPartial: options.isPartial, isError: context.isError },
|
|
135
|
+
theme,
|
|
136
|
+
),
|
|
83
137
|
execute: async (_id, { message }, signal) => {
|
|
84
138
|
try {
|
|
85
139
|
const current = observer;
|
|
@@ -108,12 +162,8 @@ export default function advisor(pi: ExtensionAPI): void {
|
|
|
108
162
|
);
|
|
109
163
|
}
|
|
110
164
|
|
|
111
|
-
pi.registerEntryRenderer(
|
|
112
|
-
"pi-advisor-status",
|
|
113
|
-
(entry) => new Text(`Advisor\n${JSON.stringify(entry.data, null, 2)}`, 0, 0),
|
|
114
|
-
);
|
|
115
|
-
|
|
116
165
|
pi.on("session_start", async (_event, ctx) => {
|
|
166
|
+
activeMenu?.close();
|
|
117
167
|
const stamp = ++generation;
|
|
118
168
|
const previous = observer;
|
|
119
169
|
observer = undefined;
|
|
@@ -143,6 +193,7 @@ export default function advisor(pi: ExtensionAPI): void {
|
|
|
143
193
|
return;
|
|
144
194
|
}
|
|
145
195
|
const { ui } = ctx;
|
|
196
|
+
footer = ctx.hasUI ? ui : undefined;
|
|
146
197
|
warn = (message) => {
|
|
147
198
|
try {
|
|
148
199
|
ui.notify(message, "warning");
|
|
@@ -161,16 +212,17 @@ export default function advisor(pi: ExtensionAPI): void {
|
|
|
161
212
|
}
|
|
162
213
|
});
|
|
163
214
|
pi.on("session_tree", async (_event, ctx) => {
|
|
215
|
+
activeMenu?.close();
|
|
164
216
|
generation++;
|
|
165
217
|
observer?.reset();
|
|
166
218
|
for (const child of children.values()) child.observer?.reset();
|
|
167
219
|
await refresh(ctx);
|
|
168
220
|
});
|
|
169
|
-
pi.on("session_compact", () => observer?.reset());
|
|
170
221
|
pi.on("model_select", () => observer?.reset());
|
|
171
222
|
pi.on("before_agent_start", () => observer?.beforeTask());
|
|
172
223
|
pi.on("agent_settled", () => observer?.settled());
|
|
173
224
|
pi.on("session_shutdown", async () => {
|
|
225
|
+
activeMenu?.close();
|
|
174
226
|
generation++;
|
|
175
227
|
observed = undefined;
|
|
176
228
|
unsubscribe?.();
|
|
@@ -181,6 +233,8 @@ export default function advisor(pi: ExtensionAPI): void {
|
|
|
181
233
|
]);
|
|
182
234
|
observer = undefined;
|
|
183
235
|
children.clear();
|
|
236
|
+
publishState();
|
|
237
|
+
footer = undefined;
|
|
184
238
|
});
|
|
185
239
|
|
|
186
240
|
function configureChild(
|
|
@@ -202,6 +256,7 @@ export default function advisor(pi: ExtensionAPI): void {
|
|
|
202
256
|
});
|
|
203
257
|
warn(`Advisor for ${child.agentId} paused: ${message}`);
|
|
204
258
|
},
|
|
259
|
+
onStateChange: publishState,
|
|
205
260
|
onIntervention: (finding) => {
|
|
206
261
|
if (children.get(session) === child)
|
|
207
262
|
pi.appendEntry("pi-advisor-child", { agentId: child.agentId, ...finding });
|
|
@@ -254,6 +309,8 @@ export default function advisor(pi: ExtensionAPI): void {
|
|
|
254
309
|
} catch (cause) {
|
|
255
310
|
error = cause instanceof Error ? cause.message : String(cause);
|
|
256
311
|
}
|
|
312
|
+
// A new child observer reports no state change until it reviews; show it now.
|
|
313
|
+
publishState();
|
|
257
314
|
payload.attach({
|
|
258
315
|
beginTurn: () => child.observer?.beforeTask(),
|
|
259
316
|
finishTurn: async () => {
|
|
@@ -284,6 +341,7 @@ export default function advisor(pi: ExtensionAPI): void {
|
|
|
284
341
|
pi.appendEntry("pi-advisor-status", { state: "paused", error: message });
|
|
285
342
|
warn(`Advisor paused: ${message}`);
|
|
286
343
|
},
|
|
344
|
+
onStateChange: publishState,
|
|
287
345
|
},
|
|
288
346
|
);
|
|
289
347
|
for (const [session, child] of children) configureChild(session, child, settings);
|
|
@@ -298,25 +356,45 @@ export default function advisor(pi: ExtensionAPI): void {
|
|
|
298
356
|
observer = undefined;
|
|
299
357
|
for (const child of children.values()) child.observer = undefined;
|
|
300
358
|
}
|
|
359
|
+
publishState();
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** Current effective settings and live Advisor state, as recorded in status entries. */
|
|
363
|
+
function statusData(subject: AgentSession): AdvisorStatusEntry {
|
|
364
|
+
const resolved = readAdvisorSettings(subject, layers);
|
|
365
|
+
const live = observer?.status;
|
|
366
|
+
return {
|
|
367
|
+
state: live?.state ?? (resolved.settings.enabled ? "armed" : "disabled"),
|
|
368
|
+
...resolved,
|
|
369
|
+
backlog: live?.backlog ?? 0,
|
|
370
|
+
effectiveModel: live?.effectiveModel ?? null,
|
|
371
|
+
effectiveThinkingLevel: live?.effectiveThinkingLevel ?? null,
|
|
372
|
+
usage: live?.usage ?? null,
|
|
373
|
+
cost: live?.cost ?? null,
|
|
374
|
+
reviewCost: live?.reviewCost ?? null,
|
|
375
|
+
deferredFindings: live?.deferredFindings ?? 0,
|
|
376
|
+
droppedFindings: live?.droppedFindings ?? {
|
|
377
|
+
overNitCap: 0,
|
|
378
|
+
unsupported: 0,
|
|
379
|
+
superseded: 0,
|
|
380
|
+
invalidReviews: 0,
|
|
381
|
+
},
|
|
382
|
+
unavailableTools: live?.unavailableTools ?? null,
|
|
383
|
+
children: [...children.values()].map((child) => ({
|
|
384
|
+
agentId: child.agentId,
|
|
385
|
+
...child.observer?.status,
|
|
386
|
+
})),
|
|
387
|
+
lastError: live?.lastError ?? null,
|
|
388
|
+
error: error ?? live?.lastError ?? null,
|
|
389
|
+
};
|
|
301
390
|
}
|
|
302
391
|
|
|
303
|
-
function status(ctx: ExtensionContext): void {
|
|
392
|
+
function status(ctx: ExtensionContext, changes: readonly AdvisorAppliedChange[] = []): void {
|
|
304
393
|
try {
|
|
305
394
|
if (!observed) throw new Error(error ?? "Advisor session is unavailable");
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
...resolved,
|
|
310
|
-
backlog: 0,
|
|
311
|
-
usage: null,
|
|
312
|
-
cost: null,
|
|
313
|
-
...observer?.status,
|
|
314
|
-
children: [...children.values()].map((child) => ({
|
|
315
|
-
agentId: child.agentId,
|
|
316
|
-
...child.observer?.status,
|
|
317
|
-
})),
|
|
318
|
-
error: error ?? observer?.status.lastError ?? null,
|
|
319
|
-
});
|
|
395
|
+
const entry = statusData(observed);
|
|
396
|
+
if (changes.length) entry.changes = [...changes];
|
|
397
|
+
pi.appendEntry("pi-advisor-status", entry);
|
|
320
398
|
} catch (cause) {
|
|
321
399
|
error = cause instanceof Error ? cause.message : String(cause);
|
|
322
400
|
pi.appendEntry("pi-advisor-status", { state: "paused", error, usage: null, cost: null });
|
|
@@ -324,8 +402,116 @@ export default function advisor(pi: ExtensionAPI): void {
|
|
|
324
402
|
if (error) ctx.ui.notify(`Advisor: ${error}`, "error");
|
|
325
403
|
}
|
|
326
404
|
|
|
405
|
+
/** Persist one validated change at its scope and reconfigure; undefined if superseded. */
|
|
406
|
+
async function applyChange(
|
|
407
|
+
ctx: ExtensionContext,
|
|
408
|
+
subject: AgentSession,
|
|
409
|
+
scope: AdvisorSettingScope,
|
|
410
|
+
change: AdvisorChange,
|
|
411
|
+
isCurrent: () => boolean,
|
|
412
|
+
): Promise<AdvisorAppliedChange | undefined> {
|
|
413
|
+
if (scope === "session") {
|
|
414
|
+
const overrides = readAdvisorOverrides(subject.sessionManager);
|
|
415
|
+
if (change.action === "inherit") delete overrides[change.key];
|
|
416
|
+
else Object.assign(overrides, change.patch);
|
|
417
|
+
pi.appendEntry("pi-advisor-settings", { version: 1, overrides });
|
|
418
|
+
} else {
|
|
419
|
+
const updated = await writeAdvisorSettings(subject.settingsManager, scope, change, isCurrent);
|
|
420
|
+
if (!isCurrent() || !updated) return undefined;
|
|
421
|
+
layers[scope] = updated;
|
|
422
|
+
}
|
|
423
|
+
error = undefined;
|
|
424
|
+
await refresh(ctx);
|
|
425
|
+
return { scope, key: change.key, options: change.action === "inherit" ? {} : change.patch };
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/** The settings menu's view of this extension's settings authority. */
|
|
429
|
+
function menuHost(
|
|
430
|
+
ctx: ExtensionContext,
|
|
431
|
+
subject: AgentSession,
|
|
432
|
+
theme: AdvisorRenderTheme,
|
|
433
|
+
applied: AdvisorAppliedChange[],
|
|
434
|
+
): AdvisorMenuHost {
|
|
435
|
+
return {
|
|
436
|
+
view() {
|
|
437
|
+
const data = statusData(subject);
|
|
438
|
+
const authored: Partial<AdvisorScopedOptions> = {
|
|
439
|
+
session: readAdvisorOverrides(subject.sessionManager),
|
|
440
|
+
};
|
|
441
|
+
if (!(layers.project instanceof Error)) authored.project = layers.project;
|
|
442
|
+
if (!(layers.global instanceof Error)) authored.global = layers.global;
|
|
443
|
+
return {
|
|
444
|
+
headline: advisorStatusHeadline(data, theme),
|
|
445
|
+
paused: data.state === "paused",
|
|
446
|
+
scopes: subject.settingsManager.isProjectTrusted()
|
|
447
|
+
? ["session", "project", "global"]
|
|
448
|
+
: ["session", "global"],
|
|
449
|
+
settings: data.settings ?? {},
|
|
450
|
+
sources: data.sources ?? {},
|
|
451
|
+
authored,
|
|
452
|
+
models: ctx.modelRegistry.getAvailable().map((model) => `${model.provider}/${model.id}`),
|
|
453
|
+
tools: pi
|
|
454
|
+
.getAllTools()
|
|
455
|
+
.map((tool) => tool.name)
|
|
456
|
+
.filter((name) => name !== askToolName),
|
|
457
|
+
};
|
|
458
|
+
},
|
|
459
|
+
async apply(scope, change) {
|
|
460
|
+
const stamp = ++generation;
|
|
461
|
+
const result = await applyChange(
|
|
462
|
+
ctx,
|
|
463
|
+
subject,
|
|
464
|
+
scope,
|
|
465
|
+
change,
|
|
466
|
+
() => generation === stamp && observed === subject,
|
|
467
|
+
);
|
|
468
|
+
if (result) applied.push(result);
|
|
469
|
+
},
|
|
470
|
+
async resume() {
|
|
471
|
+
error = undefined;
|
|
472
|
+
await refresh(ctx);
|
|
473
|
+
},
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/** Native settings list in the editor area, as `/settings` does; one status entry on close. */
|
|
478
|
+
async function openMenu(ctx: ExtensionContext, subject: AgentSession): Promise<void> {
|
|
479
|
+
const applied: AdvisorAppliedChange[] = [];
|
|
480
|
+
let discarded = false;
|
|
481
|
+
let menu: AdvisorSettingsMenu | undefined;
|
|
482
|
+
await ctx.ui.custom<void>((tui, theme, keybindings, done) => {
|
|
483
|
+
const opened = new AdvisorSettingsMenu(
|
|
484
|
+
menuHost(ctx, subject, theme, applied),
|
|
485
|
+
{
|
|
486
|
+
tui,
|
|
487
|
+
keybindings,
|
|
488
|
+
theme,
|
|
489
|
+
externalEditorCommand: subject.settingsManager.getExternalEditorCommand(),
|
|
490
|
+
},
|
|
491
|
+
() => done(),
|
|
492
|
+
);
|
|
493
|
+
menu = opened;
|
|
494
|
+
activeMenu = {
|
|
495
|
+
refresh: () => {
|
|
496
|
+
opened.refresh();
|
|
497
|
+
tui.requestRender();
|
|
498
|
+
},
|
|
499
|
+
close: () => {
|
|
500
|
+
discarded = true;
|
|
501
|
+
activeMenu = undefined;
|
|
502
|
+
done();
|
|
503
|
+
},
|
|
504
|
+
};
|
|
505
|
+
return opened;
|
|
506
|
+
});
|
|
507
|
+
activeMenu = undefined;
|
|
508
|
+
// Edits started before closing are still recorded once they settle.
|
|
509
|
+
await menu?.settled();
|
|
510
|
+
if (!discarded && observed === subject && applied.length) status(ctx, applied);
|
|
511
|
+
}
|
|
512
|
+
|
|
327
513
|
pi.registerCommand("advisor", {
|
|
328
|
-
description: "Advisor on, off, inherit,
|
|
514
|
+
description: "Advisor settings menu, or on, off, inherit, set, prompt, and status",
|
|
329
515
|
getArgumentCompletions: completeAdvisorCommandArguments,
|
|
330
516
|
async handler(args, ctx) {
|
|
331
517
|
if (privateSession) {
|
|
@@ -338,10 +524,17 @@ export default function advisor(pi: ExtensionAPI): void {
|
|
|
338
524
|
const subject = observed;
|
|
339
525
|
let stamp = generation;
|
|
340
526
|
const isCurrent = () => generation === stamp && observed === subject;
|
|
527
|
+
let applied: AdvisorAppliedChange | undefined;
|
|
341
528
|
try {
|
|
342
529
|
if (!subject) throw new Error(error ?? "Advisor session is unavailable");
|
|
343
530
|
const command = parseAdvisorCommand(args);
|
|
344
|
-
if (command.action
|
|
531
|
+
if (command.action === "menu" && ctx.mode === "tui") {
|
|
532
|
+
// Opening fails closed to the status entry when settings cannot be read.
|
|
533
|
+
statusData(subject);
|
|
534
|
+
await openMenu(ctx, subject);
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
if (command.action !== "status" && command.action !== "menu") {
|
|
345
538
|
stamp = ++generation;
|
|
346
539
|
let change: AdvisorChange;
|
|
347
540
|
if (command.action === "prompt") {
|
|
@@ -354,32 +547,17 @@ export default function advisor(pi: ExtensionAPI): void {
|
|
|
354
547
|
if (!isCurrent() || edited === undefined) return;
|
|
355
548
|
change = {
|
|
356
549
|
action: "set",
|
|
550
|
+
key: "prompt",
|
|
357
551
|
patch: parseAdvisorOptions({ prompt: edited }, command.scope),
|
|
358
552
|
};
|
|
359
553
|
} else change = command;
|
|
360
|
-
|
|
361
|
-
const overrides = readAdvisorOverrides(subject.sessionManager);
|
|
362
|
-
if (change.action === "inherit") delete overrides[change.key];
|
|
363
|
-
else Object.assign(overrides, change.patch);
|
|
364
|
-
pi.appendEntry("pi-advisor-settings", { version: 1, overrides });
|
|
365
|
-
} else {
|
|
366
|
-
const updated = await writeAdvisorSettings(
|
|
367
|
-
subject.settingsManager,
|
|
368
|
-
command.scope,
|
|
369
|
-
change,
|
|
370
|
-
isCurrent,
|
|
371
|
-
);
|
|
372
|
-
if (!isCurrent() || !updated) return;
|
|
373
|
-
layers[command.scope] = updated;
|
|
374
|
-
}
|
|
375
|
-
error = undefined;
|
|
376
|
-
await refresh(ctx);
|
|
554
|
+
applied = await applyChange(ctx, subject, command.scope, change, isCurrent);
|
|
377
555
|
}
|
|
378
556
|
} catch (cause) {
|
|
379
557
|
if (!isCurrent()) return;
|
|
380
558
|
error = cause instanceof Error ? cause.message : String(cause);
|
|
381
559
|
}
|
|
382
|
-
if (isCurrent()) status(ctx);
|
|
560
|
+
if (isCurrent()) status(ctx, applied ? [applied] : []);
|
|
383
561
|
},
|
|
384
562
|
});
|
|
385
563
|
}
|