Compact the supervisor fork before work

Co-Authored-By: PI[gpt-5.6-sol] <288921227+claudypoo@users.noreply.github.com>
This commit is contained in:
wassname
2026-09-05 21:31:46 +08:00
co-authored by PI[gpt-5.6-sol]
parent 3eaaec9f5a
commit 6cfeaf44ee
8 changed files with 117 additions and 27 deletions
+4 -4
View File
@@ -72,10 +72,10 @@ pi -e npm:pi-subagents -e .
1. Plan. The agent explores read-only and drafts the plan.
2. Review. After Pi settles, the full plan is printed in the transcript. Check that User-visible
result names the final artifact or behavior you expect. Ready starts the retained supervisor with
a small fresh context and preserves the main context. Ready (compact) starts that supervisor first,
then requests Pi's normal compaction of the main session only. It never compacts the retained
supervisor or worker. Refine collects short notes. Edit opens the full plan in Pi's editor.
result names the final artifact or behavior you expect. Ready forks the retained supervisor and
preserves the main context. Ready (compact) forks the supervisor, compacts its planning history
before its first turn, then compacts the main session. It never compacts the retained worker.
Refine collects short notes. Edit opens the full plan in Pi's editor.
3. Work. The topology is:
```text
@@ -86,6 +86,6 @@ Usage from the run status files:
| supervisor, including recovery | 42 | 169,288 | 2,670,336 | $2.35 |
| worker | 17 | 67,803 | 812,544 | $0.90 |
The corrective patch starts the supervisor with fresh context, removes global/project/skill inheritance from that supervisor, blocks status polling, removes the unavailable tool, and treats process-terminal as terminal worker state. Unit tests pass; a second model-backed run is still required.
The corrective patch keeps the supervisor fork, compacts its planning history before the first turn when Ready (compact) is selected, removes global/project/skill prompt inheritance, replaces raw status polling with a concise worker-state tool, removes the unavailable tool, and treats process-terminal as terminal worker state. Unit tests pass; a second model-backed run is still required.
-- PI[gpt-5.6-sol]
+5 -5
View File
@@ -187,7 +187,7 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
}
}
async function startOrResumeWorker(ctx: ExtensionContext, task: string, signal?: AbortSignal): Promise<string> {
async function startOrResumeWorker(ctx: ExtensionContext, task: string, compactPlanning: boolean, signal?: AbortSignal): Promise<string> {
if (workerLaunchPending) throw new Error("A goal-worker launch is already in progress.");
if (!workerRegistration) setupWorker(ctx);
if (!workerRegistration) throw new Error(`Goal worker unavailable: ${workerRegistrationError ?? "pi-subagents is not ready"}.`);
@@ -196,7 +196,7 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
try {
const runId = state.workerRunId
? await resumeGoalSupervisor(pi.events, state.workerRunId, task, signal)
: await startGoalSupervisor(pi.events, ctx.cwd, task, signal);
: await startGoalSupervisor(pi.events, ctx.cwd, task, compactPlanning, signal);
rememberWorkerRun(runId);
return runId;
} finally {
@@ -238,7 +238,7 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
return `${instruction}\n\nYou are the retained goal-supervisor. Here is the complete current plan; inspect its exact goal blocks and cited evidence before directing or approving work.\nPlan path: ${planPath(ctx)}\nApproval ID: ${state.approvalId}\nNested worker model: ${state.workerModel ?? "pi-subagents default"}\nPass the exact approval ID to ApproveGoal. Keep checkpoint paths and the approval ID from the nested worker.\nPrivate approval checkpoints, one per current goal:\n${checkpoints || "(no open goals)"}\n\n${plan}`;
}
async function directSupervisor(ctx: ExtensionContext, instruction: string, signal?: AbortSignal): Promise<string> {
async function directSupervisor(ctx: ExtensionContext, instruction: string, signal?: AbortSignal, compactPlanning = false): Promise<string> {
beginReview(ctx);
const task = supervisorTask(ctx, instruction);
if (state.workerPending && state.workerRunId) {
@@ -251,7 +251,7 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
persist();
}
}
return startOrResumeWorker(ctx, task, signal);
return startOrResumeWorker(ctx, task, compactPlanning, signal);
}
function wakeSupervisor(ctx: ExtensionContext, reason: string): void {
@@ -518,7 +518,7 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
persist();
updateWidget(ctx);
try {
await directSupervisor(ctx, "Start by launching or resuming the nested goal-worker. Then supervise the current plan.");
await directSupervisor(ctx, "Start by launching or resuming the nested goal-worker. Then supervise the current plan.", undefined, choice === "Ready (compact)");
scheduleSupervisorCheck(ctx);
return true;
} catch (error) {
+57 -1
View File
@@ -7,6 +7,7 @@ import { isSupervisorReadOnlyCommand } from "./index.js";
import { processWorkState } from "./worker.js";
const NESTED_STATE = "pi-goals-nested-worker";
const COMPACTED_STATE = "pi-goals-supervisor-compacted";
interface NestedState {
runId: string | null;
@@ -22,8 +23,17 @@ function targetRun(input: Record<string, unknown>): string | null {
return typeof value === "string" && value ? value : null;
}
function compactPlanningRequested(): boolean {
const raw = process.env.PI_SUBAGENT_EXTENSION_BINDINGS;
if (!raw) return false;
const bindings = JSON.parse(raw) as { "pi-goals/1"?: { compactPlanning?: unknown } };
return bindings["pi-goals/1"]?.compactPlanning === true;
}
export default function goalSupervisorRuntime(pi: ExtensionAPI): void {
let nested: NestedState = { runId: null, pending: false };
let compacting = false;
let compactionDone = Promise.resolve();
const persist = () => pi.appendEntry<NestedState>(NESTED_STATE, nested);
pi.events.on("subagent:async-started", (raw) => {
@@ -41,11 +51,46 @@ export default function goalSupervisorRuntime(pi: ExtensionAPI): void {
pi.events.on("subagent:async-complete", completeNested);
pi.events.on("subagent:process-terminal", completeNested);
pi.on("session_before_compact", async (event) => {
if (!compacting) return;
const branchEntries = event.branchEntries as Array<{ id?: string; type?: string; message?: { role?: string } }>;
const latestMessage = [...branchEntries].reverse().find((entry) => entry.type === "message" && ["user", "assistant"].includes(entry.message?.role ?? ""));
return {
compaction: {
summary: "Planning is complete. The latest retained goal-supervisor task contains the current plan and approval paths; use it as the source of truth. -- PI[gpt-5.6-sol]",
firstKeptEntryId: latestMessage?.id ?? event.preparation.firstKeptEntryId,
tokensBefore: event.preparation.tokensBefore,
details: { source: "pi-goals-plan-handoff" },
},
};
});
pi.on("session_start", async (_event, ctx) => {
const last = ctx.sessionManager.getEntries()
const entries = ctx.sessionManager.getEntries();
const last = entries
.filter((entry: { type?: string; customType?: string }) => entry.type === "custom" && entry.customType === NESTED_STATE)
.pop() as { data?: NestedState } | undefined;
nested = last?.data ?? nested;
if (!compactPlanningRequested()) return;
if (entries.some((entry: { type?: string; customType?: string }) => entry.type === "custom" && entry.customType === COMPACTED_STATE)) return;
compacting = true;
compactionDone = new Promise<void>((resolvePromise, reject) => {
ctx.compact({
onComplete: () => {
compacting = false;
pi.appendEntry(COMPACTED_STATE, { version: 1 });
resolvePromise();
},
onError: (error) => {
compacting = false;
reject(error);
},
});
});
});
pi.on("before_agent_start", async () => {
await compactionDone;
});
pi.on("tool_call", async (event) => {
@@ -68,6 +113,17 @@ export default function goalSupervisorRuntime(pi: ExtensionAPI): void {
return { block: true, reason: "The supervisor may inspect or control only its retained goal-worker." };
});
pi.registerTool({
name: "CheckWorkerState",
label: "Check retained worker",
description: "Return concise retained-worker state after a needs-attention notice or scheduled review. This does not return transcript text.",
parameters: Type.Object({}),
async execute() {
const state = nested.runId ? (nested.pending ? "active" : "terminal") : "not-started";
return result(`retained-worker=${state}${nested.runId ? `; run=${nested.runId}` : ""}`);
},
});
pi.registerTool({
name: "ApproveGoal",
label: "Approve goal",
+8 -6
View File
@@ -39,8 +39,9 @@ interface AsyncSnapshot {
export type WorkState = "active" | "idle" | "unknown";
export const supervisorSystemPrompt = `You are the retained goal supervisor. The main Pi session only coordinates with the human.
Launch one goal-worker, then rely on native progress and completion updates. Do not poll status, wait, or repeatedly
steer an active worker. Read the current plan, repository, cited evidence, and saved verification output yourself after
Your forked planning history is compacted before your first turn. Launch one goal-worker, then rely on native progress
and completion updates. Do not poll status, wait, or repeatedly steer an active worker. Use CheckWorkerState once only
after a needs-attention notice or a scheduled review. Read the current plan, repository, cited evidence, and saved verification output yourself after
the worker finishes. Do not edit project files. Use read/search and standard verification commands only. The worker must
commit its changes before approval. When no nested work is active, HEAD is committed, the worktree is clean, and the
evidence proves the discriminator, call ApproveGoal with the current approval ID. Otherwise give the retained worker
@@ -54,7 +55,7 @@ export function registerGoalSupervisor(events: EventBus, model: string | null):
definition: {
description: "Read-only supervisor that owns a nested retained implementation worker.",
systemPrompt: supervisorSystemPrompt,
tools: ["read", "grep", "find", "ls", "bash", "subagent", "ApproveGoal"],
tools: ["read", "grep", "find", "ls", "bash", "subagent", "CheckWorkerState", "ApproveGoal"],
allowNestedSubagents: true,
subagentOnlyExtensions: [supervisorRuntime],
...(model ? { model } : {}),
@@ -63,7 +64,7 @@ export function registerGoalSupervisor(events: EventBus, model: string | null):
inheritProjectContext: false,
inheritGlobalContext: false,
inheritSkills: false,
defaultContext: "fresh",
defaultContext: "fork",
defaultAsync: true,
defaultProgress: true,
},
@@ -112,14 +113,15 @@ function asyncRunId(data: RpcData): string {
return runId;
}
export async function startGoalSupervisor(events: EventBus, cwd: string, task: string, signal?: AbortSignal): Promise<string> {
export async function startGoalSupervisor(events: EventBus, cwd: string, task: string, compactPlanning: boolean, signal?: AbortSignal): Promise<string> {
const data = await rpc(events, "spawn", {
agent: SUPERVISOR_AGENT,
task,
cwd,
context: "fresh",
context: "fork",
async: true,
mission: false,
extensionBindings: { "pi-goals/1": { compactPlanning } },
}, signal);
return asyncRunId(data);
}
+4 -2
View File
@@ -255,7 +255,7 @@ describe("/goals draft flow", () => {
await flow.hooks.get("agent_settled")({}, flow.ctx);
expect(flow.events).toEqual(["display", "select"]);
expect(flow.rpcRequests[0]).toMatchObject({ method: "spawn", params: { agent: "goal-supervisor", context: "fresh" } });
expect(flow.rpcRequests[0]).toMatchObject({ method: "spawn", params: { agent: "goal-supervisor", context: "fork" } });
expect(flow.messages.filter((message) => !message.display)).toHaveLength(1);
const supervisor = await flow.hooks.get("before_agent_start")({}, flow.ctx);
expect(supervisor.systemPrompt).toContain("thin human-facing coordinator");
@@ -339,8 +339,10 @@ describe("/goals draft flow", () => {
expect(ready.compactCalls).toHaveLength(0);
expect(ready.rpcRequests).toHaveLength(1);
expect(ready.rpcRequests[0]).toMatchObject({ params: { extensionBindings: { "pi-goals/1": { compactPlanning: false } } } });
expect(compacted.compactCalls).toHaveLength(1);
expect(compacted.rpcRequests).toHaveLength(1);
expect(compacted.rpcRequests[0]).toMatchObject({ params: { extensionBindings: { "pi-goals/1": { compactPlanning: true } } } });
const resync = await compacted.hooks.get("context")({ messages: [] }, compacted.ctx);
expect(resync.messages.at(-1).content[0].text).toContain("The main coordinator was compacted after the retained supervisor started.");
} finally {
@@ -430,7 +432,7 @@ describe("/goals draft flow", () => {
const planPath = join(flow.cwd, ".pi/plan/session-a-v1.md");
writeFileSync(planPath, "# Plan\n\n## Goals\n\n1. [/] goal: produce report\n - discriminator: report.txt contains PASS\n - evidence:\n - report.txt: `PASS`\n\n## Log\n");
await flow.hooks.get("agent_settled")({}, flow.ctx);
expect(flow.rpcRequests[0]).toMatchObject({ method: "spawn", params: { agent: "goal-supervisor", context: "fresh" } });
expect(flow.rpcRequests[0]).toMatchObject({ method: "spawn", params: { agent: "goal-supervisor", context: "fork" } });
flow.eventBus.emit("subagent:async-complete", { runId: "worker-1", results: [{ success: true }] });
const resumed = await flow.tools.get("GuideGoalWorker").execute("", { instruction: "Verify report.txt." }, undefined, undefined, flow.ctx);
+32 -3
View File
@@ -29,6 +29,8 @@ function setup() {
execFileSync("git", ["-c", "user.name=test", "-c", "user.email=test@example.com", "commit", "-qm", "initial"], { cwd });
const hooks = new Map<string, any>();
const tools = new Map<string, any>();
const entries: any[] = [];
const compactCalls: any[] = [];
const events = new Events();
events.on("subagents:rpc:v1:request", (raw) => {
const request = raw as any;
@@ -42,17 +44,18 @@ function setup() {
});
const ctx = {
cwd,
sessionManager: { getSessionId: () => "supervisor-session" },
sessionManager: { getSessionId: () => "supervisor-session", getEntries: () => entries },
compact: (options: any) => compactCalls.push(options),
ui: { notify() {} },
};
const pi = {
events,
on: (name: string, handler: any) => hooks.set(name, handler),
appendEntry() {},
appendEntry: (customType: string, data: unknown) => entries.push({ type: "custom", customType, data }),
registerTool: (tool: any) => tools.set(tool.name, tool),
};
supervisorRuntime(pi as any);
return { cwd, ctx, events, hooks, tools };
return { cwd, ctx, events, hooks, tools, entries, compactCalls };
}
describe("supervisor-only runtime", () => {
@@ -70,10 +73,34 @@ describe("supervisor-only runtime", () => {
}
});
it("compacts a requested fork before the first supervisor turn", async () => {
const previous = process.env.PI_SUBAGENT_EXTENSION_BINDINGS;
process.env.PI_SUBAGENT_EXTENSION_BINDINGS = JSON.stringify({ "pi-goals/1": { compactPlanning: true } });
const runtime = setup();
try {
await runtime.hooks.get("session_start")({}, runtime.ctx);
expect(runtime.compactCalls).toHaveLength(1);
const replacement = await runtime.hooks.get("session_before_compact")({
preparation: { firstKeptEntryId: "old", tokensBefore: 70_000 },
branchEntries: [{ id: "recent", type: "message", message: { role: "assistant" } }],
}, runtime.ctx);
expect(replacement.compaction).toMatchObject({ firstKeptEntryId: "recent", tokensBefore: 70_000 });
runtime.compactCalls[0].onComplete({});
await runtime.hooks.get("before_agent_start")({}, runtime.ctx);
expect(runtime.entries).toContainEqual({ type: "custom", customType: "pi-goals-supervisor-compacted", data: { version: 1 } });
} finally {
if (previous === undefined) delete process.env.PI_SUBAGENT_EXTENSION_BINDINGS;
else process.env.PI_SUBAGENT_EXTENSION_BINDINGS = previous;
rmSync(runtime.cwd, { recursive: true, force: true });
}
});
it("blocks approval while its retained worker is pending", async () => {
const runtime = setup();
try {
runtime.events.emit("subagent:async-started", { id: "nested-1", agent: "goal-worker" });
const state = await runtime.tools.get("CheckWorkerState").execute("", {}, undefined, undefined, runtime.ctx);
expect(state.content[0].text).toBe("retained-worker=active; run=nested-1");
const blocked = await runtime.tools.get("ApproveGoal").execute("", {}, undefined, undefined, runtime.ctx);
expect(blocked.isError).toBe(true);
expect(blocked.content[0].text).toContain("retained worker is pending");
@@ -93,6 +120,8 @@ describe("supervisor-only runtime", () => {
const checkpoint = approvalPath(runtime.cwd, "main-session", "ship it");
runtime.events.emit("subagent:async-started", { id: "nested-1", agent: "goal-worker" });
runtime.events.emit("subagent:process-terminal", { runId: "nested-1", state: "observed" });
const state = await runtime.tools.get("CheckWorkerState").execute("", {}, undefined, undefined, runtime.ctx);
expect(state.content[0].text).toBe("retained-worker=terminal; run=nested-1");
const accepted = await runtime.tools.get("ApproveGoal").execute("", {
approvalId: "review-1",
goal: "ship it",
+6 -5
View File
@@ -46,23 +46,24 @@ describe("goal hierarchy registration", () => {
registerGoalSupervisor(events, "provider/cheap-model");
expect(definition?.model).toBe("provider/cheap-model");
expect(definition?.defaultContext).toBe("fresh");
expect(definition?.defaultContext).toBe("fork");
expect(definition?.thinking).toBe("low");
expect(definition?.inheritProjectContext).toBe(false);
expect(definition?.inheritGlobalContext).toBe(false);
expect(definition?.inheritSkills).toBe(false);
expect(definition?.defaultProgress).toBe(true);
expect(definition?.allowNestedSubagents).toBe(true);
expect(definition?.tools).toEqual(["read", "grep", "find", "ls", "bash", "subagent", "ApproveGoal"]);
expect(definition?.tools).toEqual(["read", "grep", "find", "ls", "bash", "subagent", "CheckWorkerState", "ApproveGoal"]);
expect(definition?.subagentOnlyExtensions).toEqual([expect.stringContaining("supervisor-runtime.ts")]);
expect(supervisorSystemPrompt).toContain("Launch one goal-worker");
expect(supervisorSystemPrompt).toContain("Do not poll status");
expect(supervisorSystemPrompt).toContain("forked planning history is compacted");
expect(supervisorSystemPrompt).toContain("ApproveGoal");
});
});
describe("goal worker RPC", () => {
it("starts fresh, resumes retained context, and steers a live run", async () => {
it("starts from a fork, resumes retained context, and steers a live run", async () => {
const events = new Events();
const requests: any[] = [];
replyToRpc(events, (request) => {
@@ -70,12 +71,12 @@ describe("goal worker RPC", () => {
return { text: "ok", details: { asyncId: `run-${requests.length}` } };
});
await expect(startGoalSupervisor(events, "/repo", "start")).resolves.toBe("run-1");
await expect(startGoalSupervisor(events, "/repo", "start", true)).resolves.toBe("run-1");
await expect(resumeGoalSupervisor(events, "run-1", "continue")).resolves.toBe("run-2");
await steerGoalSupervisor(events, "run-2", "report");
await stopGoalSupervisor(events, "run-2");
expect(requests[0]).toMatchObject({ method: "spawn", params: { agent: "goal-supervisor", cwd: "/repo", context: "fresh", async: true } });
expect(requests[0]).toMatchObject({ method: "spawn", params: { agent: "goal-supervisor", cwd: "/repo", context: "fork", async: true, extensionBindings: { "pi-goals/1": { compactPlanning: true } } } });
expect(requests[1]).toMatchObject({ method: "resume", params: { id: "run-1", message: "continue" } });
expect(requests[2]).toMatchObject({ method: "steer", params: { id: "run-2", message: "report", mode: "steer" } });
expect(requests[3]).toMatchObject({ method: "stop", params: { id: "run-2" } });