fix(sessions): clarify subsession join guidance

This commit is contained in:
Federico Jaramillo Martinez
2026-07-11 22:03:31 +02:00
parent dfab743a71
commit 6fbeed345e
5 changed files with 26 additions and 14 deletions
+1 -1
View File
@@ -2,4 +2,4 @@
"@jmfederico/pi-web": patch "@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.
+13 -2
View File
@@ -513,8 +513,19 @@
to be enabled. to be enabled.
</p> </p>
<p> <p>
Tracked subsessions let an agent delegate work to child sessions, get notified when children stop Tracked subsessions let an agent delegate work to child sessions, receive a notification when each child
working, and inspect their transcripts. Restart the session daemon after changing this setting. stops working, and inspect their status and transcripts. Calling <code>spawn_subsession</code> 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.
</p>
<p>
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.
<code>list_subsessions</code>, <code>check_subsession</code>, and <code>read_subsession</code> 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.
</p> </p>
<p> <p>
In <strong>Settings → Session daemon</strong>, these keys are saved on the selected machine. Restart the In <strong>Settings → Session daemon</strong>, these keys are saved on the selected machine. Restart the
+3 -1
View File
@@ -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. `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. In **Settings → Session daemon**, these keys are saved on the selected machine. Restart the session daemon on that machine after changing them.
@@ -52,26 +52,25 @@ describe("createSubsessionToolDefinitions", () => {
expect(firstText(result.content)).toContain("Started tracked subsession child-1"); 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({ const { spawn: spawnTool } = tools({
spawn: vi.fn(() => Promise.resolve({ sessionId: "child-1", cwd: "/repos/a-feature" })), 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 result = await spawnTool.execute("call-contract", { prompt: "do it" }, undefined, undefined, ctxFor("parent-1", undefined));
const message = firstText(result.content); 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.");
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);
}); });
it("keeps all subsession tool descriptions capability-oriented", () => { it("keeps subsession inspection tool descriptions capability-oriented", () => {
const definitions = tools({}); 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.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.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."); 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); expect(definition.description).not.toMatch(/use this|do not poll|continue working|start narrow|for just the final|relay/i);
} }
}); });
+3 -3
View File
@@ -178,8 +178,8 @@ export function createSubsessionToolDefinitions(spawningCwd: string, deps: Subse
const spawnTool = defineTool<typeof SpawnSubsessionParams, SpawnSubsessionResult>({ const spawnTool = defineTool<typeof SpawnSubsessionParams, SpawnSubsessionResult>({
name: "spawn_subsession", name: "spawn_subsession",
label: "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.", 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: start a tracked child session you will be notified about", promptSnippet: "spawn_subsession: delegate parallel work; yield at a join point until all required children complete.",
parameters: SpawnSubsessionParams, parameters: SpawnSubsessionParams,
async execute(_toolCallId, params, _signal, _onUpdate, ctx) { async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
const parentSessionId = ctx.sessionManager.getSessionId(); const parentSessionId = ctx.sessionManager.getSessionId();
@@ -193,7 +193,7 @@ export function createSubsessionToolDefinitions(spawningCwd: string, deps: Subse
...(ctx.model === undefined ? {} : { model: ctx.model }), ...(ctx.model === undefined ? {} : { model: ctx.model }),
}); });
return { 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, details: result,
}; };
}, },