refactor: share subagent followup reply helpers

This commit is contained in:
Peter Steinberger
2026-03-13 21:25:17 +00:00
parent d3f46fa7fa
commit 7fd21b6bc6
2 changed files with 46 additions and 81 deletions

View File

@@ -33,6 +33,29 @@ async function resolveAfterAdvancingTimers<T>(promise: Promise<T>, advanceMs = 1
return promise;
}
function createDescendantRun(params?: {
runId?: string;
childSessionKey?: string;
task?: string;
cleanup?: "keep" | "delete";
endedAt?: number;
frozenResultText?: string | null;
}) {
return {
runId: params?.runId ?? "run-1",
childSessionKey: params?.childSessionKey ?? "child-1",
requesterSessionKey: "test-session",
requesterDisplayKey: "test-session",
task: params?.task ?? "task-1",
cleanup: params?.cleanup ?? "keep",
createdAt: 1000,
endedAt: params?.endedAt ?? 2000,
...(params?.frozenResultText === undefined
? {}
: { frozenResultText: params.frozenResultText }),
};
}
describe("isLikelyInterimCronMessage", () => {
it("detects 'on it' as interim", () => {
expect(isLikelyInterimCronMessage("on it")).toBe(true);
@@ -85,18 +108,7 @@ describe("readDescendantSubagentFallbackReply", () => {
});
it("reads reply from child session transcript", async () => {
vi.mocked(listDescendantRunsForRequester).mockReturnValue([
{
runId: "run-1",
childSessionKey: "child-1",
requesterSessionKey: "test-session",
requesterDisplayKey: "test-session",
task: "task-1",
cleanup: "keep",
createdAt: 1000,
endedAt: 2000,
},
]);
vi.mocked(listDescendantRunsForRequester).mockReturnValue([createDescendantRun()]);
vi.mocked(readLatestAssistantReply).mockResolvedValue("child output text");
const result = await readDescendantSubagentFallbackReply({
sessionKey: "test-session",
@@ -107,17 +119,10 @@ describe("readDescendantSubagentFallbackReply", () => {
it("falls back to frozenResultText when session transcript unavailable", async () => {
vi.mocked(listDescendantRunsForRequester).mockReturnValue([
{
runId: "run-1",
childSessionKey: "child-1",
requesterSessionKey: "test-session",
requesterDisplayKey: "test-session",
task: "task-1",
createDescendantRun({
cleanup: "delete",
createdAt: 1000,
endedAt: 2000,
frozenResultText: "frozen child output",
},
}),
]);
vi.mocked(readLatestAssistantReply).mockResolvedValue(undefined);
const result = await readDescendantSubagentFallbackReply({
@@ -129,17 +134,7 @@ describe("readDescendantSubagentFallbackReply", () => {
it("prefers session transcript over frozenResultText", async () => {
vi.mocked(listDescendantRunsForRequester).mockReturnValue([
{
runId: "run-1",
childSessionKey: "child-1",
requesterSessionKey: "test-session",
requesterDisplayKey: "test-session",
task: "task-1",
cleanup: "keep",
createdAt: 1000,
endedAt: 2000,
frozenResultText: "frozen text",
},
createDescendantRun({ frozenResultText: "frozen text" }),
]);
vi.mocked(readLatestAssistantReply).mockResolvedValue("live transcript text");
const result = await readDescendantSubagentFallbackReply({
@@ -151,28 +146,14 @@ describe("readDescendantSubagentFallbackReply", () => {
it("joins replies from multiple descendants", async () => {
vi.mocked(listDescendantRunsForRequester).mockReturnValue([
{
runId: "run-1",
childSessionKey: "child-1",
requesterSessionKey: "test-session",
requesterDisplayKey: "test-session",
task: "task-1",
cleanup: "keep",
createdAt: 1000,
endedAt: 2000,
frozenResultText: "first child output",
},
{
createDescendantRun({ frozenResultText: "first child output" }),
createDescendantRun({
runId: "run-2",
childSessionKey: "child-2",
requesterSessionKey: "test-session",
requesterDisplayKey: "test-session",
task: "task-2",
cleanup: "keep",
createdAt: 1000,
endedAt: 3000,
frozenResultText: "second child output",
},
}),
]);
vi.mocked(readLatestAssistantReply).mockResolvedValue(undefined);
const result = await readDescendantSubagentFallbackReply({
@@ -184,27 +165,14 @@ describe("readDescendantSubagentFallbackReply", () => {
it("skips SILENT_REPLY_TOKEN descendants", async () => {
vi.mocked(listDescendantRunsForRequester).mockReturnValue([
{
runId: "run-1",
childSessionKey: "child-1",
requesterSessionKey: "test-session",
requesterDisplayKey: "test-session",
task: "task-1",
cleanup: "keep",
createdAt: 1000,
endedAt: 2000,
},
{
createDescendantRun(),
createDescendantRun({
runId: "run-2",
childSessionKey: "child-2",
requesterSessionKey: "test-session",
requesterDisplayKey: "test-session",
task: "task-2",
cleanup: "keep",
createdAt: 1000,
endedAt: 3000,
frozenResultText: "useful output",
},
}),
]);
vi.mocked(readLatestAssistantReply).mockImplementation(async (params) => {
if (params.sessionKey === "child-1") {
@@ -221,17 +189,10 @@ describe("readDescendantSubagentFallbackReply", () => {
it("returns undefined when frozenResultText is null", async () => {
vi.mocked(listDescendantRunsForRequester).mockReturnValue([
{
runId: "run-1",
childSessionKey: "child-1",
requesterSessionKey: "test-session",
requesterDisplayKey: "test-session",
task: "task-1",
createDescendantRun({
cleanup: "delete",
createdAt: 1000,
endedAt: 2000,
frozenResultText: null,
},
}),
]);
vi.mocked(readLatestAssistantReply).mockResolvedValue(undefined);
const result = await readDescendantSubagentFallbackReply({

View File

@@ -169,7 +169,7 @@ export async function waitForDescendantSubagentSummary(params: {
// CRON_SUBAGENT_FINAL_REPLY_GRACE_MS) to capture that synthesis.
const gracePeriodDeadline = Math.min(Date.now() + CRON_SUBAGENT_FINAL_REPLY_GRACE_MS, deadline);
while (Date.now() < gracePeriodDeadline) {
const resolveUsableLatestReply = async () => {
const latest = (await readLatestAssistantReply({ sessionKey: params.sessionKey }))?.trim();
if (
latest &&
@@ -178,16 +178,20 @@ export async function waitForDescendantSubagentSummary(params: {
) {
return latest;
}
return undefined;
};
while (Date.now() < gracePeriodDeadline) {
const latest = await resolveUsableLatestReply();
if (latest) {
return latest;
}
await new Promise<void>((resolve) => setTimeout(resolve, CRON_SUBAGENT_GRACE_POLL_MS));
}
// Final read after grace period expires.
const latest = (await readLatestAssistantReply({ sessionKey: params.sessionKey }))?.trim();
if (
latest &&
latest.toUpperCase() !== SILENT_REPLY_TOKEN.toUpperCase() &&
(latest !== initialReply || !isLikelyInterimCronMessage(latest))
) {
const latest = await resolveUsableLatestReply();
if (latest) {
return latest;
}