diff --git a/.changeset/clarify-subsession-waiting.md b/.changeset/clarify-subsession-waiting.md index 2ebeec3..c76196c 100644 --- a/.changeset/clarify-subsession-waiting.md +++ b/.changeset/clarify-subsession-waiting.md @@ -2,4 +2,4 @@ "@jmfederico/pi-web": patch --- -Keep delegation tools available to human-created and independently spawned sessions, remove them from tracked child sessions, and make delegation tool contracts capability-focused. +Keep delegation tools available to human-created and independently spawned sessions, remove them from tracked child sessions, and guide parent agents to track required subsessions and yield at a join point instead of polling. diff --git a/docs/config.html b/docs/config.html index 81a6443..766ea90 100644 --- a/docs/config.html +++ b/docs/config.html @@ -513,8 +513,19 @@ to be enabled.

- Tracked subsessions let an agent delegate work to child sessions, get notified when children stop - working, and inspect their transcripts. Restart the session daemon after changing this setting. + Tracked subsessions let an agent delegate work to child sessions, receive a notification when each child + stops working, and inspect their status and transcripts. Calling spawn_subsession returns + immediately. The parent can continue independent work while treating every child whose result it needs + as pending. Before producing work that depends on those results, the parent reaches a join point and + yields until every required child has sent a completion notice. +

+

+ A completion notice wakes an idle parent. If the parent is busy, the notice queues until the current + turn ends rather than interrupting in-flight work. For multiple required children, each notice resolves + one pending child; after processing it, the parent yields again if another required child is pending. + list_subsessions, check_subsession, and read_subsession provide + on-demand status and transcript inspection for deliberate progress checks or recovery. Completion + notifications, rather than polling these tools, are the normal synchronization mechanism.

In Settings → Session daemon, these keys are saved on the selected machine. Restart the diff --git a/docs/config.md b/docs/config.md index 61553c4..1d8dfee 100644 --- a/docs/config.md +++ b/docs/config.md @@ -171,7 +171,9 @@ The per-request size limit is still controlled by `maxUploadBytes` / `PI_WEB_MAX `subsessions` is beta and controls whether agents receive the tracked-subsession tools: `spawn_subsession`, `list_subsessions`, `check_subsession`, and `read_subsession`. It defaults to `false` and also requires `spawnSessions` to be enabled. -Tracked subsessions let an agent delegate work to child sessions, get notified when children stop working, and inspect their transcripts. +Tracked subsessions let an agent delegate work to child sessions, receive a notification when each child stops working, and inspect their status and transcripts. Calling `spawn_subsession` returns immediately. The parent can continue independent work while treating every child whose result it needs as pending. Before producing work that depends on those results, the parent reaches a join point and yields until every required child has sent a completion notice. + +A completion notice wakes an idle parent. If the parent is busy, the notice queues until the current turn ends rather than interrupting in-flight work. For multiple required children, each notice resolves one pending child; after processing it, the parent yields again if another required child is pending. `list_subsessions`, `check_subsession`, and `read_subsession` provide on-demand status and transcript inspection for deliberate progress checks or recovery. Completion notifications, rather than polling these tools, are the normal synchronization mechanism. In **Settings → Session daemon**, these keys are saved on the selected machine. Restart the session daemon on that machine after changing them. diff --git a/src/server/sessions/spawnSubsessionTool.test.ts b/src/server/sessions/spawnSubsessionTool.test.ts index c49999b..dd9546a 100644 --- a/src/server/sessions/spawnSubsessionTool.test.ts +++ b/src/server/sessions/spawnSubsessionTool.test.ts @@ -52,26 +52,25 @@ describe("createSubsessionToolDefinitions", () => { expect(firstText(result.content)).toContain("Started tracked subsession child-1"); }); - it("describes tracked dispatch and notification without workflow policy", async () => { + it("guides the parent to join all required subsessions without polling", async () => { const { spawn: spawnTool } = tools({ spawn: vi.fn(() => Promise.resolve({ sessionId: "child-1", cwd: "/repos/a-feature" })), }); - expect(spawnTool.description).toBe("Start a tracked child session and send it an initial prompt. The call returns after dispatch; the parent is notified when the child stops working and can inspect its status, latest output, and transcript."); + expect(spawnTool.description).toBe("Start a tracked child and return after dispatch. Track required children as pending: continue independent work, then yield at a join point until all have notified completion. Notifications queue while the parent is busy; do not poll for completion."); + expect(spawnTool.promptSnippet).toBe("spawn_subsession: delegate parallel work; yield at a join point until all required children complete."); const result = await spawnTool.execute("call-contract", { prompt: "do it" }, undefined, undefined, ctxFor("parent-1", undefined)); - const message = firstText(result.content); - expect(message).toBe("Started tracked subsession child-1 in /repos/a-feature. The parent will be notified when it stops working."); - expect(`${spawnTool.description}\n${message}`).not.toMatch(/do not poll|continue (?:useful|independent) work|end (?:this|the) turn|relay/i); + expect(firstText(result.content)).toBe("Started tracked subsession child-1 in /repos/a-feature. Track it as pending and, before finalizing dependent work, yield until all required children have notified completion."); }); - it("keeps all subsession tool descriptions capability-oriented", () => { + it("keeps subsession inspection tool descriptions capability-oriented", () => { const definitions = tools({}); expect(definitions.list.description).toBe("List tracked child sessions owned by the calling session, with each child's current status (working, idle, error, or unknown)."); expect(definitions.check.description).toBe("Return a tracked subsession's current status, message count, and most recent assistant output."); expect(definitions.read.description).toBe("Return a filtered, paginated transcript of a tracked subsession. Filters select message roles and content kinds, search full message content, optionally include raw tool arguments, and cap or page the returned entries."); - for (const definition of Object.values(definitions)) { + for (const definition of [definitions.list, definitions.check, definitions.read]) { expect(definition.description).not.toMatch(/use this|do not poll|continue working|start narrow|for just the final|relay/i); } }); diff --git a/src/server/sessions/spawnSubsessionTool.ts b/src/server/sessions/spawnSubsessionTool.ts index 1bb0cf1..b3c2a01 100644 --- a/src/server/sessions/spawnSubsessionTool.ts +++ b/src/server/sessions/spawnSubsessionTool.ts @@ -178,8 +178,8 @@ export function createSubsessionToolDefinitions(spawningCwd: string, deps: Subse const spawnTool = defineTool({ name: "spawn_subsession", label: "Spawn subsession", - description: "Start a tracked child session and send it an initial prompt. The call returns after dispatch; the parent is notified when the child stops working and can inspect its status, latest output, and transcript.", - promptSnippet: "spawn_subsession: start a tracked child session you will be notified about", + description: "Start a tracked child and return after dispatch. Track required children as pending: continue independent work, then yield at a join point until all have notified completion. Notifications queue while the parent is busy; do not poll for completion.", + promptSnippet: "spawn_subsession: delegate parallel work; yield at a join point until all required children complete.", parameters: SpawnSubsessionParams, async execute(_toolCallId, params, _signal, _onUpdate, ctx) { const parentSessionId = ctx.sessionManager.getSessionId(); @@ -193,7 +193,7 @@ export function createSubsessionToolDefinitions(spawningCwd: string, deps: Subse ...(ctx.model === undefined ? {} : { model: ctx.model }), }); return { - content: [{ type: "text", text: `Started tracked subsession ${result.sessionId} in ${result.cwd}. The parent will be notified when it stops working.` }], + content: [{ type: "text", text: `Started tracked subsession ${result.sessionId} in ${result.cwd}. Track it as pending and, before finalizing dependent work, yield until all required children have notified completion.` }], details: result, }; },