@jwilger/pi-development-system 0.81.0 → 0.83.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 +3 -2
- package/package.json +1 -1
- package/src/context/devsys-tool.ts +1 -1
- package/src/context/intent-trigger.ts +2 -2
- package/src/context/system-prompt.ts +1 -1
- package/src/jev/questions/test-change.ts +1 -1
- package/src/subagents/VENDORED.md +1 -0
- package/src/subagents/index.ts +164 -72
- package/src/subagents/orch/persist-signature.ts +24 -0
package/README.md
CHANGED
|
@@ -22,8 +22,9 @@ what the current phase expects. When you say a slice is finished with no review
|
|
|
22
22
|
it tells you to start one. The slash commands (`/devsys-start`, `/devsys-plan`, `/devsys-review`)
|
|
23
23
|
are shortcuts to the same tools.
|
|
24
24
|
|
|
25
|
-
With `codemode` enabled (`"defaultTools": ["+codemode"]` in pi settings), rarely used tools
|
|
26
|
-
the `judge_*` Jev wrappers
|
|
25
|
+
With `codemode` enabled (`"defaultTools": ["+codemode"]` in pi settings), rarely used tools are
|
|
26
|
+
reached through scripts and the `judge_*` Jev wrappers exist for scripts only; without codemode
|
|
27
|
+
the rarely used tools are declared directly and the wrappers are absent.
|
|
27
28
|
|
|
28
29
|
## Development
|
|
29
30
|
|
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@ import { phaseGuide } from "./phase-guide.ts";
|
|
|
9
9
|
* system prompt's state section; calling the tool returns the guidance for it.
|
|
10
10
|
*/
|
|
11
11
|
export const DEVSYS_TOOL_DESCRIPTION =
|
|
12
|
-
"What the development system expects in the current workflow phase (
|
|
12
|
+
"What the development system expects in the current workflow phase (the system prompt names it as of the start of the prompt; this call is live), and which tools to use. Call it when unsure what comes next.";
|
|
13
13
|
|
|
14
14
|
/** `devsys`: a model-only tool that returns the current phase's guidance. */
|
|
15
15
|
export function createPhaseTool(deps: { state: SessionState }): ToolDefinition {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { DevsysState } from "../core/types.ts";
|
|
2
2
|
import type { Jev } from "../jev/client.ts";
|
|
3
|
-
import { type Intent, judgeIntent } from "../jev/questions/intent.ts";
|
|
3
|
+
import { type Intent, judgeIntent, requestText } from "../jev/questions/intent.ts";
|
|
4
4
|
|
|
5
5
|
/** Probability at or above which an intent is acted on. */
|
|
6
6
|
export const INTENT_AT = 0.7;
|
|
@@ -35,7 +35,7 @@ export async function intentLineFor(
|
|
|
35
35
|
state: DevsysState,
|
|
36
36
|
jev: Jev,
|
|
37
37
|
): Promise<string | undefined> {
|
|
38
|
-
if (state.phase !== "idle" || prompt
|
|
38
|
+
if (state.phase !== "idle" || requestText(prompt) === "" || jev.availability() === "offline") {
|
|
39
39
|
return undefined;
|
|
40
40
|
}
|
|
41
41
|
const judged = await judgeIntent(jev, { prompt, phase: state.phase });
|
|
@@ -28,7 +28,7 @@ export function buildPrinciplesSection(nonNegotiables: string): string {
|
|
|
28
28
|
*/
|
|
29
29
|
export function buildStateSection(state: DevsysState): string {
|
|
30
30
|
const lines = [
|
|
31
|
-
"##
|
|
31
|
+
"## Workflow state at the start of this prompt (call the devsys tool for the live phase)",
|
|
32
32
|
`- phase: ${state.phase}`,
|
|
33
33
|
`- sizing: ${state.sizing ?? "none"}`,
|
|
34
34
|
`- active slice: ${state.activeSlice ?? "none"}`,
|
|
@@ -79,7 +79,7 @@ export async function judgeTestChange(
|
|
|
79
79
|
? { before: input.before ?? "", after: "" }
|
|
80
80
|
: changeWindow(input.before ?? "", input.after);
|
|
81
81
|
const state = {
|
|
82
|
-
path: input.path,
|
|
82
|
+
path: clip(input.path).slice(0, 400),
|
|
83
83
|
before: clip(windowed.before),
|
|
84
84
|
after: input.after === undefined ? "" : clip(windowed.after),
|
|
85
85
|
deleted: input.after === undefined,
|
|
@@ -41,3 +41,4 @@ SOFTWARE.
|
|
|
41
41
|
6. Empty scope (I5 review): `orch/runtime.ts` `availableScopedModels` treats an empty `/scoped-models` as no restriction (pi's meaning) instead of "matches nothing", because every devsys agent declares `models:` and would otherwise be unspawnable by default. Pinned models are also re-applied on resume (`resolveInitialSettings`), since a restored session may hold only inherited history.
|
|
42
42
|
7. Bundled agents outside `src/subagents`: `agents/researcher.md` (devsys `models:` family list) and `agents/reviewer.md` (devsys `models:` list and packet format; description rewritten; upstream `icon` and `modelSuggestions` removed) differ from upstream; `agents/{advisor,implementer,lens-*}.md` are new. The agent file format itself is unchanged.
|
|
43
43
|
8. Thread cap (I7b.1): `orch/manager.ts` `archiveFinished` replaces the hard "Total thread limit reached" check. At `maxThreads` the oldest finished (completed/failed/stopped) thread without a retained descendant is dropped from the registry to make room; the limit is refused only when every kept thread is live, paused or a parent of one. Upstream kept finished threads forever, so long autonomous runs stalled on their own history. A dropped thread cannot be resumed with `agent_steer` (its session file stays on disk).
|
|
44
|
+
9. Registry writes (OOM fix): `index.ts` `persist` signs the registry with `orch/persist-signature.ts` `registrySignature`, which leaves a starting or running thread's status line and partial output out of the change signature. Upstream wrote the whole registry (every thread, about 12 KB each) on every status change; pi never rewrites session entries, so one session grew to 2.3 GB (5,909 snapshots) and pi died loading it. Writes now happen when a thread is added or removed, starts, settles, pauses or is stopped.
|
package/src/subagents/index.ts
CHANGED
|
@@ -1,21 +1,31 @@
|
|
|
1
|
-
//
|
|
1
|
+
// biome-ignore-all lint/suspicious/noUnnecessaryConditions: vendored upstream TUI/config code with no tests; rewriting it risks behaviour (see src/subagents/VENDORED.md)
|
|
2
|
+
// biome-ignore-all lint/suspicious/useAwait: vendored upstream TUI/config code with no tests; rewriting it risks behaviour (see src/subagents/VENDORED.md)
|
|
3
|
+
// biome-ignore-all lint/complexity/noVoid: vendored upstream TUI/config code with no tests; rewriting it risks behaviour (see src/subagents/VENDORED.md)
|
|
4
|
+
// biome-ignore-all lint/style/noNestedTernary: vendored upstream TUI/config code with no tests; rewriting it risks behaviour (see src/subagents/VENDORED.md)
|
|
5
|
+
// biome-ignore-all lint/style/noNonNullAssertion: vendored upstream TUI/config code with no tests; rewriting it risks behaviour (see src/subagents/VENDORED.md)
|
|
6
|
+
// biome-ignore-all lint/complexity/noExcessiveCognitiveComplexity: vendored upstream TUI/config code with no tests; rewriting it risks behaviour (see src/subagents/VENDORED.md)
|
|
2
7
|
import { randomUUID } from "node:crypto";
|
|
3
|
-
import { buildSessionContext, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
4
8
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import { buildSessionContext, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
5
10
|
import { fuzzyFilter } from "@earendil-works/pi-tui";
|
|
6
|
-
import {
|
|
7
|
-
import { IMPORT_REQUEST_PREFIX, markImportOffered, offerAgentImport } from "./prefs/agent-import.ts";
|
|
11
|
+
import { createInheritedToolSource } from "./orch/inherited-tools.ts";
|
|
8
12
|
import { ThreadManager } from "./orch/manager.ts";
|
|
13
|
+
import { registrySignature } from "./orch/persist-signature.ts";
|
|
14
|
+
import { subagentPrompt } from "./orch/prompt.ts";
|
|
15
|
+
import { createDriverFactory } from "./orch/runtime.ts";
|
|
16
|
+
import { agentTools } from "./orch/tools.ts";
|
|
17
|
+
import {
|
|
18
|
+
IMPORT_REQUEST_PREFIX,
|
|
19
|
+
markImportOffered,
|
|
20
|
+
offerAgentImport,
|
|
21
|
+
} from "./prefs/agent-import.ts";
|
|
22
|
+
import { ConfigStore } from "./prefs/config.ts";
|
|
9
23
|
import { loadManagerSettings } from "./prefs/settings.ts";
|
|
24
|
+
import type { SavedThread, ThreadEvent } from "./types.ts";
|
|
25
|
+
import { AgentNavigationEditor } from "./ui/agent-navigation-editor.ts";
|
|
10
26
|
import { configureAgents } from "./ui/settings-ui.ts";
|
|
11
27
|
import { AgentNavigationController, showAgentStatus } from "./ui/status-ui.ts";
|
|
12
|
-
import { AgentNavigationEditor } from "./ui/agent-navigation-editor.ts";
|
|
13
|
-
import { createDriverFactory } from "./orch/runtime.ts";
|
|
14
|
-
import { createInheritedToolSource } from "./orch/inherited-tools.ts";
|
|
15
|
-
import { subagentPrompt } from "./orch/prompt.ts";
|
|
16
|
-
import { agentTools } from "./orch/tools.ts";
|
|
17
28
|
import { editAgentTypes, updateWidget } from "./ui/ui.ts";
|
|
18
|
-
import type { SavedThread, ThreadEvent } from "./types.ts";
|
|
19
29
|
|
|
20
30
|
const REGISTRY_ENTRY = "pi-subagent:registry:v1";
|
|
21
31
|
const ROOT_MAILBOX_ENTRY = "pi-subagent:root-mailbox:v1";
|
|
@@ -81,32 +91,55 @@ export default function piSubagent(pi: ExtensionAPI): void {
|
|
|
81
91
|
if (existingFactory && existingFactory !== navigationEditorFactory) return;
|
|
82
92
|
// The host only transfers raw text, not cursor/undo/expanded-paste state.
|
|
83
93
|
if (ctx.ui.getEditorText?.()) return;
|
|
84
|
-
const history = buildSessionContext(ctx.sessionManager.getBranch()).messages.flatMap(
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
94
|
+
const history = buildSessionContext(ctx.sessionManager.getBranch()).messages.flatMap(
|
|
95
|
+
(message) => {
|
|
96
|
+
if (message.role !== "user") return [];
|
|
97
|
+
const content = message.content;
|
|
98
|
+
return [
|
|
99
|
+
typeof content === "string"
|
|
100
|
+
? content
|
|
101
|
+
: content
|
|
102
|
+
.filter((block) => block.type === "text")
|
|
103
|
+
.map((block) => block.text)
|
|
104
|
+
.join(""),
|
|
105
|
+
];
|
|
106
|
+
},
|
|
107
|
+
);
|
|
90
108
|
navigationEditorFactory = (tui, theme, keys) => {
|
|
91
109
|
const editor = new AgentNavigationEditor(tui, theme, keys, {
|
|
92
110
|
// Initial startup hydrates after session_start; replacement editors do not.
|
|
93
|
-
|
|
111
|
+
...(seedHistory ? { initialHistory: history } : {}),
|
|
94
112
|
generation: () => navigationGeneration,
|
|
95
|
-
canOpen: () =>
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
113
|
+
canOpen: () =>
|
|
114
|
+
manager !== undefined &&
|
|
115
|
+
context?.mode === "tui" &&
|
|
116
|
+
limits.subagentMode !== "off" &&
|
|
117
|
+
!navigation.isOpen &&
|
|
118
|
+
manager
|
|
119
|
+
.list()
|
|
120
|
+
.some(
|
|
121
|
+
(thread) =>
|
|
122
|
+
thread.path !== "/root" &&
|
|
123
|
+
(thread.state === "starting" || thread.state === "running"),
|
|
124
|
+
),
|
|
125
|
+
openTree: () =>
|
|
126
|
+
manager && context
|
|
127
|
+
? navigation.open(context, manager.scope("/root"), undefined, limits.nerdFontIcons)
|
|
128
|
+
: undefined,
|
|
129
|
+
canCollapse: () =>
|
|
130
|
+
manager !== undefined &&
|
|
131
|
+
context?.mode === "tui" &&
|
|
132
|
+
limits.subagentMode !== "off" &&
|
|
133
|
+
!navigation.isOpen &&
|
|
134
|
+
!widgetCollapsed &&
|
|
135
|
+
limits.widgetMode === "full" &&
|
|
136
|
+
widgetThreads().some((thread) => thread.path !== "/root"),
|
|
104
137
|
collapseWidget: () => {
|
|
105
138
|
widgetCollapsed = true;
|
|
106
139
|
if (context) refreshWidget(context);
|
|
107
140
|
},
|
|
108
|
-
onError: (error) =>
|
|
109
|
-
error instanceof Error ? error.message : String(error), "error"),
|
|
141
|
+
onError: (error) =>
|
|
142
|
+
context?.ui.notify(error instanceof Error ? error.message : String(error), "error"),
|
|
110
143
|
});
|
|
111
144
|
navigationEditor = editor;
|
|
112
145
|
return editor;
|
|
@@ -120,7 +153,10 @@ export default function piSubagent(pi: ExtensionAPI): void {
|
|
|
120
153
|
// Migration requires the root's file tools and must not delegate source prompts.
|
|
121
154
|
if (limits.subagentMode === "orchestration") {
|
|
122
155
|
if (!firstRun)
|
|
123
|
-
ctx.ui.notify(
|
|
156
|
+
ctx.ui.notify(
|
|
157
|
+
"Agent import requires Opportunistic mode. Change Subagent Mode in /agents settings.",
|
|
158
|
+
"info",
|
|
159
|
+
);
|
|
124
160
|
return;
|
|
125
161
|
}
|
|
126
162
|
if (importInProgress) return;
|
|
@@ -140,12 +176,15 @@ export default function piSubagent(pi: ExtensionAPI): void {
|
|
|
140
176
|
if (!manager) throw new Error("Subagent threads are not initialized; start a Pi session first");
|
|
141
177
|
return manager;
|
|
142
178
|
};
|
|
143
|
-
const widgetThreads = () =>
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
179
|
+
const widgetThreads = () =>
|
|
180
|
+
requireManager()
|
|
181
|
+
.list()
|
|
182
|
+
.filter((thread) => {
|
|
183
|
+
// Resuming a retained session makes it visible for this task, even after it settles again.
|
|
184
|
+
if (thread.state === "starting" || thread.state === "running")
|
|
185
|
+
hiddenWidgetThreads.delete(thread.path);
|
|
186
|
+
return !hiddenWidgetThreads.has(thread.path);
|
|
187
|
+
});
|
|
149
188
|
const requireContext = () => {
|
|
150
189
|
if (!context) throw new Error("No active Pi session");
|
|
151
190
|
return context;
|
|
@@ -153,7 +192,7 @@ export default function piSubagent(pi: ExtensionAPI): void {
|
|
|
153
192
|
const persist = () => {
|
|
154
193
|
if (!manager) return;
|
|
155
194
|
const threads = manager.saved();
|
|
156
|
-
const signature =
|
|
195
|
+
const signature = registrySignature(threads);
|
|
157
196
|
if (signature !== persistenceSignature) {
|
|
158
197
|
pi.appendEntry(REGISTRY_ENTRY, {
|
|
159
198
|
version: 1,
|
|
@@ -187,44 +226,72 @@ export default function piSubagent(pi: ExtensionAPI): void {
|
|
|
187
226
|
};
|
|
188
227
|
const pendingSummaries = (ctx: ExtensionContext) => {
|
|
189
228
|
const entries = ctx.sessionManager.getBranch();
|
|
190
|
-
const summarized = new Set(
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
229
|
+
const summarized = new Set(
|
|
230
|
+
entries.flatMap((entry) => {
|
|
231
|
+
if (entry.type !== "custom_message" || entry.customType !== ROOT_SUMMARY_MESSAGE) return [];
|
|
232
|
+
const ids = (entry.details as { mailboxIds?: unknown } | undefined)?.mailboxIds;
|
|
233
|
+
return Array.isArray(ids) ? ids.filter((id): id is string => typeof id === "string") : [];
|
|
234
|
+
}),
|
|
235
|
+
);
|
|
195
236
|
return entries.flatMap((entry) => {
|
|
196
237
|
if (entry.type !== "custom" || entry.customType !== ROOT_MAILBOX_ENTRY) return [];
|
|
197
238
|
const notification = entry.data as RootNotification | undefined;
|
|
198
239
|
return notification?.rootSessionId === ctx.sessionManager.getSessionId() &&
|
|
199
|
-
notification.finalRecap === true &&
|
|
240
|
+
notification.finalRecap === true &&
|
|
241
|
+
typeof notification.content === "string" &&
|
|
200
242
|
typeof notification.details?.mailboxId === "string" &&
|
|
201
|
-
!summarized.has(notification.details.mailboxId)
|
|
243
|
+
!summarized.has(notification.details.mailboxId)
|
|
244
|
+
? [notification]
|
|
245
|
+
: [];
|
|
202
246
|
});
|
|
203
247
|
};
|
|
204
248
|
const scheduleSummary = (ctx: ExtensionContext) => {
|
|
205
|
-
if (
|
|
206
|
-
|
|
249
|
+
if (
|
|
250
|
+
!limits.finalRecap ||
|
|
251
|
+
limits.subagentMode === "off" ||
|
|
252
|
+
summaryRunning ||
|
|
253
|
+
suspendingSummaries ||
|
|
254
|
+
summaryTimer !== undefined ||
|
|
255
|
+
ctx.isIdle() === false ||
|
|
256
|
+
pendingSummaries(ctx).length === 0
|
|
257
|
+
)
|
|
258
|
+
return;
|
|
207
259
|
const currentGeneration = generation;
|
|
208
260
|
// Coalesce siblings finishing together and leave the settlement boundary before
|
|
209
261
|
// starting a new turn. Recheck settings/session/idle state after the deferral.
|
|
210
262
|
summaryTimer = setTimeout(() => {
|
|
211
263
|
summaryTimer = undefined;
|
|
212
|
-
if (
|
|
213
|
-
|
|
264
|
+
if (
|
|
265
|
+
currentGeneration !== generation ||
|
|
266
|
+
context?.sessionManager !== ctx.sessionManager ||
|
|
267
|
+
!limits.finalRecap ||
|
|
268
|
+
limits.subagentMode === "off" ||
|
|
269
|
+
suspendingSummaries ||
|
|
270
|
+
ctx.isIdle() === false
|
|
271
|
+
)
|
|
272
|
+
return;
|
|
214
273
|
const notifications = pendingSummaries(ctx);
|
|
215
|
-
if (
|
|
274
|
+
if (notifications.length === 0) return;
|
|
216
275
|
summaryRunning = true;
|
|
217
276
|
try {
|
|
218
|
-
pi.sendMessage(
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
277
|
+
pi.sendMessage(
|
|
278
|
+
{
|
|
279
|
+
customType: ROOT_SUMMARY_MESSAGE,
|
|
280
|
+
content:
|
|
281
|
+
"Summarize the newly finished asynchronous subagent results for the user. " +
|
|
282
|
+
"Their results are in the preceding subagent notifications; use agent_output if more detail is needed. " +
|
|
283
|
+
"Give a concise main-thread answer covering results and any failures. " +
|
|
284
|
+
"Do not spawn or resume agents solely to summarize.\nAgents: " +
|
|
285
|
+
[...new Set(notifications.map((notification) => notification.details.path))].join(
|
|
286
|
+
", ",
|
|
287
|
+
),
|
|
288
|
+
display: false,
|
|
289
|
+
details: {
|
|
290
|
+
mailboxIds: notifications.map((notification) => notification.details.mailboxId),
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
{ triggerTurn: true },
|
|
294
|
+
);
|
|
228
295
|
} catch (error) {
|
|
229
296
|
summaryRunning = false;
|
|
230
297
|
warnDelivery(error);
|
|
@@ -278,7 +345,10 @@ export default function piSubagent(pi: ExtensionAPI): void {
|
|
|
278
345
|
content: message,
|
|
279
346
|
// Foreground results already handled by a busy main thread do not need
|
|
280
347
|
// another turn. Completions during a summary do need a follow-up summary.
|
|
281
|
-
finalRecap:
|
|
348
|
+
finalRecap:
|
|
349
|
+
limits.finalRecap &&
|
|
350
|
+
!suspendingSummaries &&
|
|
351
|
+
event.kind === "settled" &&
|
|
282
352
|
(thread.state === "completed" || thread.state === "failed") &&
|
|
283
353
|
(requireContext().isIdle() || summaryRunning),
|
|
284
354
|
details: {
|
|
@@ -335,8 +405,14 @@ export default function piSubagent(pi: ExtensionAPI): void {
|
|
|
335
405
|
const refreshWidget = (ctx: ExtensionContext) => {
|
|
336
406
|
if (limits.subagentMode === "off") {
|
|
337
407
|
if (ctx.hasUI) ctx.ui.setWidget("pi-subagent", undefined);
|
|
338
|
-
} else
|
|
339
|
-
|
|
408
|
+
} else
|
|
409
|
+
updateWidget(
|
|
410
|
+
ctx,
|
|
411
|
+
widgetThreads(),
|
|
412
|
+
widgetCollapsed ? "minimal" : limits.widgetMode,
|
|
413
|
+
limits.nerdFontIcons,
|
|
414
|
+
rootTurnEnded,
|
|
415
|
+
);
|
|
340
416
|
};
|
|
341
417
|
pi.on("agent_start", async (_event, ctx) => {
|
|
342
418
|
// A new task must not resurrect the previous task's settled agents. Automatic
|
|
@@ -356,7 +432,7 @@ export default function piSubagent(pi: ExtensionAPI): void {
|
|
|
356
432
|
// A migration can span clarification turns. Reload after each root turn, without converting files.
|
|
357
433
|
if (limits.subagentMode === "off" || !migrationRequested) return;
|
|
358
434
|
store.reload();
|
|
359
|
-
if (store.diagnostics.length) context?.ui.notify(store.diagnostics.join("\n"), "warning");
|
|
435
|
+
if (store.diagnostics.length > 0) context?.ui.notify(store.diagnostics.join("\n"), "warning");
|
|
360
436
|
});
|
|
361
437
|
syncTools();
|
|
362
438
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
@@ -369,12 +445,17 @@ export default function piSubagent(pi: ExtensionAPI): void {
|
|
|
369
445
|
ctx.ui.notify(`Could not save import onboarding state: ${String(error)}`, "warning");
|
|
370
446
|
}
|
|
371
447
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
448
|
+
// A section, not a returned `systemPrompt`: returning one makes pi force the whole prompt as
|
|
449
|
+
// the head of every request, so any change to our state section would miss the prompt cache.
|
|
450
|
+
event.systemPromptOptions.sections["pi-subagent"] = prompt;
|
|
451
|
+
return undefined;
|
|
375
452
|
});
|
|
376
453
|
|
|
377
|
-
const attachSession = async (
|
|
454
|
+
const attachSession = async (
|
|
455
|
+
ctx: ExtensionContext,
|
|
456
|
+
installEditor = false,
|
|
457
|
+
seedHistory = true,
|
|
458
|
+
) => {
|
|
378
459
|
cancelSummary();
|
|
379
460
|
const token = ++generation;
|
|
380
461
|
closeNavigation(installEditor);
|
|
@@ -437,7 +518,7 @@ export default function piSubagent(pi: ExtensionAPI): void {
|
|
|
437
518
|
restoreRootMailbox(ctx);
|
|
438
519
|
refreshWidget(ctx);
|
|
439
520
|
const diagnostics = [...store.diagnostics, ...settingsDiagnostics];
|
|
440
|
-
if (limits.subagentMode !== "off" && diagnostics.length)
|
|
521
|
+
if (limits.subagentMode !== "off" && diagnostics.length > 0)
|
|
441
522
|
ctx.ui.notify(diagnostics.join("\n"), "warning");
|
|
442
523
|
};
|
|
443
524
|
pi.on("session_start", async (event, ctx) => {
|
|
@@ -511,10 +592,11 @@ export default function piSubagent(pi: ExtensionAPI): void {
|
|
|
511
592
|
refreshWidget(ctx);
|
|
512
593
|
restoreRootMailbox(ctx);
|
|
513
594
|
ctx.ui.notify(
|
|
514
|
-
diagnostics.length
|
|
595
|
+
diagnostics.length > 0
|
|
515
596
|
? diagnostics.join("\n")
|
|
516
597
|
: `Loaded ${store.list().length} agent types; maximum ${limits.maxLevels} levels`,
|
|
517
|
-
|
|
598
|
+
|
|
599
|
+
diagnostics.length > 0 ? "warning" : "info",
|
|
518
600
|
);
|
|
519
601
|
} else if (!command || command === "settings") {
|
|
520
602
|
await configureAgents(ctx, {
|
|
@@ -527,13 +609,23 @@ export default function piSubagent(pi: ExtensionAPI): void {
|
|
|
527
609
|
syncTools();
|
|
528
610
|
refreshWidget(ctx);
|
|
529
611
|
restoreRootMailbox(ctx);
|
|
530
|
-
if (diagnostics.length) ctx.ui.notify(diagnostics.join("\n"), "warning");
|
|
612
|
+
if (diagnostics.length > 0) ctx.ui.notify(diagnostics.join("\n"), "warning");
|
|
531
613
|
},
|
|
532
614
|
});
|
|
533
615
|
} else if (command === "status") {
|
|
534
|
-
await showAgentStatus(
|
|
616
|
+
await showAgentStatus(
|
|
617
|
+
ctx,
|
|
618
|
+
requireManager().scope("/root"),
|
|
619
|
+
undefined,
|
|
620
|
+
limits.nerdFontIcons,
|
|
621
|
+
);
|
|
535
622
|
} else if (command === "tree") {
|
|
536
|
-
await navigation.open(
|
|
623
|
+
await navigation.open(
|
|
624
|
+
ctx,
|
|
625
|
+
requireManager().scope("/root"),
|
|
626
|
+
rest.join(" ") || undefined,
|
|
627
|
+
limits.nerdFontIcons,
|
|
628
|
+
);
|
|
537
629
|
} else
|
|
538
630
|
ctx.ui.notify(
|
|
539
631
|
"Usage: /agents [tree [path] | status | settings | types | import | reload]",
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { SavedThread } from "../types.ts";
|
|
2
|
+
|
|
3
|
+
const LIVE = new Set(["starting", "running"]);
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* What decides whether the registry needs writing again.
|
|
7
|
+
*
|
|
8
|
+
* pi appends every `appendEntry` to the session file and never rewrites it, and each
|
|
9
|
+
* registry entry holds every thread (about 12 KB each). A running thread changes its
|
|
10
|
+
* status line on every tool call, so signing the full snapshot wrote hundreds of
|
|
11
|
+
* kilobytes per tool call: one session reached 2.3 GB and pi ran out of memory loading
|
|
12
|
+
* it. A live thread's status and output are not needed to restore it (a reload marks it
|
|
13
|
+
* interrupted), so they stay out of the signature. Starting, settling, pausing and
|
|
14
|
+
* adding or removing a thread still write.
|
|
15
|
+
*/
|
|
16
|
+
export function registrySignature(threads: readonly SavedThread[]): string {
|
|
17
|
+
return JSON.stringify(
|
|
18
|
+
threads.map((thread) =>
|
|
19
|
+
LIVE.has(thread.view.state)
|
|
20
|
+
? { ...thread, view: { ...thread.view, status: "", output: undefined } }
|
|
21
|
+
: thread,
|
|
22
|
+
),
|
|
23
|
+
);
|
|
24
|
+
}
|