diff --git a/.changeset/clarify-session-delegation.md b/.changeset/clarify-session-delegation.md new file mode 100644 index 0000000..2a0d2e1 --- /dev/null +++ b/.changeset/clarify-session-delegation.md @@ -0,0 +1,5 @@ +--- +"@jmfederico/pi-web": patch +--- + +Clarify agent instructions so independent sessions are created only when explicitly requested and tracked subsessions remain part of the current task. diff --git a/src/server/sessions/spawnSessionTool.test.ts b/src/server/sessions/spawnSessionTool.test.ts index 3c23363..f2fa1c6 100644 --- a/src/server/sessions/spawnSessionTool.test.ts +++ b/src/server/sessions/spawnSessionTool.test.ts @@ -20,11 +20,12 @@ describe("createSpawnSessionToolDefinition", () => { expect(result.content[0]).toMatchObject({ type: "text", text: "Started independent session new-1 in /repos/a-feature." }); }); - it("describes the independent-session capability without workflow policy", () => { + it("describes a fully independent session and restricts it to explicit requests", () => { const tool = createSpawnSessionToolDefinition("/repos/a", { spawn: vi.fn() }); - expect(tool.description).toBe("Start a new independent pi-web session and send it an initial prompt. The session is not tracked by the caller, can be opened by a human, and runs without returning its later output to the caller."); - expect(tool.description).not.toMatch(/use this|continue work|follow a plan|relay/i); + expect(tool.description).toBe("Start a fully independent session; its transcript and results are unavailable here. Use only when the user or active workflow explicitly requests a separate session."); + expect(tool.promptSnippet).toBe("spawn_session: independent session; results unavailable here; explicit requests only"); + expect(tool.description).not.toMatch(/subsession|child|parent/i); }); it("forwards omitted cwd as undefined and omits a missing dispatching model", async () => { diff --git a/src/server/sessions/spawnSessionTool.ts b/src/server/sessions/spawnSessionTool.ts index 43cbb1f..aad33ee 100644 --- a/src/server/sessions/spawnSessionTool.ts +++ b/src/server/sessions/spawnSessionTool.ts @@ -41,8 +41,8 @@ export function createSpawnSessionToolDefinition(spawningCwd: string, deps: Spaw return defineTool({ name: "spawn_session", label: "Spawn session", - description: "Start a new independent pi-web session and send it an initial prompt. The session is not tracked by the caller, can be opened by a human, and runs without returning its later output to the caller.", - promptSnippet: "spawn_session: start a new independent session with a first prompt", + description: "Start a fully independent session; its transcript and results are unavailable here. Use only when the user or active workflow explicitly requests a separate session.", + promptSnippet: "spawn_session: independent session; results unavailable here; explicit requests only", parameters: SpawnSessionParams, async execute(_toolCallId, params, _signal, _onUpdate, ctx) { // Failures throw: the agent loop turns the thrown message into an error diff --git a/src/server/sessions/spawnSubsessionTool.test.ts b/src/server/sessions/spawnSubsessionTool.test.ts index c68396f..bf25ccb 100644 --- a/src/server/sessions/spawnSubsessionTool.test.ts +++ b/src/server/sessions/spawnSubsessionTool.test.ts @@ -35,7 +35,7 @@ function tools(deps: Partial) { } function workingGuidance(sessionId: string): string { - return `Subsession ${sessionId} is working; partial output is withheld. Continue independent work, or call yield_to_subsessions alone and last at the join point. Completion notices wake you; do not poll.`; + return `Subsession ${sessionId} is working; partial output is withheld. Continue other work, or call yield_to_subsessions alone and last at the join point. Completion notices wake you; do not poll.`; } function firstText(content: readonly (TextContent | ImageContent)[]): string { @@ -62,16 +62,17 @@ describe("createSubsessionToolDefinitions", () => { expect(firstText(result.content)).toContain("Started tracked subsession child-1"); }); - it("guides the parent to continue independent work and use the explicit join action", async () => { + it("describes tracked child work whose result remains available to the parent", 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 and return immediately. Continue independent work, then use yield_to_subsessions at the join point. Completion notices wake you; do not poll."); - expect(spawnTool.promptSnippet).toBe("spawn_subsession: tracked parallel work; continue, then join with yield_to_subsessions"); + expect(spawnTool.description).toBe("Start a tracked child session to carry out part of the current task and return immediately. Its transcript and result are available here after it finishes."); + expect(spawnTool.promptSnippet).toBe("spawn_subsession: tracked child work for the current task; result available after completion"); + expect(spawnTool.description).not.toMatch(/spawn_session|fully independent/i); const result = await spawnTool.execute("call-contract", { prompt: "do it" }, undefined, undefined, ctxFor("parent-1", undefined)); - expect(firstText(result.content)).toBe("Started tracked subsession child-1 in /repos/a-feature. Continue independent work, then join with yield_to_subsessions; do not poll."); + expect(firstText(result.content)).toBe("Started tracked subsession child-1 in /repos/a-feature. Continue other work, then join with yield_to_subsessions; do not poll."); }); it("distinguishes status inspection from yielding in tool metadata", () => { @@ -92,9 +93,7 @@ describe("createSubsessionToolDefinitions", () => { expect(yieldTool.description).toBe("At a join point, end this run while tracked children work; completion notices wake you. If none work, continue. Call alone and last; do not poll."); expect(yieldTool.promptSnippet).toBe("yield_to_subsessions: end the run at a join point; call alone and last"); expect(yieldTool.promptGuidelines).toEqual([ - "After independent work, yield only at a join point; use spawn_session for fire-and-forget work.", - "Call alone and last; a mixed tool batch may continue the run.", - "Completion notices wake you; do not poll inspection tools.", + "After calling spawn_subsession, you can continue with other work. At the join point, call yield_to_subsessions alone and last; completion notices wake you, so do not poll.", ]); }); diff --git a/src/server/sessions/spawnSubsessionTool.ts b/src/server/sessions/spawnSubsessionTool.ts index 1bb7357..0f9aec8 100644 --- a/src/server/sessions/spawnSubsessionTool.ts +++ b/src/server/sessions/spawnSubsessionTool.ts @@ -120,7 +120,7 @@ function statusLine(summary: SubsessionSummary): string { } function workingInspectionGuidance(sessionId: string): string { - return `Subsession ${sessionId} is working; partial output is withheld. Continue independent work, or call yield_to_subsessions alone and last at the join point. Completion notices wake you; do not poll.`; + return `Subsession ${sessionId} is working; partial output is withheld. Continue other work, or call yield_to_subsessions alone and last at the join point. Completion notices wake you; do not poll.`; } function renderEntry(entry: TranscriptEntry): string { @@ -174,18 +174,18 @@ function renderTranscript(result: SubsessionReadResult): string { * Tools that let an agent spawn *tracked* child sessions, inspect them, and * explicitly yield at a join point. * - * Unlike `spawn_session` (fire-and-forget peers), a subsession records its - * parent in its session header, the parent is notified when it stops working, - * and the parent may read its transcript/result. The tools are constructed - * per-session, carrying the spawning cwd for project-scope validation; the - * parent's identity is taken from the live extension context at call time. + * A subsession records its parent in its session header, the parent is notified + * when it stops working, and the parent may read its transcript/result. The + * tools are constructed per-session, carrying the spawning cwd for + * project-scope validation; the parent's identity is taken from the live + * extension context at call time. */ export function createSubsessionToolDefinitions(spawningCwd: string, deps: SubsessionToolDeps) { const spawnTool = defineTool({ name: "spawn_subsession", label: "Spawn subsession", - description: "Start a tracked child and return immediately. Continue independent work, then use yield_to_subsessions at the join point. Completion notices wake you; do not poll.", - promptSnippet: "spawn_subsession: tracked parallel work; continue, then join with yield_to_subsessions", + description: "Start a tracked child session to carry out part of the current task and return immediately. Its transcript and result are available here after it finishes.", + promptSnippet: "spawn_subsession: tracked child work for the current task; result available after completion", parameters: SpawnSubsessionParams, async execute(_toolCallId, params, _signal, _onUpdate, ctx) { const parentSessionId = ctx.sessionManager.getSessionId(); @@ -199,7 +199,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}. Continue independent work, then join with yield_to_subsessions; do not poll.` }], + content: [{ type: "text", text: `Started tracked subsession ${result.sessionId} in ${result.cwd}. Continue other work, then join with yield_to_subsessions; do not poll.` }], details: result, }; }, @@ -270,9 +270,7 @@ export function createSubsessionToolDefinitions(spawningCwd: string, deps: Subse description: "At a join point, end this run while tracked children work; completion notices wake you. If none work, continue. Call alone and last; do not poll.", promptSnippet: "yield_to_subsessions: end the run at a join point; call alone and last", promptGuidelines: [ - "After independent work, yield only at a join point; use spawn_session for fire-and-forget work.", - "Call alone and last; a mixed tool batch may continue the run.", - "Completion notices wake you; do not poll inspection tools.", + "After calling spawn_subsession, you can continue with other work. At the join point, call yield_to_subsessions alone and last; completion notices wake you, so do not poll.", ], parameters: YieldToSubsessionsParams, async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {