fix(coding-agent): avoid Fable split-turn summary refusals (#9908)

Separate conversation content from instructions and replace prefix/suffix wording with continuation-oriented summarization guidance.

Hopefully this avoids the refusals.

Fixes #9652
This commit is contained in:
David Brailovsky 2026-09-22 16:52:07 +02:00 • committed by GitHub
parent 25cc5c7bf4
commit d192bd6dca
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 15 additions and 12 deletions

View file

@ -8,6 +8,7 @@
### Fixed
- Fixed split-turn compaction summaries being refused by Claude Fable 5.1 by clearly separating the conversation and using continuation-oriented instructions ([#9652](https://github.com/earendil-works/pi/issues/9652)).
- Fixed missing or invalid `--mode` values being silently ignored instead of reporting an error and exiting with a nonzero status ([#9045](https://github.com/earendil-works/pi/issues/9045)).
## [0.87.0] - 2026-09-21

View file

@ -961,20 +961,20 @@ export function prepareCompaction(
// Main compaction function
// ============================================================================
const TURN_PREFIX_SUMMARIZATION_PROMPT = `This is the PREFIX of a turn that was too large to keep. The SUFFIX (recent work) is retained.
const TURN_PREFIX_SUMMARIZATION_PROMPT = `The messages above are earlier context from an ongoing conversation. Later messages are stored separately and do not need to be reconstructed.
Summarize the prefix to provide context for the retained suffix:
Create a concise checkpoint of the user's request and the progress shown above. This checkpoint will be placed before the later messages so the conversation can continue with the necessary context.
## Original Request
[What did the user ask for in this turn?]
[What did the user ask for?]
## Early Progress
- [Key decisions and work done in the prefix]
## Progress So Far
- [Key decisions and work completed in these messages]
## Context for Suffix
- [Information needed to understand the retained recent work]
## Context Needed to Continue
- [Information from these messages needed to understand the later work]
Be concise. Focus on what's needed to understand the kept suffix.`;
Only summarize information explicitly present above. Do not infer or recreate later messages.`;
/**
* Generate summaries for compaction using prepared data.
@ -1115,7 +1115,7 @@ async function generateTurnPrefixSummary(
); // Smaller budget for turn prefix
const llmMessages = convertToLlm(messages);
const conversationText = serializeConversation(llmMessages);
const promptText = `<conversation>\n${conversationText}\n</conversation>\n\n${TURN_PREFIX_SUMMARIZATION_PROMPT}`;
const promptText = `# Conversation\n${conversationText}\n\n# Instructions\n${TURN_PREFIX_SUMMARIZATION_PROMPT}`;
const response = await completeSummarization(
model,

View file

@ -146,8 +146,9 @@ describe("generateSummary reasoning options", () => {
expect(result.summary).toContain("previous checkpoint");
const requestContext = completeSimpleMock.mock.calls[0][1] as TranscriptContext;
const prompt = JSON.stringify(requestContext.messages);
expect(prompt).toContain("This is the PREFIX of a turn that was too large to keep");
expect(prompt).toContain("<conversation>");
// Regression test for #9652: clear boundaries and continuation wording avoid the reasoning-extraction false positive.
expect(prompt).toContain("# Conversation\\n[User]: Summarize this.");
expect(prompt).toContain("# Instructions\\nThe messages above are earlier context from an ongoing conversation.");
});
it("rejects tool calls from conversation summaries", async () => {

View file

@ -354,7 +354,8 @@ describe("AgentSession compaction characterization", () => {
expect(transformContext).not.toHaveBeenCalled();
expect(getCurrentSystemPrompt(requestContext?.messages ?? [])).not.toBe(harness.session.agent.state.systemPrompt);
expect(getCurrentTools(requestContext?.messages ?? [])).toEqual([]);
expect(JSON.stringify(requestContext?.messages)).toContain("<conversation>");
// Regression test for #9652: split-turn summaries use a clear Markdown conversation boundary.
expect(JSON.stringify(requestContext?.messages)).toContain("# Conversation\\n[User]: message to compact");
expect(requestOptions).toMatchObject({ cacheRetention: "none" });
expect(requestOptions?.sessionId).not.toBe("active-routing-session");
expect(requestOptions?.transport).toBeUndefined();