@ian-pascoe/pi-advisor 0.4.0 → 0.5.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 +7 -0
- package/package.json +1 -1
- package/src/advisor-command.ts +14 -8
- package/src/advisor-contract.ts +33 -0
- package/src/advisor-menu.ts +629 -0
- package/src/advisor-observer.ts +46 -39
- package/src/advisor-rendering.ts +368 -0
- package/src/advisor-settings.ts +29 -13
- package/src/index.ts +216 -45
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,6 +212,7 @@ 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();
|
|
@@ -171,6 +223,7 @@ export default function advisor(pi: ExtensionAPI): void {
|
|
|
171
223
|
pi.on("before_agent_start", () => observer?.beforeTask());
|
|
172
224
|
pi.on("agent_settled", () => observer?.settled());
|
|
173
225
|
pi.on("session_shutdown", async () => {
|
|
226
|
+
activeMenu?.close();
|
|
174
227
|
generation++;
|
|
175
228
|
observed = undefined;
|
|
176
229
|
unsubscribe?.();
|
|
@@ -181,6 +234,8 @@ export default function advisor(pi: ExtensionAPI): void {
|
|
|
181
234
|
]);
|
|
182
235
|
observer = undefined;
|
|
183
236
|
children.clear();
|
|
237
|
+
publishState();
|
|
238
|
+
footer = undefined;
|
|
184
239
|
});
|
|
185
240
|
|
|
186
241
|
function configureChild(
|
|
@@ -202,6 +257,7 @@ export default function advisor(pi: ExtensionAPI): void {
|
|
|
202
257
|
});
|
|
203
258
|
warn(`Advisor for ${child.agentId} paused: ${message}`);
|
|
204
259
|
},
|
|
260
|
+
onStateChange: publishState,
|
|
205
261
|
onIntervention: (finding) => {
|
|
206
262
|
if (children.get(session) === child)
|
|
207
263
|
pi.appendEntry("pi-advisor-child", { agentId: child.agentId, ...finding });
|
|
@@ -254,6 +310,8 @@ export default function advisor(pi: ExtensionAPI): void {
|
|
|
254
310
|
} catch (cause) {
|
|
255
311
|
error = cause instanceof Error ? cause.message : String(cause);
|
|
256
312
|
}
|
|
313
|
+
// A new child observer reports no state change until it reviews; show it now.
|
|
314
|
+
publishState();
|
|
257
315
|
payload.attach({
|
|
258
316
|
beginTurn: () => child.observer?.beforeTask(),
|
|
259
317
|
finishTurn: async () => {
|
|
@@ -284,6 +342,7 @@ export default function advisor(pi: ExtensionAPI): void {
|
|
|
284
342
|
pi.appendEntry("pi-advisor-status", { state: "paused", error: message });
|
|
285
343
|
warn(`Advisor paused: ${message}`);
|
|
286
344
|
},
|
|
345
|
+
onStateChange: publishState,
|
|
287
346
|
},
|
|
288
347
|
);
|
|
289
348
|
for (const [session, child] of children) configureChild(session, child, settings);
|
|
@@ -298,25 +357,37 @@ export default function advisor(pi: ExtensionAPI): void {
|
|
|
298
357
|
observer = undefined;
|
|
299
358
|
for (const child of children.values()) child.observer = undefined;
|
|
300
359
|
}
|
|
360
|
+
publishState();
|
|
301
361
|
}
|
|
302
362
|
|
|
303
|
-
|
|
363
|
+
/** Current effective settings and live Advisor state, as recorded in status entries. */
|
|
364
|
+
function statusData(subject: AgentSession): AdvisorStatusEntry {
|
|
365
|
+
const resolved = readAdvisorSettings(subject, layers);
|
|
366
|
+
const live = observer?.status;
|
|
367
|
+
return {
|
|
368
|
+
state: live?.state ?? (resolved.settings.enabled ? "armed" : "disabled"),
|
|
369
|
+
...resolved,
|
|
370
|
+
backlog: live?.backlog ?? 0,
|
|
371
|
+
effectiveModel: live?.effectiveModel ?? null,
|
|
372
|
+
effectiveThinkingLevel: live?.effectiveThinkingLevel ?? null,
|
|
373
|
+
usage: live?.usage ?? null,
|
|
374
|
+
cost: live?.cost ?? null,
|
|
375
|
+
unavailableTools: live?.unavailableTools ?? null,
|
|
376
|
+
children: [...children.values()].map((child) => ({
|
|
377
|
+
agentId: child.agentId,
|
|
378
|
+
...child.observer?.status,
|
|
379
|
+
})),
|
|
380
|
+
lastError: live?.lastError ?? null,
|
|
381
|
+
error: error ?? live?.lastError ?? null,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function status(ctx: ExtensionContext, changes: readonly AdvisorAppliedChange[] = []): void {
|
|
304
386
|
try {
|
|
305
387
|
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
|
-
});
|
|
388
|
+
const entry = statusData(observed);
|
|
389
|
+
if (changes.length) entry.changes = [...changes];
|
|
390
|
+
pi.appendEntry("pi-advisor-status", entry);
|
|
320
391
|
} catch (cause) {
|
|
321
392
|
error = cause instanceof Error ? cause.message : String(cause);
|
|
322
393
|
pi.appendEntry("pi-advisor-status", { state: "paused", error, usage: null, cost: null });
|
|
@@ -324,8 +395,116 @@ export default function advisor(pi: ExtensionAPI): void {
|
|
|
324
395
|
if (error) ctx.ui.notify(`Advisor: ${error}`, "error");
|
|
325
396
|
}
|
|
326
397
|
|
|
398
|
+
/** Persist one validated change at its scope and reconfigure; undefined if superseded. */
|
|
399
|
+
async function applyChange(
|
|
400
|
+
ctx: ExtensionContext,
|
|
401
|
+
subject: AgentSession,
|
|
402
|
+
scope: AdvisorSettingScope,
|
|
403
|
+
change: AdvisorChange,
|
|
404
|
+
isCurrent: () => boolean,
|
|
405
|
+
): Promise<AdvisorAppliedChange | undefined> {
|
|
406
|
+
if (scope === "session") {
|
|
407
|
+
const overrides = readAdvisorOverrides(subject.sessionManager);
|
|
408
|
+
if (change.action === "inherit") delete overrides[change.key];
|
|
409
|
+
else Object.assign(overrides, change.patch);
|
|
410
|
+
pi.appendEntry("pi-advisor-settings", { version: 1, overrides });
|
|
411
|
+
} else {
|
|
412
|
+
const updated = await writeAdvisorSettings(subject.settingsManager, scope, change, isCurrent);
|
|
413
|
+
if (!isCurrent() || !updated) return undefined;
|
|
414
|
+
layers[scope] = updated;
|
|
415
|
+
}
|
|
416
|
+
error = undefined;
|
|
417
|
+
await refresh(ctx);
|
|
418
|
+
return { scope, key: change.key, options: change.action === "inherit" ? {} : change.patch };
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/** The settings menu's view of this extension's settings authority. */
|
|
422
|
+
function menuHost(
|
|
423
|
+
ctx: ExtensionContext,
|
|
424
|
+
subject: AgentSession,
|
|
425
|
+
theme: AdvisorRenderTheme,
|
|
426
|
+
applied: AdvisorAppliedChange[],
|
|
427
|
+
): AdvisorMenuHost {
|
|
428
|
+
return {
|
|
429
|
+
view() {
|
|
430
|
+
const data = statusData(subject);
|
|
431
|
+
const authored: Partial<AdvisorScopedOptions> = {
|
|
432
|
+
session: readAdvisorOverrides(subject.sessionManager),
|
|
433
|
+
};
|
|
434
|
+
if (!(layers.project instanceof Error)) authored.project = layers.project;
|
|
435
|
+
if (!(layers.global instanceof Error)) authored.global = layers.global;
|
|
436
|
+
return {
|
|
437
|
+
headline: advisorStatusHeadline(data, theme),
|
|
438
|
+
paused: data.state === "paused",
|
|
439
|
+
scopes: subject.settingsManager.isProjectTrusted()
|
|
440
|
+
? ["session", "project", "global"]
|
|
441
|
+
: ["session", "global"],
|
|
442
|
+
settings: data.settings ?? {},
|
|
443
|
+
sources: data.sources ?? {},
|
|
444
|
+
authored,
|
|
445
|
+
models: ctx.modelRegistry.getAvailable().map((model) => `${model.provider}/${model.id}`),
|
|
446
|
+
tools: pi
|
|
447
|
+
.getAllTools()
|
|
448
|
+
.map((tool) => tool.name)
|
|
449
|
+
.filter((name) => name !== askToolName),
|
|
450
|
+
};
|
|
451
|
+
},
|
|
452
|
+
async apply(scope, change) {
|
|
453
|
+
const stamp = ++generation;
|
|
454
|
+
const result = await applyChange(
|
|
455
|
+
ctx,
|
|
456
|
+
subject,
|
|
457
|
+
scope,
|
|
458
|
+
change,
|
|
459
|
+
() => generation === stamp && observed === subject,
|
|
460
|
+
);
|
|
461
|
+
if (result) applied.push(result);
|
|
462
|
+
},
|
|
463
|
+
async resume() {
|
|
464
|
+
error = undefined;
|
|
465
|
+
await refresh(ctx);
|
|
466
|
+
},
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/** Native settings list in the editor area, as `/settings` does; one status entry on close. */
|
|
471
|
+
async function openMenu(ctx: ExtensionContext, subject: AgentSession): Promise<void> {
|
|
472
|
+
const applied: AdvisorAppliedChange[] = [];
|
|
473
|
+
let discarded = false;
|
|
474
|
+
let menu: AdvisorSettingsMenu | undefined;
|
|
475
|
+
await ctx.ui.custom<void>((tui, theme, keybindings, done) => {
|
|
476
|
+
const opened = new AdvisorSettingsMenu(
|
|
477
|
+
menuHost(ctx, subject, theme, applied),
|
|
478
|
+
{
|
|
479
|
+
tui,
|
|
480
|
+
keybindings,
|
|
481
|
+
theme,
|
|
482
|
+
externalEditorCommand: subject.settingsManager.getExternalEditorCommand(),
|
|
483
|
+
},
|
|
484
|
+
() => done(),
|
|
485
|
+
);
|
|
486
|
+
menu = opened;
|
|
487
|
+
activeMenu = {
|
|
488
|
+
refresh: () => {
|
|
489
|
+
opened.refresh();
|
|
490
|
+
tui.requestRender();
|
|
491
|
+
},
|
|
492
|
+
close: () => {
|
|
493
|
+
discarded = true;
|
|
494
|
+
activeMenu = undefined;
|
|
495
|
+
done();
|
|
496
|
+
},
|
|
497
|
+
};
|
|
498
|
+
return opened;
|
|
499
|
+
});
|
|
500
|
+
activeMenu = undefined;
|
|
501
|
+
// Edits started before closing are still recorded once they settle.
|
|
502
|
+
await menu?.settled();
|
|
503
|
+
if (!discarded && observed === subject && applied.length) status(ctx, applied);
|
|
504
|
+
}
|
|
505
|
+
|
|
327
506
|
pi.registerCommand("advisor", {
|
|
328
|
-
description: "Advisor on, off, inherit,
|
|
507
|
+
description: "Advisor settings menu, or on, off, inherit, set, prompt, and status",
|
|
329
508
|
getArgumentCompletions: completeAdvisorCommandArguments,
|
|
330
509
|
async handler(args, ctx) {
|
|
331
510
|
if (privateSession) {
|
|
@@ -338,10 +517,17 @@ export default function advisor(pi: ExtensionAPI): void {
|
|
|
338
517
|
const subject = observed;
|
|
339
518
|
let stamp = generation;
|
|
340
519
|
const isCurrent = () => generation === stamp && observed === subject;
|
|
520
|
+
let applied: AdvisorAppliedChange | undefined;
|
|
341
521
|
try {
|
|
342
522
|
if (!subject) throw new Error(error ?? "Advisor session is unavailable");
|
|
343
523
|
const command = parseAdvisorCommand(args);
|
|
344
|
-
if (command.action
|
|
524
|
+
if (command.action === "menu" && ctx.mode === "tui") {
|
|
525
|
+
// Opening fails closed to the status entry when settings cannot be read.
|
|
526
|
+
statusData(subject);
|
|
527
|
+
await openMenu(ctx, subject);
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
if (command.action !== "status" && command.action !== "menu") {
|
|
345
531
|
stamp = ++generation;
|
|
346
532
|
let change: AdvisorChange;
|
|
347
533
|
if (command.action === "prompt") {
|
|
@@ -354,32 +540,17 @@ export default function advisor(pi: ExtensionAPI): void {
|
|
|
354
540
|
if (!isCurrent() || edited === undefined) return;
|
|
355
541
|
change = {
|
|
356
542
|
action: "set",
|
|
543
|
+
key: "prompt",
|
|
357
544
|
patch: parseAdvisorOptions({ prompt: edited }, command.scope),
|
|
358
545
|
};
|
|
359
546
|
} 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);
|
|
547
|
+
applied = await applyChange(ctx, subject, command.scope, change, isCurrent);
|
|
377
548
|
}
|
|
378
549
|
} catch (cause) {
|
|
379
550
|
if (!isCurrent()) return;
|
|
380
551
|
error = cause instanceof Error ? cause.message : String(cause);
|
|
381
552
|
}
|
|
382
|
-
if (isCurrent()) status(ctx);
|
|
553
|
+
if (isCurrent()) status(ctx, applied ? [applied] : []);
|
|
383
554
|
},
|
|
384
555
|
});
|
|
385
556
|
}
|