feat(summarize): a configurable head, and an operator prompt - #252
Open
itay-nakash wants to merge 1 commit into
Open
itay-nakash wants to merge 1 commit into
itay-nakash wants to merge 1 commit into
Conversation
THE HEAD WAS A HARDCODED 1, AND "PRESERVE msgs[0]" MEANS TWO DIFFERENT THINGS. summarizeSpan pinned exactly one leading message. On Anthropic traffic that is right: the system prompt is a top-level `system` field the pipeline never sees, so msgs[0] IS the opening user turn and the task is pinned. On OpenAI-shaped traffic msgs[0] is the system prompt and the task statement is msgs[1] — the FIRST message of the summarized span. So the instruction the agent is working from was folded into the summary on every eligible turn, and no config could stop it. It was survivable rather than obvious because conversationGoal scans by ROLE, not by index: the task text still reaches the summarizer as the goal, so the summary is written toward a task whose verbatim statement is gone. The reporter saw it as "the summarizer compacts the starting messages". `keep_first` (default 1) makes the head reachable, so an OpenAI-shaped deployment sets 2 and retains [system, task]. The default is unchanged deliberately: moving it would change the output of every existing deployment. THE HEAD RULE GENERALISES RATHER THAN MOVES. The old code dropped headCount to 0 when msgs[0] was an assistant message carrying tool calls, because its results lie inside the span and keeping it emits an unanswered call. That is now a retreat loop over the head's LAST message, which reduces to exactly the old rule at keep_first: 1 and keeps a call/result pair whole at keep_first: 2. A loop rather than one decrement: consecutive tool-calling assistant messages are malformed input, and malformed input is where a provider 400 comes from. `custom_prompt` is the second knob, and the frame around it is the point. A bare instruction pasted after a long trajectory reads to the model as more trajectory and gets summarized instead of followed, so it is wrapped in text that says whose instruction it is and what precedence it has. Two limits survive it: the ban on inventing anything absent from the conversation — the base prompt's entire purpose — and the <summary> output format. `summarizer1509` is a PRESET, not a component: summarize configured as the cache-unaware, no-reuse reference summarizer (cache_state any, resummarize_tokens 0, keep_first 2), which is the control arm the cache-aware trigger's payoff is measured against, and the right starting point on a backend whose prompt-cache lifetime this repo cannot derive. It is a preset because turn-aligned cuts, orphaned tool_result repair and now a configurable head all live in summarize already — a separate component would duplicate ~500 lines to deliver three YAML values. Tests drive the real two-turn path (the summary is commissioned off the hot path, so turn 1 forwards untouched and turn 2 splices) and carry a vacuity guard, because a fixture that stops reaching the span makes every head assertion pass for the wrong reason. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Itay-Nakash <itay.nakash@ibm.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The defect
summarizeSpanpinned a hardcodedheadCount = 1, and "preservemsgs[0]" means two different things across the dialects — only one of them keeps the task:msgs[0]issystemprompt is a top-level field the pipeline never sees)msgs[1]— the first message of the summarized span ⛔So on OpenAI-shaped traffic the instruction the agent is working from was folded into the summary on every eligible turn, and no config could stop it.
It was survivable rather than obvious because
conversationGoalscans by role, not by index: the task text still reaches the summarizer as the goal, so the summary is written toward a task whose verbatim statement is gone. It was reported to me as "the summarizer compacts the starting messages".The change
keep_first(default 1, unchanged behaviour) makes the head reachable, so an OpenAI-shaped deployment sets2and retains[system, task]. A negative value is refused rather than clamped — reading a typo as 0 would summarize the task away, the one thing the head exists to pin.The head rule generalises rather than moves. The old code dropped
headCountto 0 whenmsgs[0]was an assistant message carrying tool calls, since its results lie inside the span and keeping it emits an unanswered call. That is now a retreat loop over the head's last message: it reduces to exactly the old rule atkeep_first: 1, and keeps a call/result pair whole atkeep_first: 2. A loop rather than one decrement, because consecutive tool-calling assistant messages are malformed input and malformed input is where a provider 400 comes from.custom_prompt, and the frame around it is the point: a bare instruction pasted after a long trajectory reads to the model as more trajectory and gets summarized instead of followed, so it is wrapped in text stating whose instruction it is and what precedence it has. Two limits survive it — the ban on inventing anything absent from the conversation (the base prompt's entire purpose) and the<summary>output format.summarizer1509is a preset, not a component:summarizeconfigured as the cache-unaware, no-reuse reference summarizer (cache_state: any,resummarize_tokens: 0,keep_first: 2) — the control arm the cache-aware trigger's payoff is measured against, and the right starting point on a backend whose prompt-cache lifetime this repo cannot derive. A preset because turn-aligned cuts, orphanedtool_resultrepair and now a configurable head all live insummarizealready; a separate component would duplicate ~500 lines of summarizer to deliver three YAML values.Tests
summarize_keepfirst_test.godrives the real two-turn path — the summary is commissioned off the hot path, so turn 1 forwards untouched and turn 2 splices — draining onWaitForSummaryForTestrather than sleeping. It carries a vacuity guard, because a fixture that stops reaching the span makes every head assertion pass for the wrong reason.Four cases: the default reproduces the historical shape (task dropped);
keep_first: 2retains it; the dangling-tool-call retreat; andkeep_first: 0honoured vs negative refused. Thekeep_first: 2case also re-asserts the no-system-role-away-from-index-0 contract, since a larger head is a new way to violate it.go vetandgofmtclean;./components/...and./config/...pass. Two pre-existing failures incmd/context-guru-proxyare environmental on my machine (install.shneeds bash > 3.2, and one test finds a binary already installed at~/.local/bin) — neither package is touched by this diff.One note, not a change
TestSummarizeEmitsNoSystemRoleAwayFromTheHeadcurrently skips on its own fixture (summarize declined on this fixture) — themin_request_frac0.9 default plusFracResolvablemeans it never reaches the acting path, so the guard against the400 messages.1: role 'system' must precede...regression is not actually exercised. My new test pinsmin_request_frac: 0/cache_state: anyto get there. Worth a follow-up on that fixture; I left it alone to keep this diff to one subject.🤖 Generated with Claude Code