Simplify nested goal supervision

Co-Authored-By: PI[gpt-5.6-sol] <288921227+claudypoo@users.noreply.github.com>
This commit is contained in:
wassname
2026-09-06 13:36:12 +08:00
co-authored by PI[gpt-5.6-sol]
parent 844099bdf0
commit 48e2247c00
19 changed files with 384 additions and 474 deletions
+3 -3
View File
@@ -1,6 +1,6 @@
/**
* PI: pi-goals owns one versioned plan per session. The main agent is a thin coordinator for a
* retained pi-subagents supervisor, which owns a nested retained implementation worker and approval.
* retained pi-subagents supervisor, which owns one foreground implementation worker at a time and approval.
*
* Each /goals call makes `.pi/plan/<session_id>-vN.md`. The selected version survives resume and
* compaction. Old plans stay on disk but inactive. A session with no selected plan has no widget,
@@ -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, compactPlanning, signal);
: await startGoalSupervisor(pi.events, ctx.cwd, task, compactPlanning, state.workerModel, signal);
rememberWorkerRun(runId);
return runId;
} finally {
@@ -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.", undefined, choice === "Ready (compact)");
await directSupervisor(ctx, "Start by launching the foreground implementation worker. Then supervise the current plan.", undefined, choice === "Ready (compact)");
scheduleSupervisorCheck(ctx);
return true;
} catch (error) {
+1 -1
View File
@@ -14,7 +14,7 @@
* SETUP (plan mode) 1. planDrafting — draft goals into the plan file (read-only), sent once
* EXEC, after compact 2. resync — the WHOLE file back, once
* SIGN-OFF, agent-side 3. completeGoal* — the one blessed tool's description
* SUPERVISION worker.ts - retained implementation worker
* SUPERVISION worker.ts - retained supervisor and foreground worker
*
* The goal's test is the DISCRIMINATOR: the concrete observation that tells real success from the
* named subtle failure mode. Evidence is empty at planning and filled at sign-off.
+55 -61
View File
@@ -4,52 +4,46 @@ 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, retainedRunState } from "./worker.js";
import { GOAL_WORKER_AGENT, processWorkState } from "./worker.js";
const NESTED_STATE = "pi-goals-nested-worker";
const COMPACTED_STATE = "pi-goals-supervisor-compacted";
interface NestedState {
runId: string | null;
pending: boolean;
}
function result(text: string, isError = false) {
return { content: [{ type: "text" as const, text }], details: {}, isError };
}
function targetRun(input: Record<string, unknown>): string | null {
const value = input.id ?? input.runId;
return typeof value === "string" && value ? value : null;
interface GoalBindings {
compactPlanning?: boolean;
workerModel?: string | null;
}
function compactPlanningRequested(): boolean {
function goalBindings(): GoalBindings {
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;
if (!raw) return {};
const binding = (JSON.parse(raw) as { "pi-goals/1"?: GoalBindings })["pi-goals/1"] ?? {};
if (binding.workerModel !== undefined && binding.workerModel !== null && typeof binding.workerModel !== "string") throw new Error("pi-goals workerModel binding must be a string or null.");
return binding;
}
function messageLaunchesWorker(ctx: { sessionManager: { getBranch(): unknown[] } }): boolean {
const entry = [...ctx.sessionManager.getBranch()].reverse().find((candidate) => {
const value = candidate as { type?: unknown; message?: { role?: unknown } };
return value.type === "message" && value.message?.role === "assistant";
}) as { message?: { content?: unknown } } | undefined;
if (!Array.isArray(entry?.message?.content)) return false;
return entry.message.content.some((part) => {
const value = part as { type?: unknown; name?: unknown; arguments?: Record<string, unknown> };
return value.type === "toolCall" && value.name === "subagent" && value.arguments?.agent === GOAL_WORKER_AGENT;
});
}
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) => {
const event = raw as { id?: unknown; agent?: unknown };
if (event.agent !== "goal-worker" || typeof event.id !== "string") return;
nested = { runId: event.id, pending: true };
persist();
});
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);
let currentTurn = -1;
let completedWorkerTurn: number | null = null;
let workerModel: string | null = null;
const activeWorkerCalls = new Set<string>();
pi.on("session_before_compact", async (event) => {
if (!compacting) return;
@@ -67,15 +61,9 @@ export default function goalSupervisorRuntime(pi: ExtensionAPI): void {
pi.on("session_start", async (_event, ctx) => {
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 (nested.pending && nested.runId && (await retainedRunState(pi.events, nested.runId)) === "idle") {
nested = { ...nested, pending: false };
persist();
}
if (!compactPlanningRequested()) return;
const bindings = goalBindings();
workerModel = bindings.workerModel ?? null;
if (bindings.compactPlanning !== true) 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) => {
@@ -97,6 +85,11 @@ export default function goalSupervisorRuntime(pi: ExtensionAPI): void {
await compactionDone;
});
pi.on("turn_start", async (event) => {
activeWorkerCalls.clear();
currentTurn = event.turnIndex;
});
pi.on("tool_call", async (event) => {
if (event.toolName === "edit" || event.toolName === "write") {
return { block: true, reason: "Goal supervision is read-only. Direct project changes to the nested goal-worker." };
@@ -106,34 +99,33 @@ export default function goalSupervisorRuntime(pi: ExtensionAPI): void {
}
if (event.toolName !== "subagent") return;
const input = event.input as Record<string, unknown>;
const action = typeof input.action === "string" ? input.action : null;
if (!action) {
if (input.agent === "goal-worker" && !nested.pending && input.workflowScript === undefined && input.workflowScriptPath === undefined) return;
return { block: true, reason: nested.pending ? "Wait for the retained goal-worker instead of starting another worker." : "The supervisor may start only goal-worker." };
const allowedKeys = new Set(["agent", "task", "async", "context", ...(workerModel ? ["model"] : [])]);
const unexpectedKeys = Object.keys(input).filter((key) => !allowedKeys.has(key));
const validWorker = input.agent === GOAL_WORKER_AGENT
&& typeof input.task === "string"
&& input.task.trim().length > 0
&& input.async === false
&& input.context === "fork"
&& (workerModel ? input.model === workerModel : input.model === undefined)
&& unexpectedKeys.length === 0;
if (!validWorker) {
const model = workerModel ? `, model:${JSON.stringify(workerModel)}` : "";
return { block: true, reason: `Launch only ${GOAL_WORKER_AGENT} with task, async:false, context:"fork"${model}, and no other fields.` };
}
if (action === "list") 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) {
if (nested.pending) return;
return { block: true, reason: "The retained goal-worker is terminal; start a replacement worker for a correction." };
}
return { block: true, reason: "The supervisor may inspect or control only its retained goal-worker." };
if (activeWorkerCalls.size > 0) return { block: true, reason: "A foreground goal-worker is already running." };
activeWorkerCalls.add(event.toolCallId);
completedWorkerTurn = null;
});
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.on("tool_result", async (event) => {
if (!activeWorkerCalls.delete(event.toolCallId)) return;
if (!event.isError) completedWorkerTurn = currentTurn;
});
pi.registerTool({
name: "ApproveGoal",
label: "Approve goal",
executionMode: "sequential",
description: "Record approval after inspecting the plan, repository, evidence, and saved verification output. Active or unknown work blocks approval.",
parameters: Type.Object({
approvalId: Type.String({ minLength: 1, description: "Exact approval ID from the latest main-coordinator direction." }),
@@ -146,7 +138,9 @@ export default function goalSupervisorRuntime(pi: ExtensionAPI): void {
inspectedVerifyOutput: Type.Literal(true),
}),
async execute(_id, params, _signal, _onUpdate, ctx) {
if (nested.pending) return result("Cannot approve while the retained worker is pending.", true);
if (messageLaunchesWorker(ctx) || activeWorkerCalls.size > 0 || completedWorkerTurn === null || completedWorkerTurn >= currentTurn) {
return result("Cannot approve in a worker-launch message or before reviewing a finished worker on a later turn.", true);
}
const processes = processWorkState(pi.events);
if (processes !== "idle") return result(`Cannot approve: processes=${processes}.`, true);
const planPath = resolve(params.planPath);
+33 -49
View File
@@ -7,6 +7,7 @@ const RPC_REPLY_PREFIX = "subagents:rpc:v1:reply:";
const RPC_VERSION = 1;
const RPC_TIMEOUT_MS = 15_000;
export const SUPERVISOR_AGENT = "goal-supervisor";
export const GOAL_WORKER_AGENT = "pi-goals-worker-v1";
interface EventBus {
on(event: string, handler: (data: unknown) => void): () => void;
@@ -39,44 +40,43 @@ 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.
Your forked planning history is compacted before your first turn. Launch one goal-worker, then call bg_wait with its run ID
so this supervisory turn stays alive until the worker completes or needs attention. Do not poll status or repeatedly steer
an active worker. Use CheckWorkerState once only after a needs-attention notice or a scheduled review. If a terminal worker
needs a correction, launch one replacement goal-worker instead of resuming its old run ID. 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`;
Your forked planning history may be compacted before your first turn. Launch ${GOAL_WORKER_AGENT} in the foreground with exactly
agent, task, async:false, context:"fork", and, when named in the current direction, that worker model. Wait for its result; do not use
bg_wait or worker run IDs. 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. If the evidence needs a correction, launch a new foreground ${GOAL_WORKER_AGENT} with one concrete task
and wait for it. On a later turn, when HEAD is committed, the worktree is clean, and the evidence proves the discriminator, call
ApproveGoal with the current approval ID. 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));
const request: Record<string, unknown> = {
version: 1,
name: SUPERVISOR_AGENT,
definition: {
description: "Read-only supervisor that owns a nested retained implementation worker.",
systemPrompt: supervisorSystemPrompt,
tools: ["read", "grep", "find", "ls", "bash", "subagent", "bg_wait", "CheckWorkerState", "ApproveGoal"],
allowNestedSubagents: true,
subagentOnlyExtensions: [supervisorRuntime],
...(model ? { model } : {}),
systemPromptMode: "replace",
thinking: "low",
inheritProjectContext: false,
inheritGlobalContext: false,
inheritSkills: false,
defaultContext: "fork",
defaultAsync: true,
defaultProgress: true,
},
};
function registerRuntimeAgent(events: EventBus, name: string, definition: Record<string, unknown>): Registration {
const request: Record<string, unknown> = { version: 1, name, definition };
events.emit(REGISTER_EVENT, request);
const result = request.result as { ok?: boolean; registration?: Registration; error?: Error } | undefined;
if (!result) throw new Error("pi-subagents is not installed or not ready.");
if (!result.ok || !result.registration) throw result.error ?? new Error("pi-subagents rejected the goal-supervisor agent.");
if (!result.ok || !result.registration) throw result.error ?? new Error(`pi-subagents rejected the ${name} agent.`);
return result.registration;
}
export function registerGoalSupervisor(events: EventBus, model: string | null): Registration {
const supervisorRuntime = fileURLToPath(new URL("./supervisor-runtime.ts", import.meta.url));
return registerRuntimeAgent(events, SUPERVISOR_AGENT, {
description: "Read-only supervisor that owns a foreground implementation worker.",
systemPrompt: supervisorSystemPrompt,
tools: ["read", "grep", "find", "ls", "bash", "subagent", "ApproveGoal"],
allowNestedSubagents: true,
subagentOnlyExtensions: [supervisorRuntime],
...(model ? { model } : {}),
systemPromptMode: "replace",
thinking: "low",
inheritProjectContext: false,
inheritGlobalContext: false,
inheritSkills: false,
defaultContext: "fork",
defaultAsync: true,
defaultProgress: true,
});
}
async function rpc(events: EventBus, method: "spawn" | "resume" | "steer" | "status" | "stop", params: Record<string, unknown>, signal?: AbortSignal): Promise<RpcData> {
if (signal?.aborted) throw new Error("Goal-worker request aborted.");
const requestId = randomUUID();
@@ -114,7 +114,7 @@ function asyncRunId(data: RpcData): string {
return runId;
}
export async function startGoalSupervisor(events: EventBus, cwd: string, task: string, compactPlanning: boolean, signal?: AbortSignal): Promise<string> {
export async function startGoalSupervisor(events: EventBus, cwd: string, task: string, compactPlanning: boolean, workerModel: string | null, signal?: AbortSignal): Promise<string> {
const data = await rpc(events, "spawn", {
agent: SUPERVISOR_AGENT,
task,
@@ -122,7 +122,7 @@ export async function startGoalSupervisor(events: EventBus, cwd: string, task: s
context: "fork",
async: true,
mission: false,
extensionBindings: { "pi-goals/1": { compactPlanning } },
extensionBindings: { "pi-goals/1": { compactPlanning, workerModel } },
}, signal);
return asyncRunId(data);
}
@@ -157,15 +157,6 @@ function validSnapshot(snapshot: AsyncSnapshot | undefined): snapshot is AsyncSn
return snapshot?.kind === "pi-subagents.async-status-snapshot" && snapshot.version === 1 && snapshot.omitted.runs === 0 && snapshot.omitted.children === 0 && !snapshot.omitted.byteLimitExceeded;
}
function findNode(nodes: AsyncNode[], runId: string): AsyncNode | undefined {
for (const node of nodes) {
if (node.id === runId) return node;
const child = node.children && findNode(node.children, runId);
if (child) return child;
}
return undefined;
}
async function asyncSnapshot(events: EventBus): Promise<AsyncSnapshot | undefined> {
return (await rpc(events, "status", {})).asyncSnapshot;
}
@@ -176,13 +167,6 @@ export async function subagentWorkState(events: EventBus): Promise<WorkState> {
return snapshot.runs.some(activeNode) ? "active" : "idle";
}
export async function retainedRunState(events: EventBus, runId: string): Promise<WorkState> {
const snapshot = await asyncSnapshot(events);
if (!validSnapshot(snapshot)) return "unknown";
const node = findNode(snapshot.runs, runId);
return node && activeNode(node) ? "active" : "idle";
}
export interface ProcessInfo {
status: string;
}