@ian-pascoe/pi-advisor 0.3.5 → 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. It also inherits whether scripts get the `models` API: Minimal Subagents Child Agents have none, so neither do their Advisors.
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.5",
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 {
@@ -149,8 +148,6 @@ export async function createAdvisorSession(
149
148
  observed: AgentSession,
150
149
  options: AdvisorSessionOptions,
151
150
  ): Promise<AgentSessionRuntime> {
152
- const unsupported = advisorRuntimeWarning();
153
- if (unsupported) throw new Error(unsupported);
154
151
  options.signal?.throwIfAborted();
155
152
  const cancelled = Promise.withResolvers<never>();
156
153
  const abort = () =>
@@ -225,10 +222,6 @@ async function buildAdvisorSession(
225
222
  const factoryExport = builtinFactories.get(name);
226
223
  if (builtin && factoryExport) {
227
224
  const create = piSdk[factoryExport];
228
- if (!isCallable(create))
229
- throw new Error(
230
- `Unsupported Advisor resources: this Pi runtime does not export ${factoryExport} to recreate ${extension.path}`,
231
- );
232
225
  recreated.add(index);
233
226
  const codemodeModels = options.resourceInputs?.codemodeModels;
234
227
  builtins.push({
@@ -244,10 +237,6 @@ async function buildAdvisorSession(
244
237
  }
245
238
  // Pi ships a fresh file-backed llama.cpp factory but does not export it.
246
239
  if (builtin && name === "llama.cpp") {
247
- if (!isCallable(piSdk.getPackageDir))
248
- throw new Error(
249
- "Unsupported Advisor resources: this Pi runtime does not export getPackageDir to locate llama.cpp",
250
- );
251
240
  const path = join(piSdk.getPackageDir(), "dist", "extensions", "llama", "index.js");
252
241
  if (!existsSync(path))
253
242
  throw new Error(
@@ -458,6 +447,18 @@ async function buildAdvisorSession(
458
447
  };
459
448
  if (sessionStartEvent) sessionOptions.sessionStartEvent = sessionStartEvent;
460
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
+ );
461
462
  return { ...created, services, diagnostics: services.diagnostics };
462
463
  };
463
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 {
@@ -46,8 +45,6 @@ const childRequestSchema = Type.Object({
46
45
 
47
46
  /** Review an observed session without changing its tools or standing instructions. */
48
47
  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
48
  let warn: (message: string) => void = () => {};
52
49
  let observed: AgentSession | undefined;
53
50
  let error: string | undefined;
@@ -76,6 +73,12 @@ export default function advisor(pi: ExtensionAPI): void {
76
73
  description:
77
74
  "Ask the enabled Advisor for analysis or a second opinion. Waits for its answer; does not delegate implementation.",
78
75
  parameters: askToolParameters,
76
+ annotations: {
77
+ readOnlyHint: true,
78
+ destructiveHint: false,
79
+ idempotentHint: true,
80
+ openWorldHint: false,
81
+ },
79
82
  executionMode: "sequential",
80
83
  execute: async (_id, { message }, signal) => {
81
84
  try {
@@ -147,12 +150,6 @@ export default function advisor(pi: ExtensionAPI): void {
147
150
  // A replaced session's UI is stale; the native status entry remains the durable record.
148
151
  }
149
152
  };
150
- if (unsupported) {
151
- error = unsupported;
152
- setAskToolAvailable(false);
153
- warn(unsupported);
154
- return;
155
- }
156
153
  const found = discoverPiAgentSession(pi, piSdk.AgentSession);
157
154
  if (found.ok) {
158
155
  observed = found.session;
@@ -215,7 +212,6 @@ export default function advisor(pi: ExtensionAPI): void {
215
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.
216
213
  function attachChild(payload: unknown): void {
217
214
  if (
218
- unsupported ||
219
215
  !Value.Check(childRequestSchema, payload) ||
220
216
  !(payload.session instanceof piSdk.AgentSession)
221
217
  )
@@ -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
- }