@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 +4 -4
- package/package.json +2 -2
- package/skills/pi-advisor/SKILL.md +2 -2
- package/src/advisor-observer.ts +14 -22
- package/src/advisor-runtime.ts +97 -0
- package/src/advisor-session.ts +116 -70
- package/src/advisor-settings.ts +1 -1
- package/src/index.ts +38 -28
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
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
package/src/advisor-observer.ts
CHANGED
|
@@ -1,18 +1,8 @@
|
|
|
1
1
|
import { isDeepStrictEqual } from "node:util";
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
|
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
|
|
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
|
+
}
|
package/src/advisor-session.ts
CHANGED
|
@@ -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
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
140
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
extension.path
|
|
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.
|
|
212
|
-
|
|
213
|
-
extension.
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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:
|
|
338
|
-
|
|
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
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
const
|
|
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
|
-
.
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
!bridgeAvailable
|
|
444
|
-
) {
|
|
489
|
+
.map(({ name }) => name)
|
|
490
|
+
.filter((name) => !ceiling.has(name));
|
|
491
|
+
if (escaped.length)
|
|
445
492
|
throw new Error(
|
|
446
|
-
|
|
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);
|
package/src/advisor-settings.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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)
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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 (
|
|
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);
|