@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.
@@ -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
- const optionsSchema = Type.Object(
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(optionsSchema.properties);
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 optionsSchema>;
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 type AdvisorSettingSource = "default" | "global" | "project" | "session";
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: optionsSchema,
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(optionsSchema, value)) {
74
- const issue = Value.Errors(optionsSchema, value)[0];
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(optionsSchema), input))
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
- Object.keys(optionsSchema.properties).map((key): [string, AdvisorSettingSource] => [
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 { AgentSession, ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
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
- "pi-advisor-child",
68
- (entry) => new Text(`Advisor for Child Agent\n${JSON.stringify(entry.data, null, 2)}`, 0, 0),
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 resolved = readAdvisorSettings(observed, layers);
311
- pi.appendEntry("pi-advisor-status", {
312
- state: resolved.settings.enabled ? "armed" : "disabled",
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, scoped settings, prompt editor, or status",
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 !== "status") {
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
- if (command.scope === "session") {
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
  }
@@ -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
- }