@ferris1225/pi-subagents 4.1.13 → 4.1.16
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 +333 -291
- package/agents/cleaner.md +24 -30
- package/agents/documenter.md +23 -20
- package/agents/explorer.md +7 -2
- package/agents/reviewer.md +82 -77
- package/agents/worker.md +45 -37
- package/package.json +1 -1
- package/src/announcements.ts +59 -54
- package/src/background.ts +26 -10
- package/src/completion.ts +7 -1
- package/src/config.ts +1 -1
- package/src/dispatch.ts +133 -133
- package/src/durable.ts +85 -19
- package/src/format.ts +179 -167
- package/src/monitor.ts +4 -2
- package/src/prompt.ts +14 -27
- package/src/runtime.ts +18 -14
- package/src/setup.ts +3 -3
- package/src/spawn.ts +650 -642
- package/src/temp-hygiene.ts +0 -28
- package/src/thread-lifecycle.ts +85 -99
- package/src/tools.ts +712 -708
- package/src/widget.ts +9 -0
- package/src/workflow.ts +199 -248
- package/src/worktree.ts +64 -37
package/src/announcements.ts
CHANGED
|
@@ -1,54 +1,59 @@
|
|
|
1
|
-
/** Session-start recovery, stale-config migration, and widget installation. */
|
|
2
|
-
|
|
3
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
9
|
-
import { installActiveRunsWidget } from "./widget.ts";
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* One-time-per-stale-override migration: keep agent model selections Pi still
|
|
13
|
-
* reports as available, drop the rest back to dynamic main-model routing, and
|
|
14
|
-
* tell the user what was removed. Saving the cleaned config is what makes it
|
|
15
|
-
* one-time — the dropped refs no longer exist to re-trigger the notice.
|
|
16
|
-
*/
|
|
17
|
-
async function migrateUnavailableAgentModels(
|
|
18
|
-
ctx: { ui: { notify: (message: string, kind: "info" | "warning" | "error") => void } } & Parameters<typeof availableModelsInScope>[0],
|
|
19
|
-
runtime: SubagentRuntime,
|
|
20
|
-
): Promise<void> {
|
|
21
|
-
try {
|
|
22
|
-
const config = await loadConfig(runtime.configPath);
|
|
23
|
-
const overrides = Object.entries(config.agentModels);
|
|
24
|
-
if (overrides.length === 0) return;
|
|
25
|
-
const { kept, dropped } = filterUnavailableModelOverrides(config.agentModels, availableModelsInScope(ctx));
|
|
26
|
-
if (dropped.length === 0) return;
|
|
27
|
-
await saveConfig({ ...config, agentModels: kept }, runtime.configPath);
|
|
28
|
-
const list = dropped.map(({ agent, ref }) => `${agent}: ${ref}`).join(", ");
|
|
29
|
-
ctx.ui.notify(
|
|
30
|
-
`pi-subagents: removed stale agent model overrides that are no longer available (${list}). Those agents now follow the current main model; run /subagents-setup to re-pick.`,
|
|
31
|
-
"warning",
|
|
32
|
-
);
|
|
33
|
-
} catch {
|
|
34
|
-
/* migration failures are non-fatal */
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function registerAnnouncements(pi: ExtensionAPI, runtime: SubagentRuntime): void {
|
|
39
|
-
pi.on("session_start", async (_event, ctx) => {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
1
|
+
/** Session-start recovery, stale-config migration, and widget installation. */
|
|
2
|
+
|
|
3
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { loadConfig, saveConfig } from "./config.ts";
|
|
6
|
+
import { availableModelsInScope, filterUnavailableModelOverrides } from "./models.ts";
|
|
7
|
+
import { announceRecoveryRecords } from "./recovery.ts";
|
|
8
|
+
import type { SubagentRuntime } from "./runtime.ts";
|
|
9
|
+
import { installActiveRunsWidget } from "./widget.ts";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* One-time-per-stale-override migration: keep agent model selections Pi still
|
|
13
|
+
* reports as available, drop the rest back to dynamic main-model routing, and
|
|
14
|
+
* tell the user what was removed. Saving the cleaned config is what makes it
|
|
15
|
+
* one-time — the dropped refs no longer exist to re-trigger the notice.
|
|
16
|
+
*/
|
|
17
|
+
async function migrateUnavailableAgentModels(
|
|
18
|
+
ctx: { ui: { notify: (message: string, kind: "info" | "warning" | "error") => void } } & Parameters<typeof availableModelsInScope>[0],
|
|
19
|
+
runtime: SubagentRuntime,
|
|
20
|
+
): Promise<void> {
|
|
21
|
+
try {
|
|
22
|
+
const config = await loadConfig(runtime.configPath);
|
|
23
|
+
const overrides = Object.entries(config.agentModels);
|
|
24
|
+
if (overrides.length === 0) return;
|
|
25
|
+
const { kept, dropped } = filterUnavailableModelOverrides(config.agentModels, availableModelsInScope(ctx));
|
|
26
|
+
if (dropped.length === 0) return;
|
|
27
|
+
await saveConfig({ ...config, agentModels: kept }, runtime.configPath);
|
|
28
|
+
const list = dropped.map(({ agent, ref }) => `${agent}: ${ref}`).join(", ");
|
|
29
|
+
ctx.ui.notify(
|
|
30
|
+
`pi-subagents: removed stale agent model overrides that are no longer available (${list}). Those agents now follow the current main model; run /subagents-setup to re-pick.`,
|
|
31
|
+
"warning",
|
|
32
|
+
);
|
|
33
|
+
} catch {
|
|
34
|
+
/* migration failures are non-fatal */
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function registerAnnouncements(pi: ExtensionAPI, runtime: SubagentRuntime): void {
|
|
39
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
40
|
+
if (!existsSync(runtime.configPath)) {
|
|
41
|
+
ctx.ui.notify(
|
|
42
|
+
"pi-subagents: no configuration yet — run /subagents-setup to pick agents, models, and thinking strengths. Defaults (all five agents on the main model) apply until then.",
|
|
43
|
+
"info",
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
await announceRecoveryRecords(runtime.configPath, ctx);
|
|
47
|
+
await migrateUnavailableAgentModels(ctx, runtime);
|
|
48
|
+
if (!runtime.restoredNotified && runtime.restoredRunIds.length > 0) {
|
|
49
|
+
runtime.restoredNotified = true;
|
|
50
|
+
const ids = runtime.restoredRunIds.map((id) => `#${id}`).join(", ");
|
|
51
|
+
ctx.ui.notify(
|
|
52
|
+
`pi-subagents: restored ${runtime.restoredRunIds.length} interrupted thread${runtime.restoredRunIds.length === 1 ? "" : "s"} from the previous session (${ids}). subagent_status lists them; subagent_control resume continues one.`,
|
|
53
|
+
"info",
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
if (ctx.mode !== "tui") return;
|
|
57
|
+
installActiveRunsWidget(ctx);
|
|
58
|
+
});
|
|
59
|
+
}
|
package/src/background.ts
CHANGED
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
* the user and the main agent instead of it vanishing into the queue.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
import { cpus } from "node:os";
|
|
13
|
+
|
|
14
|
+
export type BackgroundTask = (signal: AbortSignal, controller: AbortController) => Promise<void>;
|
|
13
15
|
|
|
14
16
|
interface PendingTask {
|
|
15
17
|
task: BackgroundTask;
|
|
@@ -27,13 +29,14 @@ interface PendingTask {
|
|
|
27
29
|
onError?: (error: unknown) => void | Promise<void>;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
|
-
/** How many sub-agent processes may run at once
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
|
|
36
|
-
|
|
32
|
+
/** How many sub-agent processes may run at once, derived from the host instead
|
|
33
|
+
* of being fixed: children wait on model I/O far more than on CPU, so the pool
|
|
34
|
+
* scales with cores while the bounds keep tiny machines usable and huge ones
|
|
35
|
+
* from fanning out into an API-rate-limit wall. Pacing only — the queue never
|
|
36
|
+
* rejects work; a wider parallel `subagent` call simply waits for a slot. */
|
|
37
|
+
export function resolveSubagentConcurrency(cpuCount: number = cpus().length): number {
|
|
38
|
+
return Math.min(16, Math.max(4, Math.floor(cpuCount / 2)));
|
|
39
|
+
}
|
|
37
40
|
|
|
38
41
|
export class BackgroundTaskQueue {
|
|
39
42
|
private concurrency: number;
|
|
@@ -77,9 +80,22 @@ export class BackgroundTaskQueue {
|
|
|
77
80
|
return this.completions.get(controller) ?? Promise.resolve();
|
|
78
81
|
}
|
|
79
82
|
|
|
83
|
+
/** Slot count, exposed so dispatch/status output can state the real pacing
|
|
84
|
+
* limit instead of leaving queued work looking like an unexplained cap. */
|
|
85
|
+
get capacity(): number {
|
|
86
|
+
return this.concurrency;
|
|
87
|
+
}
|
|
88
|
+
|
|
80
89
|
/** Stop counting a running task toward the concurrency limit. Its body keeps
|
|
81
90
|
* running under the same abort signal; completion still releases everything
|
|
82
|
-
* waitForTask/waitForIdle promise. Frees a slot for queued work immediately.
|
|
91
|
+
* waitForTask/waitForIdle promise. Frees a slot for queued work immediately.
|
|
92
|
+
*
|
|
93
|
+
* Used by tasks whose execution is serialized elsewhere anyway (managed
|
|
94
|
+
* workflow continuations, shared-checkout writers waiting on the repository
|
|
95
|
+
* lane): letting such a task also hold a global slot would let waiters
|
|
96
|
+
* starve independent work that could start right away. The controller is
|
|
97
|
+
* handed to the task body directly, so a task can always suspend itself
|
|
98
|
+
* without racing the enqueue() caller's assignment. */
|
|
83
99
|
suspend(controller: AbortController | undefined): void {
|
|
84
100
|
if (!controller || this.stopped) return;
|
|
85
101
|
if (!this.active.delete(controller)) return;
|
|
@@ -149,7 +165,7 @@ export class BackgroundTaskQueue {
|
|
|
149
165
|
}
|
|
150
166
|
|
|
151
167
|
this.active.add(entry.controller);
|
|
152
|
-
void entry.task(entry.controller.signal)
|
|
168
|
+
void entry.task(entry.controller.signal, entry.controller)
|
|
153
169
|
.catch(async (error: unknown) => {
|
|
154
170
|
// Cancellation is not a failure: aborted work (e.g. session
|
|
155
171
|
// shutdown) must never be reported as an exception.
|
package/src/completion.ts
CHANGED
|
@@ -139,6 +139,9 @@ export interface ActiveRunFoot {
|
|
|
139
139
|
agent: string;
|
|
140
140
|
/** Optional content label (task-derived) shown next to the agent name. */
|
|
141
141
|
label?: string;
|
|
142
|
+
/** True when the run is waiting for a free process slot rather than
|
|
143
|
+
* executing; stated so pacing is never mistaken for a stall. */
|
|
144
|
+
queued?: boolean;
|
|
142
145
|
}
|
|
143
146
|
|
|
144
147
|
/**
|
|
@@ -153,7 +156,10 @@ export function formatActiveRunsFooter(runs: readonly ActiveRunFoot[], maxListed
|
|
|
153
156
|
if (runs.length === 0) return "";
|
|
154
157
|
const listed = runs.slice(0, maxListed);
|
|
155
158
|
const items = listed
|
|
156
|
-
.map((run) =>
|
|
159
|
+
.map((run) => {
|
|
160
|
+
const tagged = run.queued ? " (queued, starts when a slot frees)" : "";
|
|
161
|
+
return `#${run.id} ${run.agent}${run.label ? `·${run.label}` : ""}${tagged}`;
|
|
162
|
+
})
|
|
157
163
|
.join(", ");
|
|
158
164
|
const more = runs.length > listed.length ? `, +${runs.length - listed.length} more` : "";
|
|
159
165
|
return `\n\n⚠ ${runs.length} other run${runs.length === 1 ? "" : "s"} still active: ${items}${more}. Do not conclude the overall task yet — wait for their results (they wake you automatically) or check subagent_status.`;
|
package/src/config.ts
CHANGED
|
@@ -16,7 +16,7 @@ import { getAgentDir, withFileMutationQueue } from "@earendil-works/pi-coding-ag
|
|
|
16
16
|
export const BUILTIN_AGENT_NAMES = ["explorer", "worker", "cleaner", "documenter", "reviewer"] as const;
|
|
17
17
|
|
|
18
18
|
/** Agents enabled out of the box on a fresh install. */
|
|
19
|
-
export const DEFAULT_ENABLED_AGENTS: readonly string[] = ["explorer", "worker", "cleaner", "reviewer"];
|
|
19
|
+
export const DEFAULT_ENABLED_AGENTS: readonly string[] = ["explorer", "worker", "cleaner", "documenter", "reviewer"];
|
|
20
20
|
|
|
21
21
|
export const AGENT_SCOPE_VALUES = ["user", "project", "both"] as const;
|
|
22
22
|
export type AgentScope = (typeof AGENT_SCOPE_VALUES)[number];
|