mirror of
https://github.com/wassname/pi-plan.git
synced 2026-09-26 14:10:23 +08:00
Reduce supervisor context and recover terminal workers
Co-Authored-By: PI[gpt-5.6-sol] <288921227+claudypoo@users.noreply.github.com>
This commit is contained in:
co-authored by
PI[gpt-5.6-sol]
parent
0a33ff2852
commit
3eaaec9f5a
@@ -72,8 +72,8 @@ 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 forks the retained supervisor and
|
||||
preserves the main context. Ready (compact) first forks that supervisor from the full main context,
|
||||
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.
|
||||
3. Work. The topology is:
|
||||
|
||||
@@ -71,8 +71,21 @@ $ git diff --stat 2852432
|
||||
test/worker.test.ts | 18 ++-
|
||||
12 files changed, 354 insertions(+), 143 deletions(-)
|
||||
|
||||
## Runtime boundary
|
||||
## Dogfood run
|
||||
|
||||
No model-backed Ready -> goal-supervisor -> goal-worker run was executed here. Unit tests cover the package-discovery contract, nested tool restrictions, review freshness, clean-state filtering, stop dispatch, stale-run recovery, and mechanical sign-off. The README manual check is the user acceptance test.
|
||||
The model-backed run produced commit `0a33ff2` and independently verified 47 text-file word counts with zero set, count, or order mismatches. Approval then deadlocked:
|
||||
|
||||
> Cannot approve while the retained worker is pending.
|
||||
|
||||
The worker process was terminal, but its model result was `Request was aborted`; the completion event did not clear retained state. A supervisor resume also failed because `subagent_supervisor` was unavailable in its strict tool list.
|
||||
|
||||
Usage from the run status files:
|
||||
|
||||
| agent | turns | new tokens | cached reads | reported cost |
|
||||
| --- | ---: | ---: | ---: | ---: |
|
||||
| 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.
|
||||
|
||||
-- PI[gpt-5.6-sol]
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
import { Type } from "typebox";
|
||||
import { goalBlock, hashGoalBlock, repositoryState, writeApproval } from "./approval.js";
|
||||
import { isSupervisorReadOnlyCommand } from "./index.js";
|
||||
import { processWorkState, subagentWorkState } from "./worker.js";
|
||||
import { processWorkState } from "./worker.js";
|
||||
|
||||
const NESTED_STATE = "pi-goals-nested-worker";
|
||||
|
||||
@@ -32,11 +32,14 @@ export default function goalSupervisorRuntime(pi: ExtensionAPI): void {
|
||||
nested = { runId: event.id, pending: true };
|
||||
persist();
|
||||
});
|
||||
pi.events.on("subagent:async-complete", (raw) => {
|
||||
if ((raw as { runId?: unknown }).runId !== nested.runId) return;
|
||||
const completeNested = (raw: unknown) => {
|
||||
const event = raw as { id?: unknown; runId?: unknown };
|
||||
if ((event.runId ?? event.id) !== nested.runId) return;
|
||||
nested = { ...nested, pending: false };
|
||||
persist();
|
||||
});
|
||||
};
|
||||
pi.events.on("subagent:async-complete", completeNested);
|
||||
pi.events.on("subagent:process-terminal", completeNested);
|
||||
|
||||
pi.on("session_start", async (_event, ctx) => {
|
||||
const last = ctx.sessionManager.getEntries()
|
||||
@@ -60,7 +63,7 @@ export default function goalSupervisorRuntime(pi: ExtensionAPI): void {
|
||||
return { block: true, reason: nested.runId ? "Resume the retained goal-worker instead of starting another worker." : "The supervisor may start only goal-worker." };
|
||||
}
|
||||
if (action === "list") return;
|
||||
if (action === "status" && (!targetRun(input) || targetRun(input) === nested.runId)) return;
|
||||
if (action === "status") return { block: true, reason: "Do not poll the retained worker. Use its native progress and completion updates." };
|
||||
if (["resume", "steer", "interrupt", "stop"].includes(action) && targetRun(input) === nested.runId) return;
|
||||
return { block: true, reason: "The supervisor may inspect or control only its retained goal-worker." };
|
||||
});
|
||||
@@ -81,8 +84,8 @@ export default function goalSupervisorRuntime(pi: ExtensionAPI): void {
|
||||
}),
|
||||
async execute(_id, params, _signal, _onUpdate, ctx) {
|
||||
if (nested.pending) return result("Cannot approve while the retained worker is pending.", true);
|
||||
const [subagents, processes] = await Promise.all([subagentWorkState(pi.events), Promise.resolve(processWorkState(pi.events))]);
|
||||
if (subagents !== "idle" || processes !== "idle") return result(`Cannot approve: subagents=${subagents}; processes=${processes}.`, true);
|
||||
const processes = processWorkState(pi.events);
|
||||
if (processes !== "idle") return result(`Cannot approve: processes=${processes}.`, true);
|
||||
const planPath = resolve(params.planPath);
|
||||
let plan: string;
|
||||
let repository: ReturnType<typeof repositoryState>;
|
||||
|
||||
+14
-15
@@ -38,15 +38,13 @@ interface AsyncSnapshot {
|
||||
|
||||
export type WorkState = "active" | "idle" | "unknown";
|
||||
|
||||
export const supervisorSystemPrompt = `You are the retained goal supervisor. The main Pi session is a thin human-facing coordinator.
|
||||
You own the current plan review and the retained implementation worker. At every review, reread the full
|
||||
current plan named in your task, identify the exact goal block, inspect the repository, cited artifacts, and
|
||||
saved verification output, then launch, resume, or steer the one nested goal-worker as needed. Use the worker model
|
||||
named in each direction when spawning it; resume the retained worker after that. Do not edit project files. Use
|
||||
read/search and standard verification commands only. Keep approval IDs and checkpoint paths from the worker. The worker
|
||||
must commit its changes before approval. When no nested work is active, HEAD is committed, the worktree is clean, and
|
||||
you inspected the plan, repository, evidence, and saved verification output, call ApproveGoal with the current approval
|
||||
ID. Otherwise continue or redirect the worker. Only ApproveGoal creates acceptance. -- Pi/Codex`;
|
||||
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
|
||||
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
|
||||
one concrete correction. Only ApproveGoal creates acceptance. -- Pi/Codex`;
|
||||
|
||||
export function registerGoalSupervisor(events: EventBus, model: string | null): Registration {
|
||||
const supervisorRuntime = fileURLToPath(new URL("./supervisor-runtime.ts", import.meta.url));
|
||||
@@ -56,15 +54,16 @@ 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", "subagent_supervisor", "ApproveGoal"],
|
||||
tools: ["read", "grep", "find", "ls", "bash", "subagent", "ApproveGoal"],
|
||||
allowNestedSubagents: true,
|
||||
subagentOnlyExtensions: [supervisorRuntime],
|
||||
...(model ? { model } : {}),
|
||||
systemPromptMode: "replace",
|
||||
inheritProjectContext: true,
|
||||
inheritGlobalContext: true,
|
||||
inheritSkills: true,
|
||||
defaultContext: "fork",
|
||||
thinking: "low",
|
||||
inheritProjectContext: false,
|
||||
inheritGlobalContext: false,
|
||||
inheritSkills: false,
|
||||
defaultContext: "fresh",
|
||||
defaultAsync: true,
|
||||
defaultProgress: true,
|
||||
},
|
||||
@@ -118,7 +117,7 @@ export async function startGoalSupervisor(events: EventBus, cwd: string, task: s
|
||||
agent: SUPERVISOR_AGENT,
|
||||
task,
|
||||
cwd,
|
||||
context: "fork",
|
||||
context: "fresh",
|
||||
async: true,
|
||||
mission: false,
|
||||
}, signal);
|
||||
|
||||
@@ -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: "fork" } });
|
||||
expect(flow.rpcRequests[0]).toMatchObject({ method: "spawn", params: { agent: "goal-supervisor", context: "fresh" } });
|
||||
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");
|
||||
@@ -430,7 +430,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: "fork" } });
|
||||
expect(flow.rpcRequests[0]).toMatchObject({ method: "spawn", params: { agent: "goal-supervisor", context: "fresh" } });
|
||||
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);
|
||||
|
||||
@@ -63,6 +63,7 @@ describe("supervisor-only runtime", () => {
|
||||
expect((await runtime.hooks.get("tool_call")({ toolName: "bash", input: { command: "git branch new-name" } }, runtime.ctx))?.block).toBe(true);
|
||||
expect((await runtime.hooks.get("tool_call")({ toolName: "subagent", input: { agent: "worker" } }, runtime.ctx))?.block).toBe(true);
|
||||
expect((await runtime.hooks.get("tool_call")({ toolName: "subagent", input: { agent: "goal-worker" } }, runtime.ctx))).toBeUndefined();
|
||||
expect((await runtime.hooks.get("tool_call")({ toolName: "subagent", input: { action: "status", id: "nested-1" } }, runtime.ctx))?.block).toBe(true);
|
||||
expect((await runtime.hooks.get("tool_call")({ toolName: "bash", input: { command: "git status && npm test" } }, runtime.ctx))).toBeUndefined();
|
||||
} finally {
|
||||
rmSync(runtime.cwd, { recursive: true, force: true });
|
||||
@@ -90,6 +91,8 @@ describe("supervisor-only runtime", () => {
|
||||
mkdirSync(join(runtime.cwd, ".pi/plan"), { recursive: true });
|
||||
writeFileSync(planPath, "# Plan\n\n## Goals\n\n1. [/] goal: ship it\n - evidence: verify.log: PASS\n");
|
||||
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 accepted = await runtime.tools.get("ApproveGoal").execute("", {
|
||||
approvalId: "review-1",
|
||||
goal: "ship it",
|
||||
|
||||
+10
-5
@@ -46,18 +46,23 @@ describe("goal hierarchy registration", () => {
|
||||
registerGoalSupervisor(events, "provider/cheap-model");
|
||||
|
||||
expect(definition?.model).toBe("provider/cheap-model");
|
||||
expect(definition?.defaultContext).toBe("fork");
|
||||
expect(definition?.defaultContext).toBe("fresh");
|
||||
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", "subagent_supervisor", "ApproveGoal"]);
|
||||
expect(definition?.tools).toEqual(["read", "grep", "find", "ls", "bash", "subagent", "ApproveGoal"]);
|
||||
expect(definition?.subagentOnlyExtensions).toEqual([expect.stringContaining("supervisor-runtime.ts")]);
|
||||
expect(supervisorSystemPrompt).toContain("nested goal-worker");
|
||||
expect(supervisorSystemPrompt).toContain("Launch one goal-worker");
|
||||
expect(supervisorSystemPrompt).toContain("Do not poll status");
|
||||
expect(supervisorSystemPrompt).toContain("ApproveGoal");
|
||||
});
|
||||
});
|
||||
|
||||
describe("goal worker RPC", () => {
|
||||
it("starts from a fork, resumes retained context, and steers a live run", async () => {
|
||||
it("starts fresh, resumes retained context, and steers a live run", async () => {
|
||||
const events = new Events();
|
||||
const requests: any[] = [];
|
||||
replyToRpc(events, (request) => {
|
||||
@@ -70,7 +75,7 @@ describe("goal worker RPC", () => {
|
||||
await steerGoalSupervisor(events, "run-2", "report");
|
||||
await stopGoalSupervisor(events, "run-2");
|
||||
|
||||
expect(requests[0]).toMatchObject({ method: "spawn", params: { agent: "goal-supervisor", cwd: "/repo", context: "fork", async: true } });
|
||||
expect(requests[0]).toMatchObject({ method: "spawn", params: { agent: "goal-supervisor", cwd: "/repo", context: "fresh", async: 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" } });
|
||||
|
||||
Reference in New Issue
Block a user