Agents: address compaction notice review feedback

- Regenerate config baseline sha256 (pnpm config:docs:gen)
- Add incomplete compaction notice when compaction ends without completing
- Add test for onCompactionEnd callback precedence over notifyUser notice
- Add test for incomplete compaction notice delivery
This commit is contained in:
Sebastian Otaegui
2026-04-16 22:41:50 -03:00
committed by Josh Lehman
parent 176adc30c7
commit f938a02a38
3 changed files with 147 additions and 12 deletions

View File

@@ -1,4 +1,4 @@
747a24d0acf12f95ec75feabb47dad8f03ff0e3a7173b4d277c648f75d956ce5 config-baseline.json
cbb9a6ee1cb69068d5eb63f00f95512ba19778415ea5b2eabe056aaea38978b5 config-baseline.core.json
e239cc20f20f8d0172812bc0ad3ee6df52da88e2e2702e3d03a47e01561132ae config-baseline.channel.json
b695cb31b4c0cf1d31f842f2892e99cc3ff8d84263ae72b72977cae844b81d6e config-baseline.plugin.json
12639f02b5283d35fb01136164f68301c22b8ac6786e69662142ec484924bdd8 config-baseline.json
ada810ee23e23ad739f1b652891adffb34988d455638d5e4ea3cb8ac15197304 config-baseline.core.json
99bb34fcf83ba6bb50a3fc11f170bd379bee5728b0938707fc39ebd7638e12eb config-baseline.channel.json
5f5d4e850df6e9854a85b5d008236854ce185c707fdbb566efcf00f8c08b36e3 config-baseline.plugin.json

View File

@@ -931,6 +931,131 @@ describe("runAgentTurnWithFallback", () => {
);
});
it("prefers onCompactionEnd callback over default notice when notifyUser is enabled", async () => {
const onBlockReply = vi.fn();
const onCompactionEnd = vi.fn();
state.runEmbeddedPiAgentMock.mockImplementationOnce(async (params: EmbeddedAgentParams) => {
await params.onAgentEvent?.({ stream: "compaction", data: { phase: "start" } });
await params.onAgentEvent?.({
stream: "compaction",
data: { phase: "end", completed: true },
});
return { payloads: [{ text: "final" }], meta: {} };
});
const followupRun = createFollowupRun();
followupRun.run.config = {
agents: {
defaults: {
compaction: {
notifyUser: true,
},
},
},
};
const runAgentTurnWithFallback = await getRunAgentTurnWithFallback();
const result = await runAgentTurnWithFallback({
commandBody: "hello",
followupRun,
sessionCtx: {
Provider: "whatsapp",
MessageSid: "msg",
} as unknown as TemplateContext,
opts: { onBlockReply, onCompactionEnd },
typingSignals: createMockTypingSignaler(),
blockReplyPipeline: null,
blockStreamingEnabled: false,
resolvedBlockStreamingBreak: "message_end",
applyReplyToMode: (payload) => payload,
shouldEmitToolResult: () => true,
shouldEmitToolOutput: () => false,
pendingToolTasks: new Set(),
resetSessionAfterCompactionFailure: async () => false,
resetSessionAfterRoleOrderingConflict: async () => false,
isHeartbeat: false,
sessionKey: "main",
getActiveSessionEntry: () => undefined,
resolvedVerboseLevel: "off",
});
expect(result.kind).toBe("success");
expect(onCompactionEnd).toHaveBeenCalledTimes(1);
// The start notice still fires (no onCompactionStart callback provided),
// but the completion notice is suppressed in favor of the callback.
expect(onBlockReply).toHaveBeenCalledTimes(1);
expect(onBlockReply).toHaveBeenCalledWith(
expect.objectContaining({
text: "🧹 Compacting context...",
isCompactionNotice: true,
}),
);
});
it("emits an incomplete compaction notice when compaction ends without completing", async () => {
const onBlockReply = vi.fn();
state.runEmbeddedPiAgentMock.mockImplementationOnce(async (params: EmbeddedAgentParams) => {
await params.onAgentEvent?.({ stream: "compaction", data: { phase: "start" } });
await params.onAgentEvent?.({
stream: "compaction",
data: { phase: "end", completed: false },
});
return { payloads: [{ text: "final" }], meta: {} };
});
const followupRun = createFollowupRun();
followupRun.run.config = {
agents: {
defaults: {
compaction: {
notifyUser: true,
},
},
},
};
const runAgentTurnWithFallback = await getRunAgentTurnWithFallback();
const result = await runAgentTurnWithFallback({
commandBody: "hello",
followupRun,
sessionCtx: {
Provider: "whatsapp",
MessageSid: "msg",
} as unknown as TemplateContext,
opts: { onBlockReply },
typingSignals: createMockTypingSignaler(),
blockReplyPipeline: null,
blockStreamingEnabled: false,
resolvedBlockStreamingBreak: "message_end",
applyReplyToMode: (payload) => payload,
shouldEmitToolResult: () => true,
shouldEmitToolOutput: () => false,
pendingToolTasks: new Set(),
resetSessionAfterCompactionFailure: async () => false,
resetSessionAfterRoleOrderingConflict: async () => false,
isHeartbeat: false,
sessionKey: "main",
getActiveSessionEntry: () => undefined,
resolvedVerboseLevel: "off",
});
expect(result.kind).toBe("success");
expect(onBlockReply).toHaveBeenNthCalledWith(
1,
expect.objectContaining({
text: "🧹 Compacting context...",
isCompactionNotice: true,
}),
);
expect(onBlockReply).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
text: "🧹 Compaction incomplete",
isCompactionNotice: true,
}),
);
});
it("does not show a rate-limit countdown for mixed-cause fallback exhaustion", async () => {
state.runWithModelFallbackMock.mockRejectedValueOnce(
Object.assign(

View File

@@ -626,12 +626,18 @@ export async function runAgentTurnWithFallback(params: {
const currentMessageId = params.sessionCtx.MessageSidFull ?? params.sessionCtx.MessageSid;
const shouldNotifyUserAboutCompaction =
runtimeConfig?.agents?.defaults?.compaction?.notifyUser === true;
const sendCompactionNotice = async (phase: "start" | "end") => {
const sendCompactionNotice = async (phase: "start" | "end" | "incomplete") => {
if (!params.opts?.onBlockReply) {
return;
}
const text =
phase === "start"
? "🧹 Compacting context..."
: phase === "end"
? "🧹 Compaction complete"
: "🧹 Compaction incomplete";
const noticePayload = params.applyReplyToMode({
text: phase === "start" ? "🧹 Compacting context..." : "🧹 Compaction complete",
text,
replyToId: currentMessageId,
replyToCurrent: true,
isCompactionNotice: true,
@@ -1172,13 +1178,17 @@ export async function runAgentTurnWithFallback(params: {
await sendCompactionNotice("start");
}
}
const completed = evt.data?.completed === true;
if (phase === "end" && completed) {
attemptCompactionCount += 1;
if (params.opts?.onCompactionEnd) {
await params.opts.onCompactionEnd();
if (phase === "end") {
const completed = evt.data?.completed === true;
if (completed) {
attemptCompactionCount += 1;
if (params.opts?.onCompactionEnd) {
await params.opts.onCompactionEnd();
} else if (shouldNotifyUserAboutCompaction) {
await sendCompactionNotice("end");
}
} else if (shouldNotifyUserAboutCompaction) {
await sendCompactionNotice("end");
await sendCompactionNotice("incomplete");
}
}
}