@ian-pascoe/pi-advisor 0.3.1 → 0.3.3

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 Pi `0.87.1` and Node `>=22.19.0`. Other Pi versions pause review with an explicit compatibility error.
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.
6
6
 
7
7
  ## Install
8
8
 
@@ -35,7 +35,7 @@ When Advisor is enabled, the main agent can call `advisor_ask({ message })` for
35
35
 
36
36
  A Consultation receives only observed context that has not already been supplied to that Advisor. It does not reduce the Review Backlog, create an Intervention, or trigger a Corrective Turn. Its ordinary tool call and result remain in the observed transcript for later Reviews.
37
37
 
38
- The tool is absent while Advisor is disabled. A paused Advisor keeps it visible so the call can report the pause reason and recovery commands. User cancellation stops only that Consultation; an inference, authentication, deadline, or investigative-tool failure pauses Advisor. CodeMode exposure settings continue to decide whether the tool is direct, CodeMode-only, or both.
38
+ The tool is absent while Advisor is disabled. A paused Advisor keeps it visible so the call can report the pause reason and recovery commands. User cancellation stops only that Consultation; an inference, authentication, deadline, or investigative-tool failure pauses Advisor. With Pi's built-in `codemode` active, scripts can call it too; under `codemode.mode: "only"` its declaration is hidden and it is reachable only through `codemode` scripts.
39
39
 
40
40
  Consultation authorizes analysis and investigation, not implementation or other side effects. The configured Advisor Prompt remains authoritative. Tool Grants still expose each granted tool's full native interface, so exclude mutating tools when a prompt-level boundary is insufficient.
41
41
 
@@ -54,13 +54,13 @@ Consultation authorizes analysis and investigation, not implementation or other
54
54
  | Findings per Review | 4 |
55
55
  | Automatic Corrective Turns | 1 per request/task |
56
56
 
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. CodeMode-only tools require an explicitly compatible transport grant; Advisor never adds aliases, autogrants missing tools, or changes CodeMode exposure.
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.
58
58
 
59
59
  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
60
 
61
61
  ## Sessions and Context Management
62
62
 
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 0.87.1's hidden inline `llama.cpp` built-in is recreated from the installed Pi package's shipped extension file, retaining its ordered metadata and fresh provider state. This is a fixed-version compatibility recipe, not general inline-factory support; it requires the shipped file and the built-in's expected registration shape. 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.
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.
64
64
 
65
65
  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
66
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ian-pascoe/pi-advisor",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "private": false,
5
5
  "description": "Background review and attributed corrective advice for Pi agent sessions.",
6
6
  "keywords": [
@@ -32,7 +32,7 @@
32
32
  "provenance": true
33
33
  },
34
34
  "dependencies": {
35
- "@ian-pascoe/pi-utils": "^0.2.0"
35
+ "@ian-pascoe/pi-utils": "^0.3.0"
36
36
  },
37
37
  "peerDependencies": {
38
38
  "@earendil-works/pi-agent-core": "*",
@@ -11,10 +11,10 @@ Use this sequence for a live Advisor problem:
11
11
  1. Run `/advisor status`. Record the effective settings, each setting's source, review state, backlog, usage/cost, and last error. Treat unknown cost as unknown, not zero.
12
12
  2. If the state is disabled, check the selected session override, trusted-project settings, and global settings in that order. Advisor defaults to disabled. Use `/advisor on`, `/advisor off`, `/advisor inherit`, or `/advisor set <key> <JSON>` at the intended scope; use `/advisor prompt` for the native prompt editor. Confirm the selected branch after resume, fork, or tree navigation.
13
13
  3. If the state is paused, read the reported capability, Review, or Consultation error. Correct the setting or unavailable runtime capability, then explicitly re-enable or reload as the diagnostic instructs. A pause leaves the observed agent running.
14
- 4. If `advisor_ask` is missing, confirm Advisor is enabled; disabled Advisors intentionally hide it. A paused Advisor keeps the tool visible for its diagnostic. Check CodeMode exposure when the tool is available only through `codemode_execute`. Consultations wait for the active Review, return plain advice, and leave Review Backlog accounting unchanged. User cancellation does not pause Advisor; runtime failures do.
14
+ 4. If `advisor_ask` is missing, confirm Advisor is enabled; disabled Advisors intentionally hide it. A paused Advisor keeps the tool visible for its diagnostic. Under `codemode.mode: "only"` it stays active but undeclared, reachable only through `codemode` scripts. Consultations wait for the active Review, return plain advice, and leave Review Backlog accounting unchanged. User cancellation does not pause Advisor; runtime failures do.
15
15
  5. For missing reviews, inspect backlog and catch-up configuration. Backlog counts completed model responses with their tool batches, including work under review. `off` never waits; a positive threshold waits only while backlog is at least that threshold, and each wait is capped at 30 seconds. A Review has its own deadline, investigative-call limit, and `maxFindingsPerReview` limit of 1–32.
16
16
  6. If Context Management is loaded, verify that all three private tools—`context_notes`, `context_history`, and `context_rollover`—are explicitly granted. Any missing grant pauses Advisor before review. These tools operate on the Advisor Session's private Notes, History, and Checkpoints; they never grant access to the observed session. Confirm native settlement and checkpoint completion rather than treating an early response or `agent_end` report as completion.
17
- 7. For tool or CodeMode failures, compare the effective `allowedTools` list with the loaded registry. Missing names are reported and ignored. CodeMode-only exposure needs an explicitly compatible transport grant; Advisor does not add aliases, autogrant tools, or change exposure mode. A Tool Grant covers the tool's full native interface and is not a filesystem or operation sandbox.
17
+ 7. For tool failures, compare the effective `allowedTools` list with the loaded registry. Missing names are reported and ignored; Advisor does not autogrant tools. Grant `codemode` explicitly for scripts. An inactive advice tool after extension binding pauses the Advisor. A Tool Grant covers the tool's full native interface and is not a filesystem or operation sandbox.
18
18
  8. For stale, duplicated, or unsafe advice, check branch/session identity and configuration-generation changes. Advisor Sessions are separate persisted native sessions with fresh extension resources. Unsupported opaque inline/custom resources pause rather than being reused. Nits use non-interrupting context delivery and never start Corrective Turns. Running Concerns and Blockers use Pi's native steering boundary; Blockers after normal interactive completion use only tracked, budgeted Corrective Turns. Deliberately interrupted, aborted, uncertain, and headless-completed work is preserved without restart. Child correction must finish inside Minimal's owned operation.
19
19
  9. On shutdown, allow only the bounded headless final drain. Verify that no hidden corrective turn starts after root completion and that native Advisor resources are aborted before disposal.
20
20
 
@@ -1,18 +1,8 @@
1
1
  import { isDeepStrictEqual } from "node:util";
2
- import {
3
- contentText,
4
- getCurrentSystemPrompt,
5
- getCurrentTools,
6
- toToolDeclaration,
7
- type Context,
8
- type ImageContent,
9
- } from "@earendil-works/pi-ai";
10
- import {
11
- defineTool,
12
- convertToLlm,
13
- type AgentSession,
14
- type AgentSessionRuntime,
15
- } from "@earendil-works/pi-coding-agent";
2
+ import * as piAi from "@earendil-works/pi-ai";
3
+ import type { Context, ImageContent } from "@earendil-works/pi-ai";
4
+ import * as piSdk from "@earendil-works/pi-coding-agent";
5
+ import type { AgentSession, AgentSessionRuntime } from "@earendil-works/pi-coding-agent";
16
6
  import { Type, type Static } from "typebox";
17
7
  import { Value } from "typebox/value";
18
8
  import type { AdvisorConfig } from "./advisor-settings.js";
@@ -65,7 +55,7 @@ const pendingQueuesSchema = Type.Object({
65
55
  _pendingCustomMessages: Type.Array(Type.Unknown()),
66
56
  });
67
57
 
68
- // oxlint-disable-next-line anti-slop/no-unknown-parameters -- SAFETY: Pi 0.87.1 has no selective queue-removal API. Validate its native queue data and remove only exact owned finding identities, never unrelated messages or journal entries.
58
+ // oxlint-disable-next-line anti-slop/no-unknown-parameters -- SAFETY: Pi has no selective queue-removal API. Validate its native queue data and remove only exact owned finding identities, never unrelated messages or journal entries.
69
59
  function retractFindings(session: unknown, findings: ReadonlySet<Finding>): void {
70
60
  if (!findings.size) return;
71
61
  if (!Value.Check(pendingQueuesSchema, session))
@@ -168,7 +158,9 @@ function operationFailure(runtime: AgentSessionRuntime, since: number): string |
168
158
  .slice(since)
169
159
  .find((message) => message.role === "toolResult" && message.isError);
170
160
  if (!failedTool || failedTool.role !== "toolResult") return;
171
- return contentText(failedTool.content).trim() || `Advisor tool ${failedTool.toolName} failed`;
161
+ return (
162
+ piAi.contentText(failedTool.content).trim() || `Advisor tool ${failedTool.toolName} failed`
163
+ );
172
164
  }
173
165
 
174
166
  /** Coalesced native review work; it never owns the observed tools, prompt, or agent loop. */
@@ -218,9 +210,9 @@ export class AdvisorObserver {
218
210
  // Pi carries the prompt and tool deltas as system messages. Replay them into the
219
211
  // current state and copy tool declarations, never execute callbacks.
220
212
  this.snapshot = structuredClone({
221
- systemPrompt: getCurrentSystemPrompt(context.messages),
222
- tools: getCurrentTools(context.messages).map((tool) => {
223
- const { name, description, parameters } = toToolDeclaration(tool);
213
+ systemPrompt: piAi.getCurrentSystemPrompt(context.messages),
214
+ tools: piAi.getCurrentTools(context.messages).map((tool) => {
215
+ const { name, description, parameters } = piAi.toToolDeclaration(tool);
224
216
  return { name, description, parameters };
225
217
  }),
226
218
  messages: context.messages.filter((message) => message.role !== "system"),
@@ -250,7 +242,7 @@ export class AdvisorObserver {
250
242
  ...this.snapshot,
251
243
  messages: [
252
244
  ...this.snapshot.messages,
253
- ...convertToLlm([event.message, ...event.toolResults]),
245
+ ...piSdk.convertToLlm([event.message, ...event.toolResults]),
254
246
  ],
255
247
  };
256
248
  if (
@@ -469,7 +461,7 @@ export class AdvisorObserver {
469
461
  },
470
462
  { additionalProperties: false },
471
463
  );
472
- const adviceTool = defineTool({
464
+ const adviceTool = piSdk.defineTool({
473
465
  name: "advisor_report",
474
466
  label: "Advisor report",
475
467
  description: `Finish the Review with up to ${this.config.maxFindingsPerReview} concise, actionable findings. Prioritize blockers, then concerns, then worthwhile nits.`,
@@ -729,7 +721,7 @@ export class AdvisorObserver {
729
721
  throw new Error("Advisor consultation did not return an answer");
730
722
  if (last.stopReason === "error" || last.stopReason === "aborted")
731
723
  throw new Error(last.errorMessage ?? "Advisor consultation did not complete");
732
- const answer = contentText(last.content).trim();
724
+ const answer = piAi.contentText(last.content).trim();
733
725
  if (!answer) throw new Error("Advisor consultation did not return a text answer");
734
726
  this.supplied = snapshot;
735
727
  this.suppliedBoundary = consultation.boundary;
@@ -0,0 +1,97 @@
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
+ }
@@ -3,27 +3,23 @@ import { join } from "node:path";
3
3
  import { isDeepStrictEqual } from "node:util";
4
4
  import { Type } from "typebox";
5
5
  import { Value } from "typebox/value";
6
- import { InMemoryCredentialStore } from "@earendil-works/pi-ai";
7
- import {
8
- createAgentSessionServices,
9
- createAgentSessionFromServices,
10
- createAgentSessionRuntime,
11
- DefaultResourceLoader,
12
- getPackageDir,
13
- ModelRuntime,
6
+ import * as piAi from "@earendil-works/pi-ai";
7
+ import * as piSdk from "@earendil-works/pi-coding-agent";
8
+ import type {
9
+ SourceInfo,
10
+ CreateModelRuntimeOptions,
11
+ AgentSession,
12
+ AgentSessionRuntime,
13
+ CreateAgentSessionRuntimeFactory,
14
+ CreateAgentSessionFromServicesOptions,
15
+ Extension,
16
+ ExtensionFactory,
17
+ InlineExtension,
14
18
  SessionManager,
15
- SettingsManager,
16
- VERSION,
17
- type SourceInfo,
18
- type CreateModelRuntimeOptions,
19
- type AgentSession,
20
- type AgentSessionRuntime,
21
- type CreateAgentSessionRuntimeFactory,
22
- type CreateAgentSessionFromServicesOptions,
23
- type ExtensionFactory,
24
- type ToolDefinition,
19
+ ToolDefinition,
25
20
  } from "@earendil-works/pi-coding-agent";
26
21
  import type { AdvisorConfig } from "./advisor-settings.js";
22
+ import { advisorRuntimeWarning, isCallable } from "./advisor-runtime.js";
27
23
 
28
24
  /** Explicit file-selection recipe supplied by the resource owner, never inferred from opaque loaders. */
29
25
  export interface AdvisorResourceInputs {
@@ -51,6 +47,21 @@ const roleSchema = Type.Object({
51
47
  observedSessionId: Type.String({ minLength: 1 }),
52
48
  });
53
49
 
50
+ // Pi exports fresh factories for these `builtin:<name>` extensions, but not llama.cpp.
51
+ const builtinFactories = new Map<
52
+ string,
53
+ "createCodemodeExtension" | "createToolSearchExtension" | "createMcpExtension"
54
+ >([
55
+ ["codemode", "createCodemodeExtension"],
56
+ ["tool-search", "createToolSearchExtension"],
57
+ ["mcp", "createMcpExtension"],
58
+ ]);
59
+ const registrationNames = ({ tools, commands, flags }: Extension) => [
60
+ ...[...tools.keys()].map((name) => `tool:${name}`),
61
+ ...[...commands.keys()].map((name) => `command:${name}`),
62
+ ...[...flags.keys()].map((name) => `flag:${name}`),
63
+ ];
64
+
54
65
  /** Private role is native journal state and is installed before session_start. */
55
66
  export function isAdvisorSession(manager: Pick<SessionManager, "getBranch">): boolean {
56
67
  return manager
@@ -72,7 +83,7 @@ export async function disposeAdvisorSession(runtime: AgentSessionRuntime): Promi
72
83
  }
73
84
  }
74
85
 
75
- // Pi 0.87.1 keeps these plain constructor inputs private; public scoped getters omit applyOverrides.
86
+ // Pi keeps these plain constructor inputs private; public scoped getters omit applyOverrides.
76
87
  /* oxlint-disable anti-slop/no-unknown-parameters -- SAFETY: Capability-check native SDK data before copying; offline SDK creation tests cover these inputs. */
77
88
  function recreationInputs(
78
89
  loader: unknown,
@@ -111,7 +122,7 @@ function recreationInputs(
111
122
  if (
112
123
  Value.Check(fileAuthSchema, store) &&
113
124
  store.authPath === store.storage.authPath &&
114
- // Pi 0.87.1's bundled CLI names the same native class _AuthStorage.
125
+ // Pi's bundled CLI names the same native class _AuthStorage.
115
126
  ["AuthStorage", "_AuthStorage"].includes(Object.getPrototypeOf(store)?.constructor.name) &&
116
127
  Object.getPrototypeOf(store.storage)?.constructor.name === "FileAuthStorageBackend"
117
128
  )
@@ -136,10 +147,8 @@ export async function createAdvisorSession(
136
147
  observed: AgentSession,
137
148
  options: AdvisorSessionOptions,
138
149
  ): Promise<AgentSessionRuntime> {
139
- if (VERSION !== "0.87.1")
140
- throw new Error(
141
- `Unsupported Pi ${VERSION}: Advisor's native tool ceiling and session lifecycle are verified on Pi 0.87.1`,
142
- );
150
+ const unsupported = advisorRuntimeWarning();
151
+ if (unsupported) throw new Error(unsupported);
143
152
  options.signal?.throwIfAborted();
144
153
  const cancelled = Promise.withResolvers<never>();
145
154
  const abort = () =>
@@ -161,7 +170,7 @@ async function buildAdvisorSession(
161
170
  const source = observed.resourceLoader;
162
171
  if (
163
172
  !options.resourceInputs &&
164
- Object.getPrototypeOf(source) !== DefaultResourceLoader.prototype
173
+ Object.getPrototypeOf(source) !== piSdk.DefaultResourceLoader.prototype
165
174
  ) {
166
175
  throw new Error(
167
176
  "Unsupported Advisor resources: custom loaders/transformations require fresh owner-supplied recreation inputs",
@@ -200,38 +209,54 @@ async function buildAdvisorSession(
200
209
  )
201
210
  throw new Error("Advisor resource owner flags do not match the observed flags");
202
211
  }
203
- const extensionPaths = source.getExtensions().extensions.map((extension) => {
204
- if (extension.resolvedPath && !extension.resolvedPath.startsWith("<"))
205
- return extension.resolvedPath;
206
- // Pi 0.87.1 injects this built-in inline, but ships a fresh file-backed factory.
207
- // Match its registration shape, not arbitrary inline closures or hidden extensions.
208
- if (
209
- extension.path === "<inline:llama.cpp>" &&
212
+ // Pi names its built-in extensions `builtin:<name>`; fresh factories must reproduce them.
213
+ const builtins: InlineExtension[] = [];
214
+ const recreated = new Set<number>();
215
+ const extensionPaths = source.getExtensions().extensions.map((extension, index) => {
216
+ const name = extension.path.slice("builtin:".length);
217
+ const builtin =
218
+ extension.path.startsWith("builtin:") &&
210
219
  extension.resolvedPath === extension.path &&
211
- extension.hidden === true &&
212
- extension.commands.size === 1 &&
213
- extension.commands.has("llama") &&
214
- [
215
- extension.handlers,
216
- extension.tools,
217
- extension.messageRenderers,
218
- extension.entryRenderers,
219
- extension.flags,
220
- extension.shortcuts,
221
- ].every((registrations) => !registrations?.size) &&
222
- !extension.markdownTransformer
223
- ) {
224
- const path = join(getPackageDir(), "dist", "extensions", "llama", "index.js");
220
+ extension.sourceInfo.source === "builtin";
221
+ if (!builtin && extension.resolvedPath && !extension.resolvedPath.startsWith("<"))
222
+ return extension.resolvedPath;
223
+ const factoryExport = builtinFactories.get(name);
224
+ if (builtin && factoryExport) {
225
+ 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
+ recreated.add(index);
231
+ builtins.push({
232
+ name,
233
+ factory: create(),
234
+ builtin: true,
235
+ replaceable: extension.replaceable === true,
236
+ });
237
+ return extension.path;
238
+ }
239
+ // Pi ships a fresh file-backed llama.cpp factory but does not export it.
240
+ 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
+ const path = join(piSdk.getPackageDir(), "dist", "extensions", "llama", "index.js");
225
246
  if (!existsSync(path))
226
247
  throw new Error(
227
248
  `Unsupported Advisor resources: Pi's built-in llama.cpp file is unavailable (${path})`,
228
249
  );
250
+ recreated.add(index);
229
251
  return path;
230
252
  }
231
253
  throw new Error(
232
254
  `Unsupported Advisor resources: inline factories require fresh owner-supplied recreation inputs (${extension.path})`,
233
255
  );
234
256
  });
257
+ const registrations = source
258
+ .getExtensions()
259
+ .extensions.map((extension) => new Set(registrationNames(extension)));
235
260
  const flags = new Map(observed.extensionRunner?.getFlagValues());
236
261
  const global = JSON.stringify(observed.settingsManager.getGlobalSettings());
237
262
  const project = JSON.stringify(observed.settingsManager.getProjectSettings());
@@ -299,7 +324,7 @@ async function buildAdvisorSession(
299
324
  ["global", global],
300
325
  ["project", project],
301
326
  ]);
302
- const settingsManager = SettingsManager.fromStorage(
327
+ const settingsManager = piSdk.SettingsManager.fromStorage(
303
328
  {
304
329
  withLock(scope, update) {
305
330
  const next = update(scopes.get(scope));
@@ -320,12 +345,12 @@ async function buildAdvisorSession(
320
345
  };
321
346
  if (constructionSignal) modelInputs.signal = constructionSignal;
322
347
  if (authPath) modelInputs.authPath = authPath;
323
- else modelInputs.credentials = new InMemoryCredentialStore();
324
- const modelRuntime = await ModelRuntime.create(modelInputs);
348
+ else modelInputs.credentials = new piAi.InMemoryCredentialStore();
349
+ const modelRuntime = await piSdk.ModelRuntime.create(modelInputs);
325
350
  const operation = constructionSignal ? { signal: constructionSignal } : undefined;
326
351
  for (const [provider, apiKey] of runtimeApiKeys)
327
352
  await modelRuntime.setRuntimeApiKey(provider, apiKey, operation);
328
- const services = await createAgentSessionServices({
353
+ const services = await piSdk.createAgentSessionServices({
329
354
  cwd,
330
355
  agentDir,
331
356
  settingsManager,
@@ -334,9 +359,25 @@ async function buildAdvisorSession(
334
359
  noExtensions: true,
335
360
  noContextFiles: true,
336
361
  additionalExtensionPaths: extensionPaths,
337
- extensionFactories: options.controlExtension
338
- ? [{ name: "advisor-control", factory: options.controlExtension }]
339
- : [],
362
+ extensionFactories: [
363
+ ...builtins,
364
+ ...(options.controlExtension
365
+ ? [{ name: "advisor-control", factory: options.controlExtension }]
366
+ : []),
367
+ ],
368
+ // CLI paths load built-ins first; restore the observed order before binding.
369
+ extensionsOverride: (base) => {
370
+ const rank = (path: string) => {
371
+ const index = extensionPaths.indexOf(path);
372
+ return index < 0 ? extensionPaths.length : index;
373
+ };
374
+ return {
375
+ ...base,
376
+ extensions: base.extensions.toSorted(
377
+ (left, right) => rank(left.resolvedPath) - rank(right.resolvedPath),
378
+ ),
379
+ };
380
+ },
340
381
  systemPromptOverride: () => config.prompt,
341
382
  appendSystemPromptOverride: () => [],
342
383
  },
@@ -377,6 +418,14 @@ async function buildAdvisorSession(
377
418
  inherited.forEach((original, index) => {
378
419
  const fresh = loaded.extensions[index];
379
420
  if (!fresh) throw new Error("Advisor inherited extension is unavailable");
421
+ // A host-supplied factory under a built-in name registers what Pi's own does not.
422
+ if (
423
+ recreated.has(index) &&
424
+ registrationNames(fresh).some((name) => !registrations[index]?.has(name))
425
+ )
426
+ throw new Error(
427
+ `Unsupported Advisor resources: ${original.path} does not match Pi's built-in extension`,
428
+ );
380
429
  fresh.path = original.path;
381
430
  fresh.resolvedPath = original.resolvedPath;
382
431
  if (original.hidden === undefined) delete fresh.hidden;
@@ -402,13 +451,13 @@ async function buildAdvisorSession(
402
451
  customTools: [options.adviceTool],
403
452
  };
404
453
  if (sessionStartEvent) sessionOptions.sessionStartEvent = sessionStartEvent;
405
- const created = await createAgentSessionFromServices(sessionOptions);
454
+ const created = await piSdk.createAgentSessionFromServices(sessionOptions);
406
455
  return { ...created, services, diagnostics: services.diagnostics };
407
456
  };
408
- const runtime = await createAgentSessionRuntime(factory, {
457
+ const runtime = await piSdk.createAgentSessionRuntime(factory, {
409
458
  cwd: observed.sessionManager.getCwd(),
410
459
  agentDir,
411
- sessionManager: SessionManager.create(
460
+ sessionManager: piSdk.SessionManager.create(
412
461
  observed.sessionManager.getCwd(),
413
462
  join(observed.sessionManager.getSessionDir(), "advisors", observed.sessionId),
414
463
  ),
@@ -429,23 +478,20 @@ async function buildAdvisorSession(
429
478
  options.signal?.throwIfAborted();
430
479
  bindingStarted = true;
431
480
  await bind(runtime.session);
432
- const active = runtime.session.getActiveToolNames();
433
- const bridgeAvailable =
434
- config.allowedTools.includes("codemode_execute") && active.includes("codemode_execute");
435
- const codeModeLoaded = source
436
- .getExtensions()
437
- .extensions.some((extension) => extension.tools.has("codemode_execute"));
438
- const hiddenGrants = runtime.session
481
+ if (!runtime.session.getActiveToolNames().includes(options.adviceTool.name))
482
+ throw new Error(
483
+ `Advisor advice tool is inactive after extension binding: ${options.adviceTool.name}`,
484
+ );
485
+ // The native `tools` option is the Advisor's tool ceiling; verify this runtime enforces it.
486
+ const ceiling = new Set([...config.allowedTools, options.adviceTool.name]);
487
+ const escaped = runtime.session
439
488
  .getAllTools()
440
- .filter((tool) => config.allowedTools.includes(tool.name) && !active.includes(tool.name));
441
- if (
442
- (!active.includes(options.adviceTool.name) || (codeModeLoaded && hiddenGrants.length > 0)) &&
443
- !bridgeAvailable
444
- ) {
489
+ .map(({ name }) => name)
490
+ .filter((name) => !ceiling.has(name));
491
+ if (escaped.length)
445
492
  throw new Error(
446
- "Advisor tools are hidden by inherited exposure policy; grant the CodeMode execution tool explicitly or configure direct exposure",
493
+ `Unsupported Pi runtime: the Advisor tool ceiling admitted ungranted tools (${escaped.join(", ")})`,
447
494
  );
448
- }
449
495
  options.signal?.throwIfAborted();
450
496
  const failure = runtime.diagnostics.find((item) => item.type === "error");
451
497
  if (failure) throw new Error(failure.message);
@@ -171,7 +171,7 @@ const storageSchema = Type.Object({
171
171
  ),
172
172
  });
173
173
 
174
- /** Use Pi's actual backend: its generic storage API is not exported in 0.87.1. */
174
+ /** Use Pi's actual backend: its generic storage API is not exported. */
175
175
  export async function writeAdvisorSettings(
176
176
  manager: SettingsManager,
177
177
  scope: "global" | "project",
package/src/index.ts CHANGED
@@ -1,8 +1,5 @@
1
- import {
2
- AgentSession,
3
- type ExtensionAPI,
4
- type ExtensionContext,
5
- } from "@earendil-works/pi-coding-agent";
1
+ import * as piSdk from "@earendil-works/pi-coding-agent";
2
+ import type { AgentSession, ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
6
3
  import { isDeepStrictEqual } from "node:util";
7
4
  import { Text } from "@earendil-works/pi-tui";
8
5
  import { Type } from "typebox";
@@ -21,6 +18,7 @@ import {
21
18
  import { completeAdvisorCommandArguments, parseAdvisorCommand } from "./advisor-command.js";
22
19
  import { AdvisorObserver } from "./advisor-observer.js";
23
20
  import { isAdvisorSession, type AdvisorResourceInputs } from "./advisor-session.js";
21
+ import { advisorRuntimeWarning } from "./advisor-runtime.js";
24
22
 
25
23
  interface WatchedChild {
26
24
  agentId: string;
@@ -28,7 +26,6 @@ interface WatchedChild {
28
26
  observer: AdvisorObserver | undefined;
29
27
  }
30
28
  const askToolName = "advisor_ask";
31
- const codeModeToolAvailabilityEvent = "pi-codemode:request-tool-availability";
32
29
  const askToolParameters = Type.Object(
33
30
  { message: Type.String({ minLength: 1 }) },
34
31
  { additionalProperties: false },
@@ -47,6 +44,9 @@ const childRequestSchema = Type.Object({
47
44
 
48
45
  /** Review an observed session without changing its tools or standing instructions. */
49
46
  export default function advisor(pi: ExtensionAPI): void {
47
+ // Capability checks, not version pins: an unmet requirement warns and leaves the Advisor unavailable.
48
+ const unsupported = advisorRuntimeWarning();
49
+ let warn: (message: string) => void = () => {};
50
50
  let observed: AgentSession | undefined;
51
51
  let error: string | undefined;
52
52
  let layers: AdvisorLayers = { global: {}, project: {} };
@@ -89,17 +89,6 @@ export default function advisor(pi: ExtensionAPI): void {
89
89
  return;
90
90
  }
91
91
  if (!askToolRegistered) return;
92
- let handled = false;
93
- if (rootSessionId)
94
- pi.events.emit(codeModeToolAvailabilityEvent, {
95
- sessionId: rootSessionId,
96
- toolName: askToolName,
97
- available,
98
- handled: () => {
99
- handled = true;
100
- },
101
- });
102
- if (handled) return;
103
92
  const active = pi.getActiveTools();
104
93
  if (available === active.includes(askToolName)) return;
105
94
  pi.setActiveTools(
@@ -141,7 +130,21 @@ export default function advisor(pi: ExtensionAPI): void {
141
130
  setAskToolAvailable(false);
142
131
  return;
143
132
  }
144
- const found = discoverPiAgentSession(pi, AgentSession);
133
+ const { ui } = ctx;
134
+ warn = (message) => {
135
+ try {
136
+ ui.notify(message, "warning");
137
+ } catch {
138
+ // A replaced session's UI is stale; the native status entry remains the durable record.
139
+ }
140
+ };
141
+ if (unsupported) {
142
+ error = unsupported;
143
+ setAskToolAvailable(false);
144
+ warn(unsupported);
145
+ return;
146
+ }
147
+ const found = discoverPiAgentSession(pi, piSdk.AgentSession);
145
148
  if (found.ok) {
146
149
  observed = found.session;
147
150
  layers = readAdvisorLayers(observed.settingsManager);
@@ -185,12 +188,13 @@ export default function advisor(pi: ExtensionAPI): void {
185
188
  child.observer = new AdvisorObserver(session, config, "owned-child", {
186
189
  resourceInputs: child.resourceInputs,
187
190
  onError: (message) => {
188
- if (children.get(session) === child)
189
- pi.appendEntry("pi-advisor-child", {
190
- agentId: child.agentId,
191
- state: "paused",
192
- error: message,
193
- });
191
+ if (children.get(session) !== child) return;
192
+ pi.appendEntry("pi-advisor-child", {
193
+ agentId: child.agentId,
194
+ state: "paused",
195
+ error: message,
196
+ });
197
+ warn(`Advisor for ${child.agentId} paused: ${message}`);
194
198
  },
195
199
  onIntervention: (finding) => {
196
200
  if (children.get(session) === child)
@@ -201,10 +205,14 @@ export default function advisor(pi: ExtensionAPI): void {
201
205
 
202
206
  // 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.
203
207
  function attachChild(payload: unknown): void {
204
- if (!Value.Check(childRequestSchema, payload) || !(payload.session instanceof AgentSession))
208
+ if (
209
+ unsupported ||
210
+ !Value.Check(childRequestSchema, payload) ||
211
+ !(payload.session instanceof piSdk.AgentSession)
212
+ )
205
213
  return;
206
214
  if (!observed) {
207
- const found = discoverPiAgentSession(pi, AgentSession);
215
+ const found = discoverPiAgentSession(pi, piSdk.AgentSession);
208
216
  if (!found.ok) return;
209
217
  observed = found.session;
210
218
  layers = readAdvisorLayers(observed.settingsManager);
@@ -266,8 +274,10 @@ export default function advisor(pi: ExtensionAPI): void {
266
274
  settings,
267
275
  ctx.hasUI ? "interactive" : "headless-root",
268
276
  {
269
- onError: (message) =>
270
- pi.appendEntry("pi-advisor-status", { state: "paused", error: message }),
277
+ onError: (message) => {
278
+ pi.appendEntry("pi-advisor-status", { state: "paused", error: message });
279
+ warn(`Advisor paused: ${message}`);
280
+ },
271
281
  },
272
282
  );
273
283
  for (const [session, child] of children) configureChild(session, child, settings);