@ian-pascoe/pi-advisor 0.3.4 → 0.4.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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  `@ian-pascoe/pi-advisor` reviews a Pi agent's completed work and surfaces concise, attributed findings for material problems and worthwhile low-risk cleanup.
4
4
 
5
- Requires Node `>=22.19.0`. Advisor does not pin a Pi version: at load it checks the Pi SDK exports and methods it uses. If any are missing, it warns, hides `advisor_ask`, and stays unavailable; `/advisor status` names the missing requirements. The observed session keeps working.
5
+ Requires Node `>=22.19.0` and Pi `>=0.99.0`.
6
6
 
7
7
  ## Install
8
8
 
@@ -41,6 +41,8 @@ Consultation authorizes analysis and investigation, not implementation or other
41
41
 
42
42
  `prompt` opens Pi's native editor and replaces the whole Advisor Prompt. `inherit` removes an override at the selected scope. Invalid keys and values are rejected. Lists, including `allowedTools`, replace the inherited list rather than merge. `catchUpThreshold` accepts any positive safe integer or `"off"`; `reviewTimeoutMs` accepts 1–2,147,483,647 milliseconds (the native timer range); `maxFindingsPerReview` accepts an integer from 1 through 32.
43
43
 
44
+ `advisor_ask` declares MCP-style `annotations` (read-only, non-destructive, idempotent, closed-world) that Pi reports through `pi.getAllTools()` for permission extensions; Pi does not send them to model providers. They describe the consultation itself, not the tools granted to the Advisor. The Advisor Session's internal `advisor_report` tool, which extensions inherited by that session can see, declares non-destructive, closed-world annotations and is not read-only, because it records the Review's findings.
45
+
44
46
  ## Defaults and access
45
47
 
46
48
  | Option | Default |
@@ -54,13 +56,13 @@ Consultation authorizes analysis and investigation, not implementation or other
54
56
  | Findings per Review | 4 |
55
57
  | Automatic Corrective Turns | 1 per request/task |
56
58
 
57
- A Tool Grant names tools; it does not sandbox their full native interfaces. Explicitly granting `lsp`, for example, permits its native operations, including mutations. Unavailable names are ignored and reported. Advisor never autogrants missing tools. Granting `codemode` lets the Advisor script its other granted tools; it inherits the observed `codemode.mode`, so under `"only"` the Advisor calls `advisor_report` from a script.
59
+ A Tool Grant names tools; it does not sandbox their full native interfaces. Explicitly granting `bash`, for example, permits any command, including mutations. Unavailable names are ignored and reported; status reflects the Advisor Session at that moment, so a granted tool that registers later, such as an MCP tool after its server connects, is reported until it does. Advisor never autogrants missing tools. Granted tools with `codemode` or `deferred` exposure (such as tools from Pi's built-in MCP) are not declared to the Advisor model; they stay callable from granted `codemode` scripts, and a granted `tool_search` can declare them, as in the observed agent. Granting `codemode` lets the Advisor script its other granted tools; it inherits the observed `codemode.mode`, so under `"only"` the Advisor calls `advisor_report` from a script. It also inherits whether scripts get the `models` API: Minimal Subagents Child Agents have none, so neither do their Advisors.
58
60
 
59
61
  The Advisor inherits each observed agent's model and thinking level independently unless configured otherwise. One root policy can cover current and future Minimal Subagents descendants when enabled.
60
62
 
61
63
  ## Sessions and Context Management
62
64
 
63
- Each watched agent has a private, persisted native Advisor Session with fresh, session-bound extension resources. Reviews reuse that session until context or configuration changes require rebuilding. Pi's enabled `builtin:<name>` extensions are recreated in their observed order: `codemode`, `tool-search`, and `mcp` from Pi's exported factories, and `llama.cpp` from the installed Pi package's shipped extension file, retaining their metadata and fresh provider state. Built-ins disabled with `-builtin:<name>` stay absent. If Pi no longer exports a needed factory, the Advisor pauses with that diagnostic. This is a built-in recreation recipe, not general inline-factory support; a host factory registered under a built-in name is rejected. Other opaque inline or custom extension resources that cannot be safely recreated are reported as unsupported and pause the Advisor; they are not silently reused or omitted. The Advisor's session state remains separate from the observed agent's state.
65
+ Each watched agent has a private, persisted native Advisor Session with fresh, session-bound extension resources. Reviews reuse that session until context or configuration changes require rebuilding. Pi's enabled `builtin:<name>` extensions are recreated in their observed order: `codemode`, `tool-search`, and `mcp` from Pi's exported factories, and `llama.cpp` from the installed Pi package's shipped extension file, retaining their metadata and fresh provider state. Built-ins disabled with `-builtin:<name>` stay absent. This is a built-in recreation recipe, not general inline-factory support; a host factory registered under a built-in name is rejected. Other opaque inline or custom extension resources that cannot be safely recreated are reported as unsupported and pause the Advisor; they are not silently reused or omitted. The Advisor's session state remains separate from the observed agent's state.
64
66
 
65
67
  Context Management is optional. If loaded and its tools are granted, `context_notes`, `context_history`, and `context_rollover` operate on the Advisor's private context. If any of those three tools is excluded, the Advisor pauses before reviewing. There is no autogrant or hook bypass.
66
68
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ian-pascoe/pi-advisor",
3
- "version": "0.3.4",
3
+ "version": "0.4.0",
4
4
  "private": false,
5
5
  "description": "Background review and attributed corrective advice for Pi agent sessions.",
6
6
  "keywords": [
@@ -32,13 +32,13 @@
32
32
  "provenance": true
33
33
  },
34
34
  "dependencies": {
35
- "@ian-pascoe/pi-utils": "^0.3.0"
35
+ "@ian-pascoe/pi-utils": "^0.3.1"
36
36
  },
37
37
  "peerDependencies": {
38
- "@earendil-works/pi-agent-core": "*",
39
- "@earendil-works/pi-ai": "*",
40
- "@earendil-works/pi-coding-agent": "*",
41
- "@earendil-works/pi-tui": "*",
38
+ "@earendil-works/pi-agent-core": ">=0.99.0",
39
+ "@earendil-works/pi-ai": ">=0.99.0",
40
+ "@earendil-works/pi-coding-agent": ">=0.99.0",
41
+ "@earendil-works/pi-tui": ">=0.99.0",
42
42
  "typebox": "*"
43
43
  },
44
44
  "engines": {
@@ -466,6 +466,12 @@ export class AdvisorObserver {
466
466
  label: "Advisor report",
467
467
  description: `Finish the Review with up to ${this.config.maxFindingsPerReview} concise, actionable findings. Prioritize blockers, then concerns, then worthwhile nits.`,
468
468
  parameters: reportSchema,
469
+ annotations: {
470
+ readOnlyHint: false,
471
+ destructiveHint: false,
472
+ idempotentHint: false,
473
+ openWorldHint: false,
474
+ },
469
475
  prepareArguments: (arguments_) => {
470
476
  if (Value.Check(reportSchema, arguments_)) return arguments_;
471
477
  if (!Value.Check(legacyReportSchema, arguments_))
@@ -19,7 +19,6 @@ import type {
19
19
  ToolDefinition,
20
20
  } from "@earendil-works/pi-coding-agent";
21
21
  import type { AdvisorConfig } from "./advisor-settings.js";
22
- import { advisorRuntimeWarning, isCallable } from "./advisor-runtime.js";
23
22
 
24
23
  /** Explicit file-selection recipe supplied by the resource owner, never inferred from opaque loaders. */
25
24
  export interface AdvisorResourceInputs {
@@ -31,6 +30,8 @@ export interface AdvisorResourceInputs {
31
30
  readonly hidden?: boolean | undefined;
32
31
  }[];
33
32
  flagValues: ReadonlyMap<string, boolean | string>;
33
+ /** Whether the owner's built-in codemode gives scripts the `models` API; Pi's default is true. */
34
+ codemodeModels?: boolean | undefined;
34
35
  }
35
36
 
36
37
  /** Fresh factories and native services; the observed session supplies data, never bound handlers. */
@@ -147,8 +148,6 @@ export async function createAdvisorSession(
147
148
  observed: AgentSession,
148
149
  options: AdvisorSessionOptions,
149
150
  ): Promise<AgentSessionRuntime> {
150
- const unsupported = advisorRuntimeWarning();
151
- if (unsupported) throw new Error(unsupported);
152
151
  options.signal?.throwIfAborted();
153
152
  const cancelled = Promise.withResolvers<never>();
154
153
  const abort = () =>
@@ -223,14 +222,14 @@ async function buildAdvisorSession(
223
222
  const factoryExport = builtinFactories.get(name);
224
223
  if (builtin && factoryExport) {
225
224
  const create = piSdk[factoryExport];
226
- if (!isCallable(create))
227
- throw new Error(
228
- `Unsupported Advisor resources: this Pi runtime does not export ${factoryExport} to recreate ${extension.path}`,
229
- );
230
225
  recreated.add(index);
226
+ const codemodeModels = options.resourceInputs?.codemodeModels;
231
227
  builtins.push({
232
228
  name,
233
- factory: create(),
229
+ factory:
230
+ name === "codemode" && codemodeModels !== undefined
231
+ ? piSdk.createCodemodeExtension({ models: codemodeModels })
232
+ : create(),
234
233
  builtin: true,
235
234
  replaceable: extension.replaceable === true,
236
235
  });
@@ -238,10 +237,6 @@ async function buildAdvisorSession(
238
237
  }
239
238
  // Pi ships a fresh file-backed llama.cpp factory but does not export it.
240
239
  if (builtin && name === "llama.cpp") {
241
- if (!isCallable(piSdk.getPackageDir))
242
- throw new Error(
243
- "Unsupported Advisor resources: this Pi runtime does not export getPackageDir to locate llama.cpp",
244
- );
245
240
  const path = join(piSdk.getPackageDir(), "dist", "extensions", "llama", "index.js");
246
241
  if (!existsSync(path))
247
242
  throw new Error(
@@ -452,6 +447,18 @@ async function buildAdvisorSession(
452
447
  };
453
448
  if (sessionStartEvent) sessionOptions.sessionStartEvent = sessionStartEvent;
454
449
  const created = await piSdk.createAgentSessionFromServices(sessionOptions);
450
+ // Pi's `tools` option is the ceiling and also activates (declares) every named tool. Keep
451
+ // `codemode` and `deferred` grants callable but undeclared, as Pi's own defaults do;
452
+ // later registrations follow the same rule, and extensions can still declare them.
453
+ const scriptOnly = new Set(
454
+ created.session
455
+ .getAllTools()
456
+ .filter(({ exposure }) => exposure === "codemode" || exposure === "deferred")
457
+ .map(({ name }) => name),
458
+ );
459
+ created.session.setActiveToolsByName(
460
+ created.session.getActiveToolNames().filter((name) => !scriptOnly.has(name)),
461
+ );
455
462
  return { ...created, services, diagnostics: services.diagnostics };
456
463
  };
457
464
  const runtime = await piSdk.createAgentSessionRuntime(factory, {
package/src/index.ts CHANGED
@@ -18,7 +18,6 @@ import {
18
18
  import { completeAdvisorCommandArguments, parseAdvisorCommand } from "./advisor-command.js";
19
19
  import { AdvisorObserver } from "./advisor-observer.js";
20
20
  import { isAdvisorSession, type AdvisorResourceInputs } from "./advisor-session.js";
21
- import { advisorRuntimeWarning } from "./advisor-runtime.js";
22
21
  import { TROUBLESHOOTING_HINT } from "./troubleshooting-skill.js";
23
22
 
24
23
  interface WatchedChild {
@@ -40,13 +39,12 @@ const childRequestSchema = Type.Object({
40
39
  agentDir: Type.String({ minLength: 1 }),
41
40
  extensions: Type.Array(Type.Unknown()),
42
41
  flagValues: Type.Unknown(),
42
+ codemodeModels: Type.Optional(Type.Boolean()),
43
43
  }),
44
44
  });
45
45
 
46
46
  /** Review an observed session without changing its tools or standing instructions. */
47
47
  export default function advisor(pi: ExtensionAPI): void {
48
- // Capability checks, not version pins: an unmet requirement warns and leaves the Advisor unavailable.
49
- const unsupported = advisorRuntimeWarning();
50
48
  let warn: (message: string) => void = () => {};
51
49
  let observed: AgentSession | undefined;
52
50
  let error: string | undefined;
@@ -75,6 +73,12 @@ export default function advisor(pi: ExtensionAPI): void {
75
73
  description:
76
74
  "Ask the enabled Advisor for analysis or a second opinion. Waits for its answer; does not delegate implementation.",
77
75
  parameters: askToolParameters,
76
+ annotations: {
77
+ readOnlyHint: true,
78
+ destructiveHint: false,
79
+ idempotentHint: true,
80
+ openWorldHint: false,
81
+ },
78
82
  executionMode: "sequential",
79
83
  execute: async (_id, { message }, signal) => {
80
84
  try {
@@ -146,12 +150,6 @@ export default function advisor(pi: ExtensionAPI): void {
146
150
  // A replaced session's UI is stale; the native status entry remains the durable record.
147
151
  }
148
152
  };
149
- if (unsupported) {
150
- error = unsupported;
151
- setAskToolAvailable(false);
152
- warn(unsupported);
153
- return;
154
- }
155
153
  const found = discoverPiAgentSession(pi, piSdk.AgentSession);
156
154
  if (found.ok) {
157
155
  observed = found.session;
@@ -214,7 +212,6 @@ export default function advisor(pi: ExtensionAPI): void {
214
212
  // 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.
215
213
  function attachChild(payload: unknown): void {
216
214
  if (
217
- unsupported ||
218
215
  !Value.Check(childRequestSchema, payload) ||
219
216
  !(payload.session instanceof piSdk.AgentSession)
220
217
  )
@@ -246,6 +243,7 @@ export default function advisor(pi: ExtensionAPI): void {
246
243
  agentDir: payload.resourceInputs.agentDir,
247
244
  extensions,
248
245
  flagValues: new Map(resources.runtime.flagValues),
246
+ codemodeModels: payload.resourceInputs.codemodeModels,
249
247
  },
250
248
  observer: undefined,
251
249
  };
@@ -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
- }