Archived
fix: delimit subsession output from guidance
This commit is contained in:
@@ -2,4 +2,4 @@
|
||||
"@jmfederico/pi-web": patch
|
||||
---
|
||||
|
||||
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.
|
||||
Add explicit tracked-subsession yielding with no-poll wake-up guidance, remaining-child status, and clear boundaries around child output.
|
||||
|
||||
+2
-1
@@ -552,7 +552,8 @@
|
||||
or change control flow. They are for deliberate inspection or recovery, not completion polling. While a
|
||||
child works, agent-facing <code>check_subsession</code> and <code>read_subsession</code> withhold partial
|
||||
output and direct the parent to continue independent work or yield at the join point. Output becomes
|
||||
available when the child stops.
|
||||
available when the child stops. In notices and inspection results, PI WEB guidance precedes a labeled
|
||||
marker and the child output or transcript always comes last.
|
||||
</p>
|
||||
<p>
|
||||
In <strong>Settings → Session daemon</strong>, these keys are saved on the selected machine. Restart the
|
||||
|
||||
+1
-1
@@ -189,7 +189,7 @@ At a join point, after finishing its independent work, the parent calls `yield_t
|
||||
|
||||
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. 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.
|
||||
`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 notices and inspection results, PI WEB guidance precedes a labeled marker and the child output or transcript always comes last.
|
||||
|
||||
In **Settings → Session daemon**, these keys are saved on the selected machine. Restart the session daemon on that machine after changing them.
|
||||
|
||||
|
||||
@@ -804,7 +804,7 @@ describe("PiSessionService", () => {
|
||||
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.",
|
||||
"Subsession child-1 stopped working (idle).\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.\n\n--- SUBSESSION OUTPUT: child-1 ---\n(no output)",
|
||||
);
|
||||
|
||||
second.session.isStreaming = false;
|
||||
@@ -812,7 +812,7 @@ describe("PiSessionService", () => {
|
||||
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.",
|
||||
"Subsession child-2 stopped working (idle).\nNo other tracked subsessions are working.\n\n--- SUBSESSION OUTPUT: child-2 ---\n(no output)",
|
||||
);
|
||||
await service.dispose();
|
||||
});
|
||||
|
||||
@@ -931,7 +931,7 @@ export class PiSessionService {
|
||||
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}`;
|
||||
const text = `Subsession ${childId} stopped working (${status}).\n${next}\n\n--- SUBSESSION OUTPUT: ${childId} ---\n${preview}`;
|
||||
void this.notifyParentOfSubsession(link.parentSessionId, childId, text);
|
||||
}
|
||||
|
||||
|
||||
@@ -183,7 +183,7 @@ describe("createSubsessionToolDefinitions", () => {
|
||||
|
||||
expect(check).toHaveBeenCalledWith("parent-1", "child-1", "/sessions/parent-1.jsonl");
|
||||
expect(result.details).toMatchObject({ sessionId: "child-1", status: "idle", finalText: "all done" });
|
||||
expect(firstText(result.content)).toContain("all done");
|
||||
expect(firstText(result.content)).toBe("Subsession child-1 [idle].\n\n--- SUBSESSION OUTPUT: child-1 ---\nall done");
|
||||
expect(result.terminate).toBeUndefined();
|
||||
});
|
||||
|
||||
@@ -208,7 +208,7 @@ describe("createSubsessionToolDefinitions", () => {
|
||||
|
||||
const result = await checkTool.execute("call-error-check", { sessionId: "child-1" }, undefined, undefined, ctxFor("parent-1", undefined));
|
||||
|
||||
expect(firstText(result.content)).toBe("Subsession child-1 [error]:\n\nchild failed");
|
||||
expect(firstText(result.content)).toBe("Subsession child-1 [error].\n\n--- SUBSESSION OUTPUT: child-1 ---\nchild failed");
|
||||
expect(result.terminate).toBeUndefined();
|
||||
});
|
||||
|
||||
@@ -224,15 +224,15 @@ describe("createSubsessionToolDefinitions", () => {
|
||||
const read = vi.fn(() => Promise.resolve({
|
||||
sessionId: "child-1", cwd: "/repos/a", status: "idle" as const,
|
||||
entries: [{ index: 2, role: "assistant" as const, parts: [{ kind: "text" as const, text: "the answer" }] }],
|
||||
total: 5, matched: 1, start: 2, hasMore: false,
|
||||
total: 5, matched: 2, start: 2, hasMore: true,
|
||||
}));
|
||||
const { read: readTool } = tools({ read });
|
||||
|
||||
const result = await readTool.execute("call-6", { sessionId: "child-1", roles: ["assistant"], maxChars: 200 }, undefined, undefined, ctxFor("parent-1", "/sessions/parent-1.jsonl"));
|
||||
const result = await readTool.execute("call-6", { sessionId: "child-1", roles: ["assistant"], maxChars: 200, limit: 1 }, undefined, undefined, ctxFor("parent-1", "/sessions/parent-1.jsonl"));
|
||||
|
||||
expect(read).toHaveBeenCalledWith("parent-1", "child-1", { roles: ["assistant"], maxChars: 200 }, "/sessions/parent-1.jsonl");
|
||||
expect(result.details).toMatchObject({ sessionId: "child-1", matched: 1 });
|
||||
expect(firstText(result.content)).toContain("the answer");
|
||||
expect(read).toHaveBeenCalledWith("parent-1", "child-1", { roles: ["assistant"], maxChars: 200, limit: 1 }, "/sessions/parent-1.jsonl");
|
||||
expect(result.details).toMatchObject({ sessionId: "child-1", matched: 2 });
|
||||
expect(firstText(result.content)).toBe("Subsession child-1 [idle] — messages 2–2 of 5 (2 matched). Earlier matching messages exist before index 2.\n\n--- SUBSESSION TRANSCRIPT: child-1 ---\n#2 assistant\nthe answer");
|
||||
expect(result.terminate).toBeUndefined();
|
||||
});
|
||||
|
||||
|
||||
@@ -158,7 +158,7 @@ function renderTranscript(result: SubsessionReadResult): string {
|
||||
? "no messages matched your filters"
|
||||
: `no messages in this window (${String(result.matched)} matched outside it)`)
|
||||
: `messages ${String(result.start)}–${String(last.index)} of ${String(result.total)} (${String(result.matched)} matched)`;
|
||||
const more = result.hasMore ? `\n\nEarlier matching messages exist before index ${String(result.start)}.` : "";
|
||||
const more = result.hasMore ? ` Earlier matching messages exist before index ${String(result.start)}.` : "";
|
||||
// Empty entries with matches means the `before` cursor excluded every match
|
||||
// (they all sit at index >= before): the agent paged too far back and should
|
||||
// raise `before` or omit it, not page back further.
|
||||
@@ -167,7 +167,7 @@ function renderTranscript(result: SubsessionReadResult): string {
|
||||
: (result.matched === 0
|
||||
? "(no messages matched the filters)"
|
||||
: `(no messages before index ${String(result.start)}; all ${String(result.matched)} matches have later indexes)`);
|
||||
return `Subsession ${result.sessionId} [${result.status}] — ${range}:\n\n${body}${more}`;
|
||||
return `Subsession ${result.sessionId} [${result.status}] — ${range}.${more}\n\n--- SUBSESSION TRANSCRIPT: ${result.sessionId} ---\n${body}`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -235,7 +235,7 @@ export function createSubsessionToolDefinitions(spawningCwd: string, deps: Subse
|
||||
const body = result.finalText === "" ? "(no output yet)" : result.finalText;
|
||||
const text = result.status === "working"
|
||||
? workingInspectionGuidance(result.sessionId)
|
||||
: `Subsession ${result.sessionId} [${result.status}]:\n\n${body}`;
|
||||
: `Subsession ${result.sessionId} [${result.status}].\n\n--- SUBSESSION OUTPUT: ${result.sessionId} ---\n${body}`;
|
||||
return {
|
||||
content: [{ type: "text", text }],
|
||||
details: result,
|
||||
|
||||
Reference in New Issue
Block a user