@ian-pascoe/pi-advisor 0.0.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/LICENSE +21 -0
- package/README.md +63 -0
- package/package.json +64 -0
- package/skills/pi-advisor/SKILL.md +20 -0
- package/src/advisor-command.ts +58 -0
- package/src/advisor-observer.ts +664 -0
- package/src/advisor-session.ts +460 -0
- package/src/advisor-settings.ts +204 -0
- package/src/index.ts +313 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AgentSession,
|
|
3
|
+
type ExtensionAPI,
|
|
4
|
+
type ExtensionContext,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { isDeepStrictEqual } from "node:util";
|
|
7
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
8
|
+
import { Type } from "typebox";
|
|
9
|
+
import { Value } from "typebox/value";
|
|
10
|
+
import { discoverPiAgentSession } from "@ian-pascoe/pi-utils/pi-agent-session-discovery";
|
|
11
|
+
import {
|
|
12
|
+
parseAdvisorOptions,
|
|
13
|
+
readAdvisorLayers,
|
|
14
|
+
readAdvisorOverrides,
|
|
15
|
+
readAdvisorSettings,
|
|
16
|
+
writeAdvisorSettings,
|
|
17
|
+
type AdvisorLayers,
|
|
18
|
+
type AdvisorChange,
|
|
19
|
+
type AdvisorConfig,
|
|
20
|
+
} from "./advisor-settings.js";
|
|
21
|
+
import { completeAdvisorCommandArguments, parseAdvisorCommand } from "./advisor-command.js";
|
|
22
|
+
import { AdvisorObserver } from "./advisor-observer.js";
|
|
23
|
+
import { isAdvisorSession, type AdvisorResourceInputs } from "./advisor-session.js";
|
|
24
|
+
|
|
25
|
+
interface WatchedChild {
|
|
26
|
+
agentId: string;
|
|
27
|
+
resourceInputs: AdvisorResourceInputs;
|
|
28
|
+
observer: AdvisorObserver | undefined;
|
|
29
|
+
}
|
|
30
|
+
const childRequestSchema = Type.Object({
|
|
31
|
+
rootSessionId: Type.String(),
|
|
32
|
+
agentId: Type.String(),
|
|
33
|
+
session: Type.Unknown(),
|
|
34
|
+
attach: Type.Function([Type.Unknown()], Type.Void()),
|
|
35
|
+
resourceInputs: Type.Object({
|
|
36
|
+
agentDir: Type.String({ minLength: 1 }),
|
|
37
|
+
extensions: Type.Array(Type.Unknown()),
|
|
38
|
+
flagValues: Type.Unknown(),
|
|
39
|
+
}),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
/** Review an observed session without changing its tools or standing instructions. */
|
|
43
|
+
export default function advisor(pi: ExtensionAPI): void {
|
|
44
|
+
let observed: AgentSession | undefined;
|
|
45
|
+
let error: string | undefined;
|
|
46
|
+
let layers: AdvisorLayers = { global: {}, project: {} };
|
|
47
|
+
let generation = 0;
|
|
48
|
+
let privateSession = false;
|
|
49
|
+
let observer: AdvisorObserver | undefined;
|
|
50
|
+
let rootSessionId: string | undefined;
|
|
51
|
+
const children = new Map<AgentSession, WatchedChild>();
|
|
52
|
+
let unsubscribe: (() => void) | undefined = pi.events.on(
|
|
53
|
+
"pi-minimal-subagents:observe-session",
|
|
54
|
+
attachChild,
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
pi.registerEntryRenderer(
|
|
58
|
+
"pi-advisor-child",
|
|
59
|
+
(entry) => new Text(`Advisor for Child Agent\n${JSON.stringify(entry.data, null, 2)}`, 0, 0),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
pi.registerEntryRenderer(
|
|
63
|
+
"pi-advisor-status",
|
|
64
|
+
(entry) => new Text(`Advisor\n${JSON.stringify(entry.data, null, 2)}`, 0, 0),
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
68
|
+
const stamp = ++generation;
|
|
69
|
+
const previous = observer;
|
|
70
|
+
observer = undefined;
|
|
71
|
+
await previous?.dispose();
|
|
72
|
+
if (stamp !== generation) return;
|
|
73
|
+
if (rootSessionId !== ctx.sessionManager.getSessionId()) {
|
|
74
|
+
await Promise.all(
|
|
75
|
+
[...children.values()].map(async (child) => {
|
|
76
|
+
await child.observer?.dispose();
|
|
77
|
+
}),
|
|
78
|
+
);
|
|
79
|
+
children.clear();
|
|
80
|
+
}
|
|
81
|
+
rootSessionId = ctx.sessionManager.getSessionId();
|
|
82
|
+
unsubscribe ??= pi.events.on("pi-minimal-subagents:observe-session", attachChild);
|
|
83
|
+
observed = undefined;
|
|
84
|
+
error = undefined;
|
|
85
|
+
privateSession =
|
|
86
|
+
isAdvisorSession(ctx.sessionManager) ||
|
|
87
|
+
ctx.sessionManager
|
|
88
|
+
.getBranch()
|
|
89
|
+
.some(
|
|
90
|
+
(entry) => entry.type === "custom" && entry.customType === "minimal-subagents.identity",
|
|
91
|
+
);
|
|
92
|
+
if (privateSession) return;
|
|
93
|
+
const found = discoverPiAgentSession(pi, AgentSession);
|
|
94
|
+
if (found.ok) {
|
|
95
|
+
observed = found.session;
|
|
96
|
+
layers = readAdvisorLayers(observed.settingsManager);
|
|
97
|
+
await refresh(ctx);
|
|
98
|
+
} else error = found.warning;
|
|
99
|
+
});
|
|
100
|
+
pi.on("session_tree", async (_event, ctx) => {
|
|
101
|
+
generation++;
|
|
102
|
+
observer?.reset();
|
|
103
|
+
for (const child of children.values()) child.observer?.reset();
|
|
104
|
+
await refresh(ctx);
|
|
105
|
+
});
|
|
106
|
+
pi.on("session_compact", () => observer?.reset());
|
|
107
|
+
pi.on("model_select", () => observer?.reset());
|
|
108
|
+
pi.on("before_agent_start", () => observer?.beforeTask());
|
|
109
|
+
pi.on("agent_settled", () => observer?.settled());
|
|
110
|
+
pi.on("session_shutdown", async () => {
|
|
111
|
+
generation++;
|
|
112
|
+
observed = undefined;
|
|
113
|
+
unsubscribe?.();
|
|
114
|
+
unsubscribe = undefined;
|
|
115
|
+
await Promise.all([
|
|
116
|
+
observer?.dispose(),
|
|
117
|
+
...[...children.values()].map((child) => child.observer?.dispose()),
|
|
118
|
+
]);
|
|
119
|
+
observer = undefined;
|
|
120
|
+
children.clear();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
function configureChild(
|
|
124
|
+
session: AgentSession,
|
|
125
|
+
child: WatchedChild,
|
|
126
|
+
settings: AdvisorConfig,
|
|
127
|
+
): void {
|
|
128
|
+
const config = { ...settings, enabled: settings.enabled && settings.includeSubagents };
|
|
129
|
+
if (child.observer) child.observer.configure(config);
|
|
130
|
+
else
|
|
131
|
+
child.observer = new AdvisorObserver(session, config, "owned-child", {
|
|
132
|
+
resourceInputs: child.resourceInputs,
|
|
133
|
+
onError: (message) => {
|
|
134
|
+
if (children.get(session) === child)
|
|
135
|
+
pi.appendEntry("pi-advisor-child", {
|
|
136
|
+
agentId: child.agentId,
|
|
137
|
+
state: "paused",
|
|
138
|
+
error: message,
|
|
139
|
+
});
|
|
140
|
+
},
|
|
141
|
+
onIntervention: (finding) => {
|
|
142
|
+
if (children.get(session) === child)
|
|
143
|
+
pi.appendEntry("pi-advisor-child", { agentId: child.agentId, ...finding });
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 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.
|
|
149
|
+
function attachChild(payload: unknown): void {
|
|
150
|
+
if (!Value.Check(childRequestSchema, payload) || !(payload.session instanceof AgentSession))
|
|
151
|
+
return;
|
|
152
|
+
if (!observed) {
|
|
153
|
+
const found = discoverPiAgentSession(pi, AgentSession);
|
|
154
|
+
if (!found.ok) return;
|
|
155
|
+
observed = found.session;
|
|
156
|
+
layers = readAdvisorLayers(observed.settingsManager);
|
|
157
|
+
rootSessionId = observed.sessionManager.getSessionId();
|
|
158
|
+
}
|
|
159
|
+
if (privateSession || payload.rootSessionId !== rootSessionId || children.has(payload.session))
|
|
160
|
+
return;
|
|
161
|
+
const resources = payload.session.resourceLoader.getExtensions();
|
|
162
|
+
const extensions = resources.extensions.map(({ path, resolvedPath, sourceInfo, hidden }) => ({
|
|
163
|
+
path,
|
|
164
|
+
resolvedPath,
|
|
165
|
+
sourceInfo,
|
|
166
|
+
hidden,
|
|
167
|
+
}));
|
|
168
|
+
if (
|
|
169
|
+
!isDeepStrictEqual(extensions, payload.resourceInputs.extensions) ||
|
|
170
|
+
!isDeepStrictEqual(resources.runtime.flagValues, payload.resourceInputs.flagValues)
|
|
171
|
+
)
|
|
172
|
+
return;
|
|
173
|
+
const child: WatchedChild = {
|
|
174
|
+
agentId: payload.agentId,
|
|
175
|
+
resourceInputs: {
|
|
176
|
+
agentDir: payload.resourceInputs.agentDir,
|
|
177
|
+
extensions,
|
|
178
|
+
flagValues: new Map(resources.runtime.flagValues),
|
|
179
|
+
},
|
|
180
|
+
observer: undefined,
|
|
181
|
+
};
|
|
182
|
+
const session = payload.session;
|
|
183
|
+
children.set(session, child);
|
|
184
|
+
try {
|
|
185
|
+
configureChild(session, child, readAdvisorSettings(observed, layers).settings);
|
|
186
|
+
} catch (cause) {
|
|
187
|
+
error = cause instanceof Error ? cause.message : String(cause);
|
|
188
|
+
}
|
|
189
|
+
payload.attach({
|
|
190
|
+
beginTurn: () => child.observer?.beforeTask(),
|
|
191
|
+
finishTurn: async () => {
|
|
192
|
+
await child.observer?.finishOwnedTurn();
|
|
193
|
+
},
|
|
194
|
+
abort: async () => {
|
|
195
|
+
await child.observer?.abort();
|
|
196
|
+
},
|
|
197
|
+
dispose: async () => {
|
|
198
|
+
children.delete(session);
|
|
199
|
+
await child.observer?.dispose();
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function refresh(ctx: ExtensionContext): Promise<void> {
|
|
205
|
+
if (!observed || privateSession) return;
|
|
206
|
+
try {
|
|
207
|
+
const { settings } = readAdvisorSettings(observed, layers);
|
|
208
|
+
if (observer) observer.configure(settings);
|
|
209
|
+
else
|
|
210
|
+
observer = new AdvisorObserver(
|
|
211
|
+
observed,
|
|
212
|
+
settings,
|
|
213
|
+
ctx.hasUI ? "interactive" : "headless-root",
|
|
214
|
+
{
|
|
215
|
+
onError: (message) =>
|
|
216
|
+
pi.appendEntry("pi-advisor-status", { state: "paused", error: message }),
|
|
217
|
+
},
|
|
218
|
+
);
|
|
219
|
+
for (const [session, child] of children) configureChild(session, child, settings);
|
|
220
|
+
} catch (cause) {
|
|
221
|
+
error = cause instanceof Error ? cause.message : String(cause);
|
|
222
|
+
await Promise.all([
|
|
223
|
+
observer?.dispose(),
|
|
224
|
+
...[...children.values()].map((child) => child.observer?.dispose()),
|
|
225
|
+
]);
|
|
226
|
+
observer = undefined;
|
|
227
|
+
for (const child of children.values()) child.observer = undefined;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function status(ctx: ExtensionContext): void {
|
|
232
|
+
try {
|
|
233
|
+
if (!observed) throw new Error(error ?? "Advisor session is unavailable");
|
|
234
|
+
const resolved = readAdvisorSettings(observed, layers);
|
|
235
|
+
pi.appendEntry("pi-advisor-status", {
|
|
236
|
+
state: resolved.settings.enabled ? "armed" : "disabled",
|
|
237
|
+
...resolved,
|
|
238
|
+
backlog: 0,
|
|
239
|
+
usage: null,
|
|
240
|
+
cost: null,
|
|
241
|
+
...observer?.status,
|
|
242
|
+
children: [...children.values()].map((child) => ({
|
|
243
|
+
agentId: child.agentId,
|
|
244
|
+
...child.observer?.status,
|
|
245
|
+
})),
|
|
246
|
+
error: error ?? observer?.status.lastError ?? null,
|
|
247
|
+
});
|
|
248
|
+
} catch (cause) {
|
|
249
|
+
error = cause instanceof Error ? cause.message : String(cause);
|
|
250
|
+
pi.appendEntry("pi-advisor-status", { state: "paused", error, usage: null, cost: null });
|
|
251
|
+
}
|
|
252
|
+
if (error) ctx.ui.notify(`Advisor: ${error}`, "error");
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
pi.registerCommand("advisor", {
|
|
256
|
+
description: "Advisor on, off, inherit, scoped settings, prompt editor, or status",
|
|
257
|
+
getArgumentCompletions: completeAdvisorCommandArguments,
|
|
258
|
+
async handler(args, ctx) {
|
|
259
|
+
if (privateSession) {
|
|
260
|
+
pi.appendEntry("pi-advisor-status", {
|
|
261
|
+
state: "private",
|
|
262
|
+
error: "Private Advisor Sessions cannot create another Advisor",
|
|
263
|
+
});
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
const subject = observed;
|
|
267
|
+
let stamp = generation;
|
|
268
|
+
const isCurrent = () => generation === stamp && observed === subject;
|
|
269
|
+
try {
|
|
270
|
+
if (!subject) throw new Error(error ?? "Advisor session is unavailable");
|
|
271
|
+
const command = parseAdvisorCommand(args);
|
|
272
|
+
if (command.action !== "status") {
|
|
273
|
+
stamp = ++generation;
|
|
274
|
+
let change: AdvisorChange;
|
|
275
|
+
if (command.action === "prompt") {
|
|
276
|
+
if (!ctx.hasUI)
|
|
277
|
+
throw new Error("Advisor prompt editing requires UI; use /advisor set prompt <JSON>");
|
|
278
|
+
const edited = await ctx.ui.editor(
|
|
279
|
+
"Advisor Prompt",
|
|
280
|
+
readAdvisorSettings(subject, layers).settings.prompt,
|
|
281
|
+
);
|
|
282
|
+
if (!isCurrent() || edited === undefined) return;
|
|
283
|
+
change = {
|
|
284
|
+
action: "set",
|
|
285
|
+
patch: parseAdvisorOptions({ prompt: edited }, command.scope),
|
|
286
|
+
};
|
|
287
|
+
} else change = command;
|
|
288
|
+
if (command.scope === "session") {
|
|
289
|
+
const overrides = readAdvisorOverrides(subject.sessionManager);
|
|
290
|
+
if (change.action === "inherit") delete overrides[change.key];
|
|
291
|
+
else Object.assign(overrides, change.patch);
|
|
292
|
+
pi.appendEntry("pi-advisor-settings", { version: 1, overrides });
|
|
293
|
+
} else {
|
|
294
|
+
const updated = await writeAdvisorSettings(
|
|
295
|
+
subject.settingsManager,
|
|
296
|
+
command.scope,
|
|
297
|
+
change,
|
|
298
|
+
isCurrent,
|
|
299
|
+
);
|
|
300
|
+
if (!isCurrent() || !updated) return;
|
|
301
|
+
layers[command.scope] = updated;
|
|
302
|
+
}
|
|
303
|
+
error = undefined;
|
|
304
|
+
await refresh(ctx);
|
|
305
|
+
}
|
|
306
|
+
} catch (cause) {
|
|
307
|
+
if (!isCurrent()) return;
|
|
308
|
+
error = cause instanceof Error ? cause.message : String(cause);
|
|
309
|
+
}
|
|
310
|
+
if (isCurrent()) status(ctx);
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
}
|