Skip to content

fix(agent): declare skill cache boundaries on the message - #81929

Closed
fangliquanflq wants to merge 5 commits into
NousResearch:mainfrom
fangliquanflq:fix/skill-cache-prefix
Closed

fangliquanflq wants to merge 5 commits into
NousResearch:mainfrom
fangliquanflq:fix/skill-cache-prefix

Conversation

@fangliquanflq

@fangliquanflq fangliquanflq commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

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_len sidecar (also carried on a _CacheBoundedStr content value through the Event.text -> run_conversation path). The cache planner splits on that declared length; it does not re-parse instruction marker strings with rfind, 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_marker receives a user string built by agent/skill_commands.py::_build_skill_message or the cron skill prompt builder.

Causal chain:

  1. A webhook or scheduled job combines a stable expanded skill body with a volatile per-invocation instruction.
  2. _apply_cache_marker wraps that complete user string in one marked text block.
  3. The volatile tail becomes part of the cache key, so the next invocation cannot reuse the otherwise identical stable prefix.

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

  • Builders declare the stable prefix length when appending a user/cron instruction (_CacheBoundedStr + _cache_stable_prefix_len).
  • turn_context preserves the boundary across surrogate sanitization, lifts it onto the message dict, and clears it in drop_stale_api_content when content is rewritten (merge-summary prepend, etc.).
  • _apply_cache_marker / cache stripping use the declared length only; failover redecorate keeps the sidecar.
  • Message-local sidecar survives send-path api_content plain-str rewrites; invalid/missing boundaries fall back to whole-message caching (including SessionDB resume).

Related Issue

Fixes #81867

Type of Change

  • Bug fix
  • Performance improvement
  • Tests

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

  1. Manual: Build two real webhook-style /airtable skill 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.
  2. Automated:
scripts/run_tests.sh \
  tests/agent/test_prompt_caching.py \
  tests/agent/test_skill_invocation_description.py \
  tests/cron/test_scheduler.py

Checklist

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix
  • I've run the repository test entry on relevant tests and all tests pass
  • I've added regression tests for the fix
  • I've tested on Windows 10

@alt-glitch alt-glitch added type/perf Performance improvement or optimization P0 Critical — data loss, security, crash loop comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint tool/skills Skills system (list, view, manage) comp/cron Cron scheduler and job management platform/webhook Webhook / API server provider/anthropic Anthropic native Messages API sweeper:risk-caching Sweeper risk: may break/degrade prompt caching or cache-key stability (invariant) labels Aug 8, 2026
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.
@fangliquanflq fangliquanflq changed the title fix(caching): stop invocation tails from forcing full cache rewrites fix(agent): declare skill cache boundaries on the message Aug 9, 2026
@kshitijk4poor

Copy link
Copy Markdown
Contributor

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 str subclass plus a sidecar key inside canonical conversation history, which widens the invariant surface (persistence, deepcopy, compression paths) compared to #82049's process-local registry that leaves history bytes untouched and falls back to today's behavior on any miss. Your mid-session edge-case coverage (sanitizer rebasing, turn-context annotation) was the most thorough of the three and informed the review. Much appreciated!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/cron Cron scheduler and job management P0 Critical — data loss, security, crash loop platform/webhook Webhook / API server provider/anthropic Anthropic native Messages API sweeper:risk-caching Sweeper risk: may break/degrade prompt caching or cache-key stability (invariant) tool/skills Skills system (list, view, manage) type/perf Performance improvement or optimization

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Webhook/skill-invocation user messages cache as one atomic block — volatile tail bytes invalidate the entire static prefix

3 participants