Archived
fix(sessions): clarify delegation tool guidance
This commit is contained in:
@@ -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.
|
||||||
@@ -20,11 +20,12 @@ describe("createSpawnSessionToolDefinition", () => {
|
|||||||
expect(result.content[0]).toMatchObject({ type: "text", text: "Started independent session new-1 in /repos/a-feature." });
|
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() });
|
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).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.description).not.toMatch(/use this|continue work|follow a plan|relay/i);
|
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 () => {
|
it("forwards omitted cwd as undefined and omits a missing dispatching model", async () => {
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ export function createSpawnSessionToolDefinition(spawningCwd: string, deps: Spaw
|
|||||||
return defineTool<typeof SpawnSessionParams, SpawnSessionToolDetails>({
|
return defineTool<typeof SpawnSessionParams, SpawnSessionToolDetails>({
|
||||||
name: "spawn_session",
|
name: "spawn_session",
|
||||||
label: "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.",
|
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: start a new independent session with a first prompt",
|
promptSnippet: "spawn_session: independent session; results unavailable here; explicit requests only",
|
||||||
parameters: SpawnSessionParams,
|
parameters: SpawnSessionParams,
|
||||||
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
||||||
// Failures throw: the agent loop turns the thrown message into an error
|
// Failures throw: the agent loop turns the thrown message into an error
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ function tools(deps: Partial<SubsessionToolDeps>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function workingGuidance(sessionId: string): string {
|
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 {
|
function firstText(content: readonly (TextContent | ImageContent)[]): string {
|
||||||
@@ -62,16 +62,17 @@ describe("createSubsessionToolDefinitions", () => {
|
|||||||
expect(firstText(result.content)).toContain("Started tracked subsession child-1");
|
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({
|
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 and return immediately. Continue independent work, then use yield_to_subsessions at the join point. Completion notices wake you; do not poll.");
|
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 parallel work; continue, then join with yield_to_subsessions");
|
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));
|
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", () => {
|
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.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.promptSnippet).toBe("yield_to_subsessions: end the run at a join point; call alone and last");
|
||||||
expect(yieldTool.promptGuidelines).toEqual([
|
expect(yieldTool.promptGuidelines).toEqual([
|
||||||
"After independent work, yield only at a join point; use spawn_session for fire-and-forget work.",
|
"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.",
|
||||||
"Call alone and last; a mixed tool batch may continue the run.",
|
|
||||||
"Completion notices wake you; do not poll inspection tools.",
|
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ function statusLine(summary: SubsessionSummary): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function workingInspectionGuidance(sessionId: string): 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 {
|
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
|
* Tools that let an agent spawn *tracked* child sessions, inspect them, and
|
||||||
* explicitly yield at a join point.
|
* explicitly yield at a join point.
|
||||||
*
|
*
|
||||||
* Unlike `spawn_session` (fire-and-forget peers), a subsession records its
|
* A subsession records its parent in its session header, the parent is notified
|
||||||
* parent in its session header, the parent is notified when it stops working,
|
* when it stops working, and the parent may read its transcript/result. The
|
||||||
* and the parent may read its transcript/result. The tools are constructed
|
* tools are constructed per-session, carrying the spawning cwd for
|
||||||
* per-session, carrying the spawning cwd for project-scope validation; the
|
* project-scope validation; the parent's identity is taken from the live
|
||||||
* parent's identity is taken from the live extension context at call time.
|
* extension context at call time.
|
||||||
*/
|
*/
|
||||||
export function createSubsessionToolDefinitions(spawningCwd: string, deps: SubsessionToolDeps) {
|
export function createSubsessionToolDefinitions(spawningCwd: string, deps: SubsessionToolDeps) {
|
||||||
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 and return immediately. Continue independent work, then use yield_to_subsessions at the join point. Completion notices wake you; do not poll.",
|
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 parallel work; continue, then join with yield_to_subsessions",
|
promptSnippet: "spawn_subsession: tracked child work for the current task; result available after completion",
|
||||||
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();
|
||||||
@@ -199,7 +199,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}. 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,
|
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.",
|
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",
|
promptSnippet: "yield_to_subsessions: end the run at a join point; call alone and last",
|
||||||
promptGuidelines: [
|
promptGuidelines: [
|
||||||
"After independent work, yield only at a join point; use spawn_session for fire-and-forget work.",
|
"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.",
|
||||||
"Call alone and last; a mixed tool batch may continue the run.",
|
|
||||||
"Completion notices wake you; do not poll inspection tools.",
|
|
||||||
],
|
],
|
||||||
parameters: YieldToSubsessionsParams,
|
parameters: YieldToSubsessionsParams,
|
||||||
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
||||||
|
|||||||
Reference in New Issue
Block a user