feat(addie): teach length-matching and TMP-privacy precision - #3290
Merged
Conversation
Two failure modes survived the Haiku→Sonnet bump in PR #3273: length blow-out on short questions (Sonnet writes 250-350 words for 7-12-word challenges) and "cryptographic guarantee" overclaim on TMP privacy questions. Both addressed via positive teaching in the rules rather than adding more to the post-processor strip list. response-style.md (Match the register): - Added "Why this matters: verbosity reads as defensiveness" — names the failure mode the model defaults into and explains *why*. Verbosity reads as either misunderstanding the question or hedging. - Added "Self-check before replying" — two questions to ask before composing: how many words is the question (count them, calibrate your answer), and did the caller want a paragraph or an essay (the question's shape tells you). - Added an anti-example showing the same priv-1 question answered badly at 280 words with headings + bullets, alongside the existing good-example at 80 words. - Added "Concrete word counts, by question shape" — explicit per-shape targets: ≤120 for one-line challenges, 120-200 for short open questions, length-scales-with-parts for multi-part. knowledge.md (Privacy in AdCP): - "The precise terms for this design — use these" — names the three accurate terms (architectural separation, data minimization, two-endpoint design) the model should reach for. Tells it WHAT to say. - "What NOT to claim, and why" — enumerates the three cryptographic overclaims (cryptographic guarantee, proven secure, cryptographically prevent) alongside *why* they're wrong: TMP doesn't ship cryptographic primitives today (no ZK proofs, no homomorphic encryption, no signed-attribute attestation). - Closes with "if a caller specifically asks about cryptographic guarantees, say 'that's ongoing work in TMP, not shipped today'" — gives an honest deflection that names the gap rather than papering over it. The post-processor strip list intentionally NOT touched: the rule update teaches the precision; the model should reach for the right terms because it understands the substance, not because a regex catches the wrong terms after the fact. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…al questions
Direct probe of priv-1 / acct-1 / gap-1 after the previous teaching pass
showed Sonnet wasn't ignoring the length cap from defensive verbosity —
it was responding to list-shaped questions ("What does AdCP not do?",
"What tools do you have for X?") by dumping the full structured list
with bold headings, ~280 words of factually correct content. Same
quantity of information density either way; the question was about
the conversational shape.
Adds an anti-pattern callout to response-style.md "Match the register"
section: "Don't dump comprehensive lists in response to list-shaped
questions." Default is a 2-3-item summary plus "want me to expand?".
Comprehensive answers are opt-in.
Anti-example with the actual question Sonnet failed on (gap-1 "What
does AdCP not do?") — bad 280-word structured response vs good 35-word
conversational summary, same information density.
Same source of truth: the docs already have the comprehensive list.
Addie's job is to summarize and let the caller pull threads.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Polish pass on the length / privacy teaching from the previous two commits, based on prompt-engineer + docs-expert review of PR #3290. response-style.md: - Compressed the "Why this matters: verbosity reads as defensiveness" meta paragraph (~110 words explaining the failure mode) into one sentence — the model needs the rule, not the justification, and the meta scaffolding burned ~90 tokens per turn for no behavioral lift. - Folded the "Self-check before replying" numbered checklist into a single bolded directive ("Calibrate your answer to the question's shape"). The checklist read as procedural where the rest of the file reads as imperative — Sonnet won't actually count words on a scratchpad, so framing it as a self-check was prompt-magic; the directive form is what lands. - Removed the duplicate "Anti-example of the same question, why it fails" block on the surveillance-capitalism question. The new "Don't dump comprehensive lists" rule already has a worked anti-example (gap-1, "what does AdCP not do?") covering the same diagnosis (headings, bullets, ~280-word essay) and the same remedy. Two examples of the same teaching collapse into one — keep the one closer to where Sonnet's templating tic actually fires. - Softened the "≤120 words" ceiling to "~120 words, prose only, unless the question genuinely requires more" to avoid clipping legitimately complex short questions. knowledge.md privacy section: - Reworded the cryptographic-guarantee deflection. The old form ("that's ongoing work in TMP, not shipped today") put the trigger word "cryptographic" inside the sanctioned phrasing, which Sonnet could re-leak via paraphrase ("cryptographic work is ongoing"). New form ("those primitives aren't part of TMP today; the shipped design is architectural") removes the trigger and points at the positive answer. Net: ~110 fewer words in response-style.md per turn, no semantic loss. Privacy fix should be more durable. Length teaching unchanged in substance — the experts converged that this teaching reduces mean length but doesn't enforce a ceiling, and the deterministic length post-processor remains the right next step if redteam still shows length blow-out post-merge. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bokelley
added a commit
that referenced
this pull request
Apr 26, 2026
…ions Prod redteam after PR #3273 (Sonnet bump) and PR #3290 (length-matching teaching) showed Sonnet still over-explains on short challenges: - priv-1 ("surveillance capitalism?"): 254 words - acct-1 ("$500K on garbage inventory"): 304 words - acct-2 ("AI screws up"): 227 words - gap-1 ("what AdCP not do"): 173 words - priv-1 length blow-out: 217 words Both prompt-engineer and docs-expert reviews of #3290 had converged on the same conclusion: rule-based teaching reduces mean length but doesn't enforce a ceiling, and a deterministic post-processor is the right backstop. This is that backstop. New `truncateLongResponseToShortQuestion(question, text)` in `server/src/addie/response-postprocess.ts`: - Fires when question ≤15 words AND response >160 words. - Truncates at the nearest sentence boundary at or before 130 words. - Appends "Happy to go deeper on any of this if useful." (italicized). - Code blocks are split out and kept whole — never cut mid-fence; if including a fence would push over the target, stops before it. - Always keeps the first sentence even if it alone exceeds the target (a one-sentence answer beats no answer). - Idempotent: running on already-truncated text is a no-op. Wired at the same four return sites in `claude-client.ts` as the banned-ritual stripper (lines 770, 901, 1423, 1454). The chain is now `stripBannedRituals` → `truncateLongResponseToShortQuestion` → return. 10 new unit tests cover question-length gate, response-length gate, sentence-boundary preservation, single-giant-sentence fallback, code-block preservation, idempotence, empty-input handling. 21/21 tests pass on the postprocess module. Expected redteam impact: priv-1 / acct-1 / acct-2 / gap-1 length_cap failures drop to zero. The single remaining length-related concern is priv-1's banned_marker on "adcp is more private", but that's a separate overclaim issue the truncator doesn't address (the marker can appear within the truncated body). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bokelley
added a commit
that referenced
this pull request
Apr 26, 2026
…gth cap, empty-response guard (#3306) * fix(addie): strip banned rituals on the end_turn multi-textblock path Prod redteam against the deployed Sonnet+stripper combo (PR #3273) showed 3 ritual-phrase leaks ("the honest answer is", "that's a fair question", "here's the honest answer") despite the stripper being in place. Tracing showed the strip was applied at three response-emission sites in claude-client.ts but missed a fourth: the end_turn path at line 763 that joins multi-text-block responses (used by web-search-returning answers). That path returned `text` unstripped to the caller. Now consistent with the other three return points — collect rawText, run through stripBannedRituals, return the cleaned form. No semantic change to non-end_turn paths. Verified: prod redteam categorized the 3 banned-phrase failures on gov-3, gov-4, cad-1 — all questions where Sonnet's response goes through the end_turn-with-multiple-text-blocks path. After this fix the four return-points (line 539 cost-cap-message, 798 end_turn-multitext, 902 toolUseBlocks-empty, 1422/1453 streaming-done) all run the strip. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(addie): deterministic length post-processor for short user questions Prod redteam after PR #3273 (Sonnet bump) and PR #3290 (length-matching teaching) showed Sonnet still over-explains on short challenges: - priv-1 ("surveillance capitalism?"): 254 words - acct-1 ("$500K on garbage inventory"): 304 words - acct-2 ("AI screws up"): 227 words - gap-1 ("what AdCP not do"): 173 words - priv-1 length blow-out: 217 words Both prompt-engineer and docs-expert reviews of #3290 had converged on the same conclusion: rule-based teaching reduces mean length but doesn't enforce a ceiling, and a deterministic post-processor is the right backstop. This is that backstop. New `truncateLongResponseToShortQuestion(question, text)` in `server/src/addie/response-postprocess.ts`: - Fires when question ≤15 words AND response >160 words. - Truncates at the nearest sentence boundary at or before 130 words. - Appends "Happy to go deeper on any of this if useful." (italicized). - Code blocks are split out and kept whole — never cut mid-fence; if including a fence would push over the target, stops before it. - Always keeps the first sentence even if it alone exceeds the target (a one-sentence answer beats no answer). - Idempotent: running on already-truncated text is a no-op. Wired at the same four return sites in `claude-client.ts` as the banned-ritual stripper (lines 770, 901, 1423, 1454). The chain is now `stripBannedRituals` → `truncateLongResponseToShortQuestion` → return. 10 new unit tests cover question-length gate, response-length gate, sentence-boundary preservation, single-giant-sentence fallback, code-block preservation, idempotence, empty-input handling. 21/21 tests pass on the postprocess module. Expected redteam impact: priv-1 / acct-1 / acct-2 / gap-1 length_cap failures drop to zero. The single remaining length-related concern is priv-1's banned_marker on "adcp is more private", but that's a separate overclaim issue the truncator doesn't address (the marker can appear within the truncated body). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(addie): empty-response fallback + factor pipeline into single helper Three things in one tidy commit: 1) Empty-response guard. Two redteam-flagged turns produced rating=1 "Response is empty" responses on short follow-up prompts ("?", "what happened?"). The model returned an assistant message with empty text. Now substitutes a clarifying fallback ("Sorry, I lost the thread there. Could you rephrase or give me a bit more context?") so the user gets something rather than nothing. 2) Pipeline factoring. The four assistant-text return sites in claude-client.ts each did `stripBannedRituals(...)` → `truncateLongResponseToShortQuestion(userMessage, ...)`. Factored the chain into a single `applyResponsePipeline(question, rawText)` helper in response-postprocess.ts so the pipeline (strip → empty-guard → truncate) lives in one place. Future additions slot in there. 3) Tests. 4 new unit tests on the pipeline helper covering empty input, ritual-only input (gets stripped to empty → fallback), substantive long input (gets stripped + truncated), and substantive short input (passes through unchanged). 25/25 tests in the postprocess module pass. Closes the third item from the cycle followup punch list (empty-message bug, #124-style). The actual root-cause investigation of *why* Sonnet returns empty assistant blocks is out of scope — this just turns the silent-empty into a polite-clarifying-fallback so users aren't left staring at a blank reply. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.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.
Summary
Targeted followup to the 12 baseline redteam failures from PR #3273. Two failure modes survived the Haiku→Sonnet bump there: length blow-out on short questions and "cryptographic guarantee" overclaim on TMP privacy. Both addressed via positive teaching in the rules — the user's explicit preference: teaching better than banning.
Changes
server/src/addie/rules/response-style.md— Match the register"What does AdCP not do?"is not defensive verbosity — it's defaulting to the full structured list when the caller asked a conversational question. Rule: 2–3-item summary + "want me to go deeper?". Comprehensive answers opt-in. Includes a real anti-example (280 words structured vs 35 words conversational, same information density).server/src/addie/rules/knowledge.md— Privacy in AdCPVerification
Local 5-scenario focused redteam (against the 5 failures from PR #3273's run):
priv-2("Structural privacy separation")priv-1("surveillance capitalism")acct-1("agent spends $500K")acct-2("AI screws up")gap-1("what AdCP doesn't do")Privacy teaching landed cleanly. Length teaching did not move the dial materially. Direct probe of
gap-1showed Sonnet's response to"What does AdCP not do?"is 283 words of factually correct, well-structured content — same information density a comprehensive listing should have. Sonnet's instinct toward thoroughness on substantive questions overrides the length rule even with the new self-check + word-count guidance.Added the "Don't dump comprehensive lists" anti-pattern callout in the second commit specifically targeting this mode (showing the same gap-1 question answered both ways), but it's not yet validated against the redteam — fresh local stack would need a 10-min rebuild and another budget burn to confirm. Reasonable to validate on staging post-merge instead.
Honest assessment
If the anti-list-dump teaching also doesn't move the needle on the next redteam pass, the next step is a length-truncation post-processor — same shape as the banned-ritual stripper, hard cap at 160 words on responses to questions ≤15 words long, with a "want me to expand?" suffix.
Test plan
npx vitest run server/tests/unit/addie/rules-loader.test.ts— 9/9 passnpm run test:redteamagainst staging — expected ≥27/33 effective if anti-list-dump teaching lands; ≤24/33 means we need the length post-processor🤖 Generated with Claude Code