@ian-pascoe/pi-advisor 0.3.5 → 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 +12 -3
- package/package.json +6 -6
- 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 +52 -39
- package/src/advisor-rendering.ts +368 -0
- package/src/advisor-session.ts +12 -11
- package/src/advisor-settings.ts +29 -13
- package/src/index.ts +222 -55
- package/src/advisor-runtime.ts +0 -97
package/src/advisor-settings.ts
CHANGED
|
@@ -7,7 +7,8 @@ import { Type, type Static } from "typebox";
|
|
|
7
7
|
import { Value } from "typebox/value";
|
|
8
8
|
|
|
9
9
|
const positiveInteger = Type.Integer({ minimum: 1, maximum: Number.MAX_SAFE_INTEGER });
|
|
10
|
-
|
|
10
|
+
/** Authored Advisor options; every key is optional so absent values inherit. */
|
|
11
|
+
export const advisorOptionsSchema = Type.Object(
|
|
11
12
|
{
|
|
12
13
|
enabled: Type.Optional(Type.Boolean()),
|
|
13
14
|
includeSubagents: Type.Optional(Type.Boolean()),
|
|
@@ -36,15 +37,26 @@ const optionsSchema = Type.Object(
|
|
|
36
37
|
{ additionalProperties: false },
|
|
37
38
|
);
|
|
38
39
|
|
|
39
|
-
export const advisorOptionKeys = Object.keys(
|
|
40
|
+
export const advisorOptionKeys = Object.keys(advisorOptionsSchema.properties).map(advisorOptionKey);
|
|
40
41
|
|
|
41
42
|
/** Authored options; absent values inherit rather than disabling their setting. */
|
|
42
|
-
export type AdvisorOptions = Static<typeof
|
|
43
|
+
export type AdvisorOptions = Static<typeof advisorOptionsSchema>;
|
|
43
44
|
/** Fully defaulted options; absent model/thinking follows the Observed Agent. */
|
|
44
45
|
export type AdvisorConfig = Required<Omit<AdvisorOptions, "model" | "thinkingLevel">> &
|
|
45
46
|
Pick<AdvisorOptions, "model" | "thinkingLevel">;
|
|
47
|
+
/** Native settings/session layer an authored change is written to. */
|
|
48
|
+
export const advisorSettingScopeSchema = Type.Union([
|
|
49
|
+
Type.Literal("session"),
|
|
50
|
+
Type.Literal("global"),
|
|
51
|
+
Type.Literal("project"),
|
|
52
|
+
]);
|
|
53
|
+
export type AdvisorSettingScope = Static<typeof advisorSettingScopeSchema>;
|
|
46
54
|
/** Native settings/session layer supplying an effective option. */
|
|
47
|
-
export
|
|
55
|
+
export const advisorSettingSourceSchema = Type.Union([
|
|
56
|
+
Type.Literal("default"),
|
|
57
|
+
advisorSettingScopeSchema,
|
|
58
|
+
]);
|
|
59
|
+
export type AdvisorSettingSource = Static<typeof advisorSettingSourceSchema>;
|
|
48
60
|
|
|
49
61
|
const defaults = {
|
|
50
62
|
enabled: false,
|
|
@@ -62,7 +74,7 @@ const defaults = {
|
|
|
62
74
|
const sessionSchema = Type.Object(
|
|
63
75
|
{
|
|
64
76
|
version: Type.Literal(1),
|
|
65
|
-
overrides:
|
|
77
|
+
overrides: advisorOptionsSchema,
|
|
66
78
|
},
|
|
67
79
|
{ additionalProperties: false },
|
|
68
80
|
);
|
|
@@ -70,8 +82,8 @@ const sessionSchema = Type.Object(
|
|
|
70
82
|
// oxlint-disable-next-line anti-slop/no-unknown-parameters -- SAFETY: Native settings contain arbitrary authored JSON; this schema validates it before use. SDK command tests exercise the boundary.
|
|
71
83
|
export function parseAdvisorOptions(value: unknown, source: AdvisorSettingSource): AdvisorOptions {
|
|
72
84
|
if (value === undefined) return {};
|
|
73
|
-
if (!Value.Check(
|
|
74
|
-
const issue = Value.Errors(
|
|
85
|
+
if (!Value.Check(advisorOptionsSchema, value)) {
|
|
86
|
+
const issue = Value.Errors(advisorOptionsSchema, value)[0];
|
|
75
87
|
throw new Error(
|
|
76
88
|
`Invalid ${source} Advisor settings${issue?.instancePath ?? ""}: ${issue?.message ?? "invalid options"}`,
|
|
77
89
|
);
|
|
@@ -81,7 +93,7 @@ export function parseAdvisorOptions(value: unknown, source: AdvisorSettingSource
|
|
|
81
93
|
|
|
82
94
|
/** Reject inherited or unknown property names before applying an authored change. */
|
|
83
95
|
export function advisorOptionKey(input: string): keyof AdvisorOptions {
|
|
84
|
-
if (!Value.Check(Type.KeyOf(
|
|
96
|
+
if (!Value.Check(Type.KeyOf(advisorOptionsSchema), input))
|
|
85
97
|
throw new Error(`Unknown Advisor option: ${input}`);
|
|
86
98
|
return input;
|
|
87
99
|
}
|
|
@@ -100,8 +112,15 @@ export function readAdvisorOverrides(manager: Pick<SessionManager, "getBranch">)
|
|
|
100
112
|
export type AdvisorLayers = { global: AdvisorOptions | Error; project: AdvisorOptions | Error };
|
|
101
113
|
/** A validated authored mutation, independent of its persistence scope. */
|
|
102
114
|
export type AdvisorChange =
|
|
103
|
-
| { action: "set"; patch: AdvisorOptions }
|
|
115
|
+
| { action: "set"; key: keyof AdvisorOptions; patch: AdvisorOptions }
|
|
104
116
|
| { action: "inherit"; key: keyof AdvisorOptions };
|
|
117
|
+
/** An applied change as recorded for confirmation; `options` is empty when the key inherits. */
|
|
118
|
+
export const advisorAppliedChangeSchema = Type.Object({
|
|
119
|
+
scope: advisorSettingScopeSchema,
|
|
120
|
+
key: Type.KeyOf(advisorOptionsSchema),
|
|
121
|
+
options: advisorOptionsSchema,
|
|
122
|
+
});
|
|
123
|
+
export type AdvisorAppliedChange = Static<typeof advisorAppliedChangeSchema>;
|
|
105
124
|
|
|
106
125
|
/** Read Pi's stored layers, preserving configuration failures until corrected. */
|
|
107
126
|
export function readAdvisorLayers(manager: SettingsManager): AdvisorLayers {
|
|
@@ -142,10 +161,7 @@ export function readAdvisorSettings(
|
|
|
142
161
|
];
|
|
143
162
|
const settings: AdvisorConfig = structuredClone(defaults);
|
|
144
163
|
const sources: Record<string, AdvisorSettingSource> = Object.fromEntries(
|
|
145
|
-
|
|
146
|
-
key,
|
|
147
|
-
"default",
|
|
148
|
-
]),
|
|
164
|
+
advisorOptionKeys.map((key): [string, AdvisorSettingSource] => [key, "default"]),
|
|
149
165
|
);
|
|
150
166
|
for (const [source, layer] of layers) {
|
|
151
167
|
Object.assign(settings, layer);
|
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,12 +17,30 @@ 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";
|
|
21
|
-
import { advisorRuntimeWarning } from "./advisor-runtime.js";
|
|
22
44
|
import { TROUBLESHOOTING_HINT } from "./troubleshooting-skill.js";
|
|
23
45
|
|
|
24
46
|
interface WatchedChild {
|
|
@@ -46,9 +68,10 @@ const childRequestSchema = Type.Object({
|
|
|
46
68
|
|
|
47
69
|
/** Review an observed session without changing its tools or standing instructions. */
|
|
48
70
|
export default function advisor(pi: ExtensionAPI): void {
|
|
49
|
-
// Capability checks, not version pins: an unmet requirement warns and leaves the Advisor unavailable.
|
|
50
|
-
const unsupported = advisorRuntimeWarning();
|
|
51
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;
|
|
52
75
|
let observed: AgentSession | undefined;
|
|
53
76
|
let error: string | undefined;
|
|
54
77
|
let layers: AdvisorLayers = { global: {}, project: {} };
|
|
@@ -63,11 +86,32 @@ export default function advisor(pi: ExtensionAPI): void {
|
|
|
63
86
|
attachChild,
|
|
64
87
|
);
|
|
65
88
|
|
|
66
|
-
pi.registerEntryRenderer(
|
|
67
|
-
|
|
68
|
-
|
|
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),
|
|
94
|
+
);
|
|
95
|
+
pi.registerMessageRenderer("pi-advisor", (message, options, theme) =>
|
|
96
|
+
renderAdvisorIntervention(message.details, options, theme),
|
|
69
97
|
);
|
|
70
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
|
+
}
|
|
114
|
+
|
|
71
115
|
function setAskToolAvailable(available: boolean): void {
|
|
72
116
|
if (available && !askToolRegistered) {
|
|
73
117
|
pi.registerTool({
|
|
@@ -76,7 +120,20 @@ export default function advisor(pi: ExtensionAPI): void {
|
|
|
76
120
|
description:
|
|
77
121
|
"Ask the enabled Advisor for analysis or a second opinion. Waits for its answer; does not delegate implementation.",
|
|
78
122
|
parameters: askToolParameters,
|
|
123
|
+
annotations: {
|
|
124
|
+
readOnlyHint: true,
|
|
125
|
+
destructiveHint: false,
|
|
126
|
+
idempotentHint: true,
|
|
127
|
+
openWorldHint: false,
|
|
128
|
+
},
|
|
79
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
|
+
),
|
|
80
137
|
execute: async (_id, { message }, signal) => {
|
|
81
138
|
try {
|
|
82
139
|
const current = observer;
|
|
@@ -105,12 +162,8 @@ export default function advisor(pi: ExtensionAPI): void {
|
|
|
105
162
|
);
|
|
106
163
|
}
|
|
107
164
|
|
|
108
|
-
pi.registerEntryRenderer(
|
|
109
|
-
"pi-advisor-status",
|
|
110
|
-
(entry) => new Text(`Advisor\n${JSON.stringify(entry.data, null, 2)}`, 0, 0),
|
|
111
|
-
);
|
|
112
|
-
|
|
113
165
|
pi.on("session_start", async (_event, ctx) => {
|
|
166
|
+
activeMenu?.close();
|
|
114
167
|
const stamp = ++generation;
|
|
115
168
|
const previous = observer;
|
|
116
169
|
observer = undefined;
|
|
@@ -140,6 +193,7 @@ export default function advisor(pi: ExtensionAPI): void {
|
|
|
140
193
|
return;
|
|
141
194
|
}
|
|
142
195
|
const { ui } = ctx;
|
|
196
|
+
footer = ctx.hasUI ? ui : undefined;
|
|
143
197
|
warn = (message) => {
|
|
144
198
|
try {
|
|
145
199
|
ui.notify(message, "warning");
|
|
@@ -147,12 +201,6 @@ export default function advisor(pi: ExtensionAPI): void {
|
|
|
147
201
|
// A replaced session's UI is stale; the native status entry remains the durable record.
|
|
148
202
|
}
|
|
149
203
|
};
|
|
150
|
-
if (unsupported) {
|
|
151
|
-
error = unsupported;
|
|
152
|
-
setAskToolAvailable(false);
|
|
153
|
-
warn(unsupported);
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
204
|
const found = discoverPiAgentSession(pi, piSdk.AgentSession);
|
|
157
205
|
if (found.ok) {
|
|
158
206
|
observed = found.session;
|
|
@@ -164,6 +212,7 @@ export default function advisor(pi: ExtensionAPI): void {
|
|
|
164
212
|
}
|
|
165
213
|
});
|
|
166
214
|
pi.on("session_tree", async (_event, ctx) => {
|
|
215
|
+
activeMenu?.close();
|
|
167
216
|
generation++;
|
|
168
217
|
observer?.reset();
|
|
169
218
|
for (const child of children.values()) child.observer?.reset();
|
|
@@ -174,6 +223,7 @@ export default function advisor(pi: ExtensionAPI): void {
|
|
|
174
223
|
pi.on("before_agent_start", () => observer?.beforeTask());
|
|
175
224
|
pi.on("agent_settled", () => observer?.settled());
|
|
176
225
|
pi.on("session_shutdown", async () => {
|
|
226
|
+
activeMenu?.close();
|
|
177
227
|
generation++;
|
|
178
228
|
observed = undefined;
|
|
179
229
|
unsubscribe?.();
|
|
@@ -184,6 +234,8 @@ export default function advisor(pi: ExtensionAPI): void {
|
|
|
184
234
|
]);
|
|
185
235
|
observer = undefined;
|
|
186
236
|
children.clear();
|
|
237
|
+
publishState();
|
|
238
|
+
footer = undefined;
|
|
187
239
|
});
|
|
188
240
|
|
|
189
241
|
function configureChild(
|
|
@@ -205,6 +257,7 @@ export default function advisor(pi: ExtensionAPI): void {
|
|
|
205
257
|
});
|
|
206
258
|
warn(`Advisor for ${child.agentId} paused: ${message}`);
|
|
207
259
|
},
|
|
260
|
+
onStateChange: publishState,
|
|
208
261
|
onIntervention: (finding) => {
|
|
209
262
|
if (children.get(session) === child)
|
|
210
263
|
pi.appendEntry("pi-advisor-child", { agentId: child.agentId, ...finding });
|
|
@@ -215,7 +268,6 @@ export default function advisor(pi: ExtensionAPI): void {
|
|
|
215
268
|
// oxlint-disable-next-line anti-slop/no-unknown-parameters -- SAFETY: The native owner bus is a boundary; validate identity, SDK instance, and exact resource metadata before attachment.
|
|
216
269
|
function attachChild(payload: unknown): void {
|
|
217
270
|
if (
|
|
218
|
-
unsupported ||
|
|
219
271
|
!Value.Check(childRequestSchema, payload) ||
|
|
220
272
|
!(payload.session instanceof piSdk.AgentSession)
|
|
221
273
|
)
|
|
@@ -258,6 +310,8 @@ export default function advisor(pi: ExtensionAPI): void {
|
|
|
258
310
|
} catch (cause) {
|
|
259
311
|
error = cause instanceof Error ? cause.message : String(cause);
|
|
260
312
|
}
|
|
313
|
+
// A new child observer reports no state change until it reviews; show it now.
|
|
314
|
+
publishState();
|
|
261
315
|
payload.attach({
|
|
262
316
|
beginTurn: () => child.observer?.beforeTask(),
|
|
263
317
|
finishTurn: async () => {
|
|
@@ -288,6 +342,7 @@ export default function advisor(pi: ExtensionAPI): void {
|
|
|
288
342
|
pi.appendEntry("pi-advisor-status", { state: "paused", error: message });
|
|
289
343
|
warn(`Advisor paused: ${message}`);
|
|
290
344
|
},
|
|
345
|
+
onStateChange: publishState,
|
|
291
346
|
},
|
|
292
347
|
);
|
|
293
348
|
for (const [session, child] of children) configureChild(session, child, settings);
|
|
@@ -302,25 +357,37 @@ export default function advisor(pi: ExtensionAPI): void {
|
|
|
302
357
|
observer = undefined;
|
|
303
358
|
for (const child of children.values()) child.observer = undefined;
|
|
304
359
|
}
|
|
360
|
+
publishState();
|
|
361
|
+
}
|
|
362
|
+
|
|
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
|
+
};
|
|
305
383
|
}
|
|
306
384
|
|
|
307
|
-
function status(ctx: ExtensionContext): void {
|
|
385
|
+
function status(ctx: ExtensionContext, changes: readonly AdvisorAppliedChange[] = []): void {
|
|
308
386
|
try {
|
|
309
387
|
if (!observed) throw new Error(error ?? "Advisor session is unavailable");
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
...resolved,
|
|
314
|
-
backlog: 0,
|
|
315
|
-
usage: null,
|
|
316
|
-
cost: null,
|
|
317
|
-
...observer?.status,
|
|
318
|
-
children: [...children.values()].map((child) => ({
|
|
319
|
-
agentId: child.agentId,
|
|
320
|
-
...child.observer?.status,
|
|
321
|
-
})),
|
|
322
|
-
error: error ?? observer?.status.lastError ?? null,
|
|
323
|
-
});
|
|
388
|
+
const entry = statusData(observed);
|
|
389
|
+
if (changes.length) entry.changes = [...changes];
|
|
390
|
+
pi.appendEntry("pi-advisor-status", entry);
|
|
324
391
|
} catch (cause) {
|
|
325
392
|
error = cause instanceof Error ? cause.message : String(cause);
|
|
326
393
|
pi.appendEntry("pi-advisor-status", { state: "paused", error, usage: null, cost: null });
|
|
@@ -328,8 +395,116 @@ export default function advisor(pi: ExtensionAPI): void {
|
|
|
328
395
|
if (error) ctx.ui.notify(`Advisor: ${error}`, "error");
|
|
329
396
|
}
|
|
330
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
|
+
|
|
331
506
|
pi.registerCommand("advisor", {
|
|
332
|
-
description: "Advisor on, off, inherit,
|
|
507
|
+
description: "Advisor settings menu, or on, off, inherit, set, prompt, and status",
|
|
333
508
|
getArgumentCompletions: completeAdvisorCommandArguments,
|
|
334
509
|
async handler(args, ctx) {
|
|
335
510
|
if (privateSession) {
|
|
@@ -342,10 +517,17 @@ export default function advisor(pi: ExtensionAPI): void {
|
|
|
342
517
|
const subject = observed;
|
|
343
518
|
let stamp = generation;
|
|
344
519
|
const isCurrent = () => generation === stamp && observed === subject;
|
|
520
|
+
let applied: AdvisorAppliedChange | undefined;
|
|
345
521
|
try {
|
|
346
522
|
if (!subject) throw new Error(error ?? "Advisor session is unavailable");
|
|
347
523
|
const command = parseAdvisorCommand(args);
|
|
348
|
-
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") {
|
|
349
531
|
stamp = ++generation;
|
|
350
532
|
let change: AdvisorChange;
|
|
351
533
|
if (command.action === "prompt") {
|
|
@@ -358,32 +540,17 @@ export default function advisor(pi: ExtensionAPI): void {
|
|
|
358
540
|
if (!isCurrent() || edited === undefined) return;
|
|
359
541
|
change = {
|
|
360
542
|
action: "set",
|
|
543
|
+
key: "prompt",
|
|
361
544
|
patch: parseAdvisorOptions({ prompt: edited }, command.scope),
|
|
362
545
|
};
|
|
363
546
|
} else change = command;
|
|
364
|
-
|
|
365
|
-
const overrides = readAdvisorOverrides(subject.sessionManager);
|
|
366
|
-
if (change.action === "inherit") delete overrides[change.key];
|
|
367
|
-
else Object.assign(overrides, change.patch);
|
|
368
|
-
pi.appendEntry("pi-advisor-settings", { version: 1, overrides });
|
|
369
|
-
} else {
|
|
370
|
-
const updated = await writeAdvisorSettings(
|
|
371
|
-
subject.settingsManager,
|
|
372
|
-
command.scope,
|
|
373
|
-
change,
|
|
374
|
-
isCurrent,
|
|
375
|
-
);
|
|
376
|
-
if (!isCurrent() || !updated) return;
|
|
377
|
-
layers[command.scope] = updated;
|
|
378
|
-
}
|
|
379
|
-
error = undefined;
|
|
380
|
-
await refresh(ctx);
|
|
547
|
+
applied = await applyChange(ctx, subject, command.scope, change, isCurrent);
|
|
381
548
|
}
|
|
382
549
|
} catch (cause) {
|
|
383
550
|
if (!isCurrent()) return;
|
|
384
551
|
error = cause instanceof Error ? cause.message : String(cause);
|
|
385
552
|
}
|
|
386
|
-
if (isCurrent()) status(ctx);
|
|
553
|
+
if (isCurrent()) status(ctx, applied ? [applied] : []);
|
|
387
554
|
},
|
|
388
555
|
});
|
|
389
556
|
}
|
package/src/advisor-runtime.ts
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import * as piAi from "@earendil-works/pi-ai";
|
|
2
|
-
import * as piSdk from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import { Type } from "typebox";
|
|
4
|
-
import { Value } from "typebox/value";
|
|
5
|
-
|
|
6
|
-
const callable = Type.Function([], Type.Unknown());
|
|
7
|
-
|
|
8
|
-
/** True when a native SDK member is present and callable. */
|
|
9
|
-
export function isCallable<T>(value: T): boolean {
|
|
10
|
-
return Value.Check(callable, value);
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/** True when `owner` exposes `name` as a callable, including module getters and inherited methods. */
|
|
14
|
-
function hasMember<T extends object>(owner: T | undefined, name: string): boolean {
|
|
15
|
-
if (!owner) return false;
|
|
16
|
-
if (Value.Check(Type.Object({ [name]: callable }), owner)) return true;
|
|
17
|
-
// TypeBox does not treat classes as objects, so read class statics as own data properties.
|
|
18
|
-
return isCallable(Object.getOwnPropertyDescriptor(owner, name)?.value);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function missingMembers<T extends object>(
|
|
22
|
-
label: string,
|
|
23
|
-
owner: T | undefined,
|
|
24
|
-
names: readonly string[],
|
|
25
|
-
): string[] {
|
|
26
|
-
return names.filter((name) => !hasMember(owner, name)).map((name) => `${label}${name}`);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Pi capabilities the Advisor needs to observe a session and recreate a private native one.
|
|
31
|
-
* Modules are read through namespaces so a missing export is reported here instead of failing
|
|
32
|
-
* extension load. Built-in extension factories are checked only when the observed session uses them.
|
|
33
|
-
*/
|
|
34
|
-
export function advisorRuntimeIssues(
|
|
35
|
-
ai: Partial<typeof piAi> = piAi,
|
|
36
|
-
sdk: Partial<typeof piSdk> = piSdk,
|
|
37
|
-
): string[] {
|
|
38
|
-
return [
|
|
39
|
-
...missingMembers("pi-ai ", ai, [
|
|
40
|
-
"InMemoryCredentialStore",
|
|
41
|
-
"contentText",
|
|
42
|
-
"getCurrentSystemPrompt",
|
|
43
|
-
"getCurrentTools",
|
|
44
|
-
"toToolDeclaration",
|
|
45
|
-
]),
|
|
46
|
-
...missingMembers("", sdk, [
|
|
47
|
-
"AgentSession",
|
|
48
|
-
"AgentSessionRuntime",
|
|
49
|
-
"DefaultResourceLoader",
|
|
50
|
-
"ModelRuntime",
|
|
51
|
-
"SessionManager",
|
|
52
|
-
"SettingsManager",
|
|
53
|
-
"convertToLlm",
|
|
54
|
-
"createAgentSessionFromServices",
|
|
55
|
-
"createAgentSessionRuntime",
|
|
56
|
-
"createAgentSessionServices",
|
|
57
|
-
"defineTool",
|
|
58
|
-
]),
|
|
59
|
-
...missingMembers("SessionManager.", sdk.SessionManager, ["create"]),
|
|
60
|
-
...missingMembers("SettingsManager.", sdk.SettingsManager, ["fromStorage"]),
|
|
61
|
-
...missingMembers("SettingsManager#", sdk.SettingsManager?.prototype, [
|
|
62
|
-
"applyOverrides",
|
|
63
|
-
"getGlobalSettings",
|
|
64
|
-
"getProjectSettings",
|
|
65
|
-
"isProjectTrusted",
|
|
66
|
-
]),
|
|
67
|
-
...missingMembers("ModelRuntime.", sdk.ModelRuntime, ["create"]),
|
|
68
|
-
...missingMembers("ModelRuntime#", sdk.ModelRuntime?.prototype, [
|
|
69
|
-
"getAuth",
|
|
70
|
-
"getModel",
|
|
71
|
-
"getRegisteredProviderIds",
|
|
72
|
-
"isUsingOAuth",
|
|
73
|
-
"listCredentials",
|
|
74
|
-
"setRuntimeApiKey",
|
|
75
|
-
]),
|
|
76
|
-
...missingMembers("AgentSession#", sdk.AgentSession?.prototype, [
|
|
77
|
-
"abort",
|
|
78
|
-
"bindExtensions",
|
|
79
|
-
"getActiveToolNames",
|
|
80
|
-
"getAllTools",
|
|
81
|
-
"getSessionStats",
|
|
82
|
-
"prompt",
|
|
83
|
-
"sendCustomMessage",
|
|
84
|
-
"subscribe",
|
|
85
|
-
]),
|
|
86
|
-
...missingMembers("AgentSessionRuntime#", sdk.AgentSessionRuntime?.prototype, [
|
|
87
|
-
"dispose",
|
|
88
|
-
"setRebindSession",
|
|
89
|
-
]),
|
|
90
|
-
];
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/** User-facing explanation for unmet requirements, or undefined when the runtime is supported. */
|
|
94
|
-
export function advisorRuntimeWarning(issues = advisorRuntimeIssues()): string | undefined {
|
|
95
|
-
if (!issues.length) return undefined;
|
|
96
|
-
return `Advisor is unavailable: this Pi runtime lacks ${issues.join(", ")}`;
|
|
97
|
-
}
|