Archived
fix: report remaining subsessions on completion
This commit is contained in:
@@ -2,4 +2,4 @@
|
|||||||
"@jmfederico/pi-web": patch
|
"@jmfederico/pi-web": patch
|
||||||
---
|
---
|
||||||
|
|
||||||
Add an explicit tracked-subsession yield action so parents can end their run at join points and resume from completion notifications without polling or reading partial child output.
|
Add explicit tracked-subsession yielding, with wake-up notices that identify children still working so parents can continue or yield again without polling or reading partial output.
|
||||||
|
|||||||
+8
-10
@@ -542,19 +542,17 @@
|
|||||||
clearly reports that there is nothing to wait for.
|
clearly reports that there is nothing to wait for.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
A completion notice automatically wakes an idle parent. If the parent is busy, the notice queues until
|
A completion notice wakes an idle parent or queues behind in-flight work. Each notice lists any other
|
||||||
the current turn ends rather than interrupting in-flight work. When multiple children finish at
|
tracked children still working, so the parent can continue work or call
|
||||||
different times, the parent handles each completion and calls <code>yield_to_subsessions</code> again
|
<code>yield_to_subsessions</code> again at the next join point. Further notices arrive automatically; do
|
||||||
while another child is still working.
|
not poll.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<code>list_subsessions</code>, <code>check_subsession</code>, and <code>read_subsession</code> never yield
|
<code>list_subsessions</code>, <code>check_subsession</code>, and <code>read_subsession</code> never yield
|
||||||
or change control flow and are not completion-polling mechanisms. They remain available for deliberate
|
or change control flow. They are for deliberate inspection or recovery, not completion polling. While a
|
||||||
inspection or recovery. While a child is working, agent-facing <code>check_subsession</code> and
|
child works, agent-facing <code>check_subsession</code> and <code>read_subsession</code> withhold partial
|
||||||
<code>read_subsession</code> withhold partial output and transcript entries and instead direct the parent
|
output and direct the parent to continue independent work or yield at the join point. Output becomes
|
||||||
to continue independent work or yield at the join point. Once the child is no longer working, its output
|
available when the child stops.
|
||||||
and transcript are available. Completion notifications, rather than polling inspection 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
|
||||||
|
|||||||
+2
-2
@@ -187,9 +187,9 @@ Tracked subsessions are join-oriented. Calling `spawn_subsession` returns immedi
|
|||||||
|
|
||||||
At a join point, after finishing its independent work, the parent calls `yield_to_subsessions` alone as the final action in its tool batch. Pi ends a tool batch early only when every result in that batch is terminating. If any tracked child is still working, the action ends the current agent run so the parent becomes idle. If none are working, it does not end the run and clearly reports that there is nothing to wait for.
|
At a join point, after finishing its independent work, the parent calls `yield_to_subsessions` alone as the final action in its tool batch. Pi ends a tool batch early only when every result in that batch is terminating. If any tracked child is still working, the action ends the current agent run so the parent becomes idle. If none are working, it does not end the run and clearly reports that there is nothing to wait for.
|
||||||
|
|
||||||
A completion notice automatically wakes an idle parent. If the parent is busy, the notice queues until the current turn ends rather than interrupting in-flight work. When multiple children finish at different times, the parent handles each completion and calls `yield_to_subsessions` again while another child is still working.
|
A completion notice wakes an idle parent or queues behind in-flight work. Each notice lists any other tracked children still working, so the parent can continue work or call `yield_to_subsessions` again at the next join point. Further notices arrive automatically; do not poll.
|
||||||
|
|
||||||
`list_subsessions`, `check_subsession`, and `read_subsession` never yield or change control flow and are not completion-polling mechanisms. They remain available for deliberate inspection or recovery. While a child is working, agent-facing `check_subsession` and `read_subsession` withhold partial output and transcript entries and instead direct the parent to continue independent work or yield at the join point. Once the child is no longer working, its output and transcript are available. Completion notifications, rather than polling inspection tools, are the normal synchronization mechanism.
|
`list_subsessions`, `check_subsession`, and `read_subsession` never yield or change control flow. They are for deliberate inspection or recovery, not completion polling. While a child works, agent-facing `check_subsession` and `read_subsession` withhold partial output and direct the parent to continue independent work or yield at the join point. Output becomes available when the child stops.
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
|
|||||||
@@ -8,10 +8,15 @@ import { CapturingSessionEventHub, emptyArchiveStore, fakeRuntime, fakeSessionMa
|
|||||||
|
|
||||||
describe("PiSessionService", () => {
|
describe("PiSessionService", () => {
|
||||||
describe("spawnSubsession", () => {
|
describe("spawnSubsession", () => {
|
||||||
function subsessionService(decision: SpawnTargetDecision, heartbeatIntervalMs = 60_000) {
|
function subsessionService(decision: SpawnTargetDecision, heartbeatIntervalMs = 60_000, childIds = ["child-1"]) {
|
||||||
const parent = fakeRuntime("parent-1", { sessionFile: "/tmp/parent-1.jsonl" });
|
const parent = fakeRuntime("parent-1", { sessionFile: "/tmp/parent-1.jsonl" });
|
||||||
const child = fakeRuntime("child-1", { sessionFile: "/tmp/child-1.jsonl", sessionManager: fakeSessionManager("/workspace-feature") });
|
const children = childIds.map((childId) => fakeRuntime(childId, {
|
||||||
const created = [parent.runtime, child.runtime];
|
sessionFile: `/tmp/${childId}.jsonl`,
|
||||||
|
sessionManager: fakeSessionManager("/workspace-feature"),
|
||||||
|
}));
|
||||||
|
const child = children[0];
|
||||||
|
if (child === undefined) throw new Error("At least one child fixture is required");
|
||||||
|
const created = [parent.runtime, ...children.map(({ runtime }) => runtime)];
|
||||||
let index = 0;
|
let index = 0;
|
||||||
const createAgentRuntime: RuntimeCreator = async () => {
|
const createAgentRuntime: RuntimeCreator = async () => {
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
@@ -38,7 +43,7 @@ describe("PiSessionService", () => {
|
|||||||
spawnTargets: { resolveSpawnTarget: () => Promise.resolve(decision) },
|
spawnTargets: { resolveSpawnTarget: () => Promise.resolve(decision) },
|
||||||
heartbeatIntervalMs,
|
heartbeatIntervalMs,
|
||||||
});
|
});
|
||||||
return { parent, child, service };
|
return { parent, child, children, service };
|
||||||
}
|
}
|
||||||
|
|
||||||
it("records the parent, delivers the prompt, and lists the tracked child", async () => {
|
it("records the parent, delivers the prompt, and lists the tracked child", async () => {
|
||||||
@@ -777,6 +782,41 @@ describe("PiSessionService", () => {
|
|||||||
await service.dispose();
|
await service.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("reports other working children in each completion notice", async () => {
|
||||||
|
const { parent, children, service } = subsessionService(
|
||||||
|
{ allowed: true, cwd: "/workspace-feature" },
|
||||||
|
60_000,
|
||||||
|
["child-1", "child-2"],
|
||||||
|
);
|
||||||
|
const [first, second] = children;
|
||||||
|
if (first === undefined || second === undefined) throw new Error("Expected two child fixtures");
|
||||||
|
await service.start("/workspace");
|
||||||
|
await service.spawnSubsession({ spawningCwd: "/workspace", parentSessionId: "parent-1", parentSessionFile: "/tmp/parent-1.jsonl", prompt: "first", cwd: "/workspace-feature" });
|
||||||
|
await service.spawnSubsession({ spawningCwd: "/workspace", parentSessionId: "parent-1", parentSessionFile: "/tmp/parent-1.jsonl", prompt: "second", cwd: "/workspace-feature" });
|
||||||
|
|
||||||
|
first.session.isStreaming = true;
|
||||||
|
first.emit({ type: "agent_start" });
|
||||||
|
second.session.isStreaming = true;
|
||||||
|
second.emit({ type: "agent_start" });
|
||||||
|
|
||||||
|
first.session.isStreaming = false;
|
||||||
|
first.emit({ type: "agent_end" });
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||||
|
|
||||||
|
expect(parent.calls.sendCustomMessage[0]?.message.content).toBe(
|
||||||
|
"Subsession child-1 stopped working (idle). Latest output:\n\n(no output)\n\nStill working: child-2. Continue working, or call yield_to_subsessions alone and last at the next join point. Further completion notices arrive automatically; do not poll.",
|
||||||
|
);
|
||||||
|
|
||||||
|
second.session.isStreaming = false;
|
||||||
|
second.emit({ type: "agent_end" });
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||||
|
|
||||||
|
expect(parent.calls.sendCustomMessage[1]?.message.content).toBe(
|
||||||
|
"Subsession child-2 stopped working (idle). Latest output:\n\n(no output)\n\nNo other tracked subsessions are working.",
|
||||||
|
);
|
||||||
|
await service.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
it("notifies via the heartbeat when the child settles without a further event", async () => {
|
it("notifies via the heartbeat when the child settles without a further event", async () => {
|
||||||
const { parent, child, service } = subsessionService({ allowed: true, cwd: "/workspace-feature" }, 10);
|
const { parent, child, service } = subsessionService({ allowed: true, cwd: "/workspace-feature" }, 10);
|
||||||
await service.start("/workspace");
|
await service.start("/workspace");
|
||||||
|
|||||||
@@ -898,6 +898,16 @@ export class PiSessionService {
|
|||||||
return "idle";
|
return "idle";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private workingSubsessionIds(parentSessionId: string): string[] {
|
||||||
|
const childIds = this.subsessionChildren.get(parentSessionId);
|
||||||
|
if (childIds === undefined) return [];
|
||||||
|
return [...childIds].filter((childId) => {
|
||||||
|
const link = this.subsessionLinks.get(childId);
|
||||||
|
const active = link === undefined ? undefined : this.activeChildForSubsessionLink(link);
|
||||||
|
return active !== undefined && this.hasActiveWork(active.runtime.session);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Drive parent notifications from a tracked child's status. Arms a pending
|
* Drive parent notifications from a tracked child's status. Arms a pending
|
||||||
* notification while the child is working, and when it stops fires a single
|
* notification while the child is working, and when it stops fires a single
|
||||||
@@ -917,7 +927,11 @@ export class PiSessionService {
|
|||||||
const status: SubsessionStatus = this.activities.get(childId)?.phase === "error" ? "error" : "idle";
|
const status: SubsessionStatus = this.activities.get(childId)?.phase === "error" ? "error" : "idle";
|
||||||
const finalText = finalAssistantText(historyMessages(session));
|
const finalText = finalAssistantText(historyMessages(session));
|
||||||
const preview = finalText === "" ? "(no output)" : truncateForNotification(finalText);
|
const preview = finalText === "" ? "(no output)" : truncateForNotification(finalText);
|
||||||
const text = `Subsession ${childId} stopped working (status: ${status}). Latest output:\n\n${preview}\n\nStatus and latest output are available through check_subsession with sessionId "${childId}"; its full transcript is available through read_subsession.`;
|
const workingIds = this.workingSubsessionIds(link.parentSessionId);
|
||||||
|
const next = workingIds.length === 0
|
||||||
|
? "No other tracked subsessions are working."
|
||||||
|
: `Still working: ${workingIds.join(", ")}. Continue working, or call yield_to_subsessions alone and last at the next join point. Further completion notices arrive automatically; do not poll.`;
|
||||||
|
const text = `Subsession ${childId} stopped working (${status}). Latest output:\n\n${preview}\n\n${next}`;
|
||||||
void this.notifyParentOfSubsession(link.parentSessionId, childId, text);
|
void this.notifyParentOfSubsession(link.parentSessionId, childId, text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ function tools(deps: Partial<SubsessionToolDeps>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function workingGuidance(sessionId: string): string {
|
function workingGuidance(sessionId: string): string {
|
||||||
return `Subsession ${sessionId} is still working, so partial output is unavailable through agent-facing inspection. Continue independent work, or at the join point call yield_to_subsessions alone as the final action in its tool batch. Completion notifications wake the parent automatically; do not poll list_subsessions, check_subsession, or read_subsession.`;
|
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.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function firstText(content: readonly (TextContent | ImageContent)[]): string {
|
function firstText(content: readonly (TextContent | ImageContent)[]): string {
|
||||||
@@ -67,34 +67,34 @@ describe("createSubsessionToolDefinitions", () => {
|
|||||||
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 after dispatch. Tracked children are join-oriented: continue independent work, then call yield_to_subsessions at the join point. Completion notifications wake the parent automatically; do not poll.");
|
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: delegate join-oriented work; continue independently, then use yield_to_subsessions at the join point");
|
expect(spawnTool.promptSnippet).toBe("spawn_subsession: tracked parallel work; continue, then join with yield_to_subsessions");
|
||||||
|
|
||||||
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 at the join point call yield_to_subsessions rather than polling for completion.");
|
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.");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("distinguishes status inspection from yielding in tool metadata", () => {
|
it("distinguishes status inspection from yielding in tool metadata", () => {
|
||||||
const definitions = tools({});
|
const definitions = tools({});
|
||||||
|
|
||||||
expect(definitions.list.description).toBe("Return tracked child statuses for deliberate inspection. This never yields or changes control flow and is not a completion-polling mechanism.");
|
expect(definitions.list.description).toBe("List tracked child statuses. Never yields or changes control flow; do not poll.");
|
||||||
expect(definitions.list.promptSnippet).toBe("list_subsessions: deliberately inspect tracked child status without yielding");
|
expect(definitions.list.promptSnippet).toBe("list_subsessions: inspect child statuses; never yields");
|
||||||
expect(definitions.check.description).toBe("Return status, message count, and latest output for a non-working tracked child. A working child returns guidance instead of partial output. This never yields or changes control flow.");
|
expect(definitions.check.description).toBe("Get a tracked child's status and latest output. Working output is withheld. Never yields; do not poll.");
|
||||||
expect(definitions.check.promptSnippet).toBe("check_subsession: inspect a non-working child's latest output without yielding");
|
expect(definitions.check.promptSnippet).toBe("check_subsession: inspect child status and available output; never yields");
|
||||||
expect(definitions.read.description).toBe("Return a filtered, paginated transcript for a non-working tracked child. A working child returns guidance instead of partial transcript entries. This never yields or changes control flow.");
|
expect(definitions.read.description).toBe("Read a tracked child's filtered transcript. Working transcripts are withheld. Never yields; do not poll.");
|
||||||
expect(definitions.read.promptSnippet).toBe("read_subsession: inspect a non-working child's transcript without yielding");
|
expect(definitions.read.promptSnippet).toBe("read_subsession: inspect an available child transcript; never yields");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("registers the parameterless yield action with terminal-batch guidance", () => {
|
it("registers the parameterless yield action with terminal-batch guidance", () => {
|
||||||
const { yield: yieldTool } = tools({});
|
const { yield: yieldTool } = tools({});
|
||||||
|
|
||||||
expect(yieldTool.parameters).toMatchObject({ type: "object", properties: {} });
|
expect(yieldTool.parameters).toMatchObject({ type: "object", properties: {} });
|
||||||
expect(yieldTool.description).toBe("End the current agent run at a tracked-subsession join point when any child is still working, allowing completion notifications to wake the parent. If none are working, remain active and report that there is nothing to wait for. Call alone as the final action in its tool batch; 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: at a join point, end this run while tracked children work; call alone as the final tool action");
|
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([
|
||||||
"Use yield_to_subsessions only at a join point after all independent parent work is done; tracked subsessions are join-oriented, while optional fire-and-forget work belongs in spawn_session.",
|
"After independent work, yield only at a join point; use spawn_session for fire-and-forget work.",
|
||||||
"Call yield_to_subsessions alone as the final action in its tool batch. Pi ends the run only when every finalized result in that batch is terminating.",
|
"Call alone and last; a mixed tool batch may continue the run.",
|
||||||
"Do not poll list_subsessions, check_subsession, or read_subsession for completion; completion notifications wake the parent automatically.",
|
"Completion notices wake you; do not poll inspection tools.",
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -151,7 +151,7 @@ describe("createSubsessionToolDefinitions", () => {
|
|||||||
|
|
||||||
expect(list).toHaveBeenCalledWith("parent-1", "/sessions/parent-1.jsonl");
|
expect(list).toHaveBeenCalledWith("parent-1", "/sessions/parent-1.jsonl");
|
||||||
expect(result.details).toEqual({ subsessions });
|
expect(result.details).toEqual({ subsessions });
|
||||||
expect(firstText(result.content)).toBe("Yielding to working tracked subsessions: child-1, child-3. The current agent run is ending; completion notifications will wake the parent as children stop working.");
|
expect(firstText(result.content)).toBe("Working: child-1, child-3. Ending this run; completion notices will wake you.");
|
||||||
expect(result.terminate).toBe(true);
|
expect(result.terminate).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -171,7 +171,7 @@ describe("createSubsessionToolDefinitions", () => {
|
|||||||
const result = await yieldTool.execute("call-no-yield", {}, undefined, undefined, ctxFor("parent-1", undefined));
|
const result = await yieldTool.execute("call-no-yield", {}, undefined, undefined, ctxFor("parent-1", undefined));
|
||||||
|
|
||||||
expect(result.details).toEqual({ subsessions });
|
expect(result.details).toEqual({ subsessions });
|
||||||
expect(firstText(result.content)).toBe("No tracked subsessions are currently working. Nothing was yielded; continue without waiting.");
|
expect(firstText(result.content)).toBe("No tracked subsessions are working; continuing.");
|
||||||
expect(result.terminate).toBeUndefined();
|
expect(result.terminate).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -67,10 +67,10 @@ export interface SubsessionToolDeps {
|
|||||||
|
|
||||||
const SpawnSubsessionParams = Type.Object({
|
const SpawnSubsessionParams = Type.Object({
|
||||||
prompt: Type.String({
|
prompt: Type.String({
|
||||||
description: "The first instruction to send to the new tracked subsession.",
|
description: "Initial instruction for the tracked child.",
|
||||||
}),
|
}),
|
||||||
cwd: Type.Optional(Type.String({
|
cwd: Type.Optional(Type.String({
|
||||||
description: "Working directory for the subsession. Must be a workspace (worktree, or root) of the same project as this session. Defaults to this session's working directory.",
|
description: "Child workspace in the same project (worktree or root); defaults to the parent's directory.",
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -79,39 +79,39 @@ const YieldToSubsessionsParams = Type.Object({});
|
|||||||
|
|
||||||
const CheckSubsessionParams = Type.Object({
|
const CheckSubsessionParams = Type.Object({
|
||||||
sessionId: Type.String({
|
sessionId: Type.String({
|
||||||
description: "Id of a tracked subsession owned by the calling session, as returned by spawn_subsession or list_subsessions.",
|
description: "Tracked child id from spawn_subsession or list_subsessions.",
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const ReadSubsessionParams = Type.Object({
|
const ReadSubsessionParams = Type.Object({
|
||||||
sessionId: Type.String({
|
sessionId: Type.String({
|
||||||
description: "Id of a tracked subsession owned by the calling session, as returned by spawn_subsession or list_subsessions.",
|
description: "Tracked child id from spawn_subsession or list_subsessions.",
|
||||||
}),
|
}),
|
||||||
roles: Type.Optional(Type.Array(
|
roles: Type.Optional(Type.Array(
|
||||||
Type.Union([Type.Literal("assistant"), Type.Literal("user"), Type.Literal("tool"), Type.Literal("system"), Type.Literal("custom")]),
|
Type.Union([Type.Literal("assistant"), Type.Literal("user"), Type.Literal("tool"), Type.Literal("system"), Type.Literal("custom")]),
|
||||||
{ description: "Message roles to include. Omit for all roles." },
|
{ description: "Roles to include; omit for all." },
|
||||||
)),
|
)),
|
||||||
include: Type.Optional(Type.Array(
|
include: Type.Optional(Type.Array(
|
||||||
Type.Union([Type.Literal("text"), Type.Literal("thinking"), Type.Literal("tool_call"), Type.Literal("tool_result"), Type.Literal("image")]),
|
Type.Union([Type.Literal("text"), Type.Literal("thinking"), Type.Literal("tool_call"), Type.Literal("tool_result"), Type.Literal("image")]),
|
||||||
{ description: "Content kinds to keep within messages. Omit for all kinds." },
|
{ description: "Content kinds to include; omit for all." },
|
||||||
)),
|
)),
|
||||||
search: Type.Optional(Type.String({
|
search: Type.Optional(Type.String({
|
||||||
description: "Case-insensitive substring; keep only messages whose text or tool name matches. Always searches full message content, even when maxChars is set.",
|
description: "Case-insensitive text or tool-name substring; searches full content before maxChars truncation.",
|
||||||
})),
|
})),
|
||||||
maxChars: Type.Optional(Type.Integer({
|
maxChars: Type.Optional(Type.Integer({
|
||||||
minimum: 0,
|
minimum: 0,
|
||||||
description: "Truncate each text/thinking/tool-result value to this many characters; clipped parts are marked '[+N chars truncated]'. Omit for full, untruncated text (there is no default, so truncation only happens when you ask for it).",
|
description: "Maximum characters per text, thinking, or tool-result value; omit for no truncation.",
|
||||||
})),
|
})),
|
||||||
includeToolArgs: Type.Optional(Type.Boolean({
|
includeToolArgs: Type.Optional(Type.Boolean({
|
||||||
description: "Include raw tool-call arguments (can be large). A compact one-line summary of each call is always shown regardless.",
|
description: "Include raw tool-call arguments; summaries are always included.",
|
||||||
})),
|
})),
|
||||||
before: Type.Optional(Type.Integer({
|
before: Type.Optional(Type.Integer({
|
||||||
minimum: 0,
|
minimum: 0,
|
||||||
description: "Return only messages before this transcript index; page backward by passing the previous response's 'start'.",
|
description: "Return messages before this index; use the previous start to page backward.",
|
||||||
})),
|
})),
|
||||||
limit: Type.Optional(Type.Integer({
|
limit: Type.Optional(Type.Integer({
|
||||||
minimum: 1,
|
minimum: 1,
|
||||||
description: "Maximum number of most-recent matching messages to return within the window (returned in chronological order). Defaults to 50.",
|
description: "Maximum recent matches, in chronological order. Defaults to 50.",
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -120,7 +120,7 @@ function statusLine(summary: SubsessionSummary): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function workingInspectionGuidance(sessionId: string): string {
|
function workingInspectionGuidance(sessionId: string): string {
|
||||||
return `Subsession ${sessionId} is still working, so partial output is unavailable through agent-facing inspection. Continue independent work, or at the join point call yield_to_subsessions alone as the final action in its tool batch. Completion notifications wake the parent automatically; do not poll list_subsessions, check_subsession, or read_subsession.`;
|
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.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderEntry(entry: TranscriptEntry): string {
|
function renderEntry(entry: TranscriptEntry): string {
|
||||||
@@ -184,8 +184,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 and return after dispatch. Tracked children are join-oriented: continue independent work, then call yield_to_subsessions at the join point. Completion notifications wake the parent automatically; do not poll.",
|
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: delegate join-oriented work; continue independently, then use yield_to_subsessions at the join point",
|
promptSnippet: "spawn_subsession: tracked parallel work; continue, then join with yield_to_subsessions",
|
||||||
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 at the join point call yield_to_subsessions rather than polling for completion.` }],
|
content: [{ type: "text", text: `Started tracked subsession ${result.sessionId} in ${result.cwd}. Continue independent work, then join with yield_to_subsessions; do not poll.` }],
|
||||||
details: result,
|
details: result,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -208,8 +208,8 @@ export function createSubsessionToolDefinitions(spawningCwd: string, deps: Subse
|
|||||||
const listTool = defineTool<typeof ListSubsessionsParams, { subsessions: SubsessionSummary[] }>({
|
const listTool = defineTool<typeof ListSubsessionsParams, { subsessions: SubsessionSummary[] }>({
|
||||||
name: "list_subsessions",
|
name: "list_subsessions",
|
||||||
label: "List subsessions",
|
label: "List subsessions",
|
||||||
description: "Return tracked child statuses for deliberate inspection. This never yields or changes control flow and is not a completion-polling mechanism.",
|
description: "List tracked child statuses. Never yields or changes control flow; do not poll.",
|
||||||
promptSnippet: "list_subsessions: deliberately inspect tracked child status without yielding",
|
promptSnippet: "list_subsessions: inspect child statuses; never yields",
|
||||||
parameters: ListSubsessionsParams,
|
parameters: ListSubsessionsParams,
|
||||||
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
||||||
const parentSessionId = ctx.sessionManager.getSessionId();
|
const parentSessionId = ctx.sessionManager.getSessionId();
|
||||||
@@ -225,8 +225,8 @@ export function createSubsessionToolDefinitions(spawningCwd: string, deps: Subse
|
|||||||
const checkTool = defineTool<typeof CheckSubsessionParams, SubsessionCheckResult>({
|
const checkTool = defineTool<typeof CheckSubsessionParams, SubsessionCheckResult>({
|
||||||
name: "check_subsession",
|
name: "check_subsession",
|
||||||
label: "Check subsession",
|
label: "Check subsession",
|
||||||
description: "Return status, message count, and latest output for a non-working tracked child. A working child returns guidance instead of partial output. This never yields or changes control flow.",
|
description: "Get a tracked child's status and latest output. Working output is withheld. Never yields; do not poll.",
|
||||||
promptSnippet: "check_subsession: inspect a non-working child's latest output without yielding",
|
promptSnippet: "check_subsession: inspect child status and available output; never yields",
|
||||||
parameters: CheckSubsessionParams,
|
parameters: CheckSubsessionParams,
|
||||||
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
||||||
const parentSessionId = ctx.sessionManager.getSessionId();
|
const parentSessionId = ctx.sessionManager.getSessionId();
|
||||||
@@ -246,8 +246,8 @@ export function createSubsessionToolDefinitions(spawningCwd: string, deps: Subse
|
|||||||
const readTool = defineTool<typeof ReadSubsessionParams, SubsessionReadResult>({
|
const readTool = defineTool<typeof ReadSubsessionParams, SubsessionReadResult>({
|
||||||
name: "read_subsession",
|
name: "read_subsession",
|
||||||
label: "Read subsession",
|
label: "Read subsession",
|
||||||
description: "Return a filtered, paginated transcript for a non-working tracked child. A working child returns guidance instead of partial transcript entries. This never yields or changes control flow.",
|
description: "Read a tracked child's filtered transcript. Working transcripts are withheld. Never yields; do not poll.",
|
||||||
promptSnippet: "read_subsession: inspect a non-working child's transcript without yielding",
|
promptSnippet: "read_subsession: inspect an available child transcript; never yields",
|
||||||
parameters: ReadSubsessionParams,
|
parameters: ReadSubsessionParams,
|
||||||
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
||||||
const parentSessionId = ctx.sessionManager.getSessionId();
|
const parentSessionId = ctx.sessionManager.getSessionId();
|
||||||
@@ -267,12 +267,12 @@ export function createSubsessionToolDefinitions(spawningCwd: string, deps: Subse
|
|||||||
const yieldTool = defineTool<typeof YieldToSubsessionsParams, { subsessions: SubsessionSummary[] }>({
|
const yieldTool = defineTool<typeof YieldToSubsessionsParams, { subsessions: SubsessionSummary[] }>({
|
||||||
name: "yield_to_subsessions",
|
name: "yield_to_subsessions",
|
||||||
label: "Yield to subsessions",
|
label: "Yield to subsessions",
|
||||||
description: "End the current agent run at a tracked-subsession join point when any child is still working, allowing completion notifications to wake the parent. If none are working, remain active and report that there is nothing to wait for. Call alone as the final action in its tool batch; 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: at a join point, end this run while tracked children work; call alone as the final tool action",
|
promptSnippet: "yield_to_subsessions: end the run at a join point; call alone and last",
|
||||||
promptGuidelines: [
|
promptGuidelines: [
|
||||||
"Use yield_to_subsessions only at a join point after all independent parent work is done; tracked subsessions are join-oriented, while optional fire-and-forget work belongs in spawn_session.",
|
"After independent work, yield only at a join point; use spawn_session for fire-and-forget work.",
|
||||||
"Call yield_to_subsessions alone as the final action in its tool batch. Pi ends the run only when every finalized result in that batch is terminating.",
|
"Call alone and last; a mixed tool batch may continue the run.",
|
||||||
"Do not poll list_subsessions, check_subsession, or read_subsession for completion; completion notifications wake the parent automatically.",
|
"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) {
|
||||||
@@ -282,12 +282,12 @@ export function createSubsessionToolDefinitions(spawningCwd: string, deps: Subse
|
|||||||
const working = subsessions.filter(({ status }) => status === "working");
|
const working = subsessions.filter(({ status }) => status === "working");
|
||||||
if (working.length === 0) {
|
if (working.length === 0) {
|
||||||
return {
|
return {
|
||||||
content: [{ type: "text", text: "No tracked subsessions are currently working. Nothing was yielded; continue without waiting." }],
|
content: [{ type: "text", text: "No tracked subsessions are working; continuing." }],
|
||||||
details: { subsessions },
|
details: { subsessions },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
content: [{ type: "text", text: `Yielding to working tracked subsessions: ${working.map(({ sessionId }) => sessionId).join(", ")}. The current agent run is ending; completion notifications will wake the parent as children stop working.` }],
|
content: [{ type: "text", text: `Working: ${working.map(({ sessionId }) => sessionId).join(", ")}. Ending this run; completion notices will wake you.` }],
|
||||||
details: { subsessions },
|
details: { subsessions },
|
||||||
terminate: true,
|
terminate: true,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user