fix(agent): declare skill cache boundaries on the message - #81929
fangliquanflq wants to merge 5 commits into
Conversation
Replace rfind-based skill-invocation splits with a builder-declared _cache_stable_prefix_len sidecar so volatile webhook/cron tails no longer invalidate the stable scaffold, including when payloads quote the marker.
|
Closing in favor of #82423 (a salvage of #82049 by @JoaoMarcos44, which is landing as the fix for #81867). Thank you for attacking this — your PR correctly identified the atomic-block problem and shipped a working split with good sidecar/round-trip handling. The deciding factor between the three competing PRs was the boundary source: deriving the split from the message-local declared length is sound, but the implementation carries a |
What does this PR do?
Repeated webhook and cron skill invocations force Anthropic to rewrite the entire expanded skill message when only the per-run ticket, timestamp, or instruction changes. This change keeps the canonical conversation string byte-identical while request-locally marking only a builder-declared stable skill scaffold prefix and leaving the volatile invocation tail unmarked.
Builders attach the exact stable/volatile boundary at construction time via a message-local
_cache_stable_prefix_lensidecar (also carried on a_CacheBoundedStrcontent value through the Event.text -> run_conversation path). The cache planner splits on that declared length; it does not re-parse instruction marker strings withrfind, so a helpdesk payload that quotes the marker cannot poison the cached prefix.Symptom
Two skill-triggered turns within the cache TTL can contain the same large skill body and differ only in a short invocation-specific tail, yet each Anthropic request marks the whole user message as one cache block. The second request therefore cannot reuse the stable skill prefix at an internal cache breakpoint.
Impact
Webhook and scheduled jobs that repeatedly invoke large skills incur avoidable cache writes for stable content. The reported production route ran hundreds of times per day and attributed approximately 58% of account spend to these cache writes. This PR makes that stable content reusable without changing stored transcripts, ordinary user messages, or the session system prompt. It does not claim a measured post-merge billing reduction.
Bug Cause
Trigger:
agent/prompt_caching.py::_apply_cache_markerreceives a user string built byagent/skill_commands.py::_build_skill_messageor the cron skill prompt builder.Causal chain:
_apply_cache_markerwraps that complete user string in one marked text block.Why it is wrong: The boundary known at construction time was discarded when parts were joined into one string, and recovering it by searching for instruction markers is unsafe when the volatile payload quotes those markers.
Working sibling / contrast: System messages already split a stable prefix from a session-specific suffix via
static_system_prefix. Ordinary user messages and ordinary bundle invocations (instruction before skill blocks) correctly retain the one-block policy. Cron appends its instruction after all stable blocks and declares the same boundary.Ruled out: This is not TTL expiry or system-prompt mutation. The reported calls were 25 seconds apart, and the real-environment reproduction held the system bytes constant while only the user tail changed.
Fix
_CacheBoundedStr+_cache_stable_prefix_len).turn_contextpreserves the boundary across surrogate sanitization, lifts it onto the message dict, and clears it indrop_stale_api_contentwhen content is rewritten (merge-summary prepend, etc.)._apply_cache_marker/ cache stripping use the declared length only; failover redecorate keeps the sidecar.api_contentplain-str rewrites; invalid/missing boundaries fall back to whole-message caching (including SessionDB resume).Related Issue
Fixes #81867
Type of Change
Changes Made
agent/skill_commands.py- builder-declared cache boundary helpers and single-skill scaffold registration.agent/prompt_caching.py- decorate/strip using the message-local sidecar.agent/turn_context.py- lift, sanitize-preserve, and drop-stale clearing of the boundary.cron/scheduler.py- declare the boundary for cron skill prompts (scanner-mutation guarded).tests/agent/test_prompt_caching.py- stable-prefix reuse, marker-quoting, api_content survival, drop-stale clearing, strip/redecorate.tests/agent/test_skill_invocation_description.py- real-builder boundary and quoting coverage.tests/cron/test_scheduler.py- single-skill, multi-skill, bundle-only, missing-skill cron variants.How to Test
/airtableskill messages with the same installed skill and different ticket/time tails, decorate both for native Anthropic, and verify the first marked blocks are identical, the second blocks are unmarked and distinct, and stripping restores each original string exactly. Repeat with a payload that quotes the instruction marker, and with the cron prompt builder.Checklist