Skip to content

Python: [Bug]: SkillsProvider stringifies structured Anthropic instructions and disables prompt caching #7700

Description

@coding-totoro

Description

Anthropic structured system blocks work through Agent.run() until the stock SkillsProvider contributes its generated skill-catalog instructions. At that point, Agent._prepare_session_and_messages() interpolates the existing structured instructions value into an f-string, converting the list of Anthropic text blocks into a plain string.

The final Anthropic request therefore contains a string-valued system parameter. The text still contains the literal characters cache_control, but it is no longer structured metadata, so Anthropic prompt caching is not enabled.

Expected

SkillsProvider instructions should be appended without destroying provider-native structured instructions. In this example, the original cached system block should remain a mapping with cache_control, and the generated skill catalog should be appended as additional system text.

Actual

without SkillsProvider:
  system type: list
  structured cache_control: True

with SkillsProvider:
  system type: str
  structured cache_control: False

Suspected boundary

Current Agent._prepare_session_and_messages() does:

if session_context.instructions:
    combined_instructions = "\n".join(session_context.instructions)
    if "instructions" in chat_options:
        chat_options["instructions"] = f"{chat_options['instructions']}\n{combined_instructions}"

This is provider-neutral code, but Anthropic legitimately supports instructions as structured system blocks for prompt caching. The f-string coercion loses that structure.

This is related to #6450 / #6794, which added structured Anthropic system-block support at the provider boundary. The provider support works in the baseline below; the later ContextProvider merge undoes it when SkillsProvider is present.

Code Sample

The reproduction uses only released packages and a fake Anthropic transport. It performs no network request and requires no API key.

import asyncio

from agent_framework import Agent, ChatOptions, InlineSkill, SkillFrontmatter, SkillsProvider
from agent_framework_anthropic import AnthropicClient
from anthropic.types.beta import BetaMessage, BetaTextBlock, BetaUsage


class FakeMessages:
    def __init__(self):
        self.requests = []

    async def create(self, **kwargs):
        self.requests.append(kwargs)
        return BetaMessage(
            id="msg_repro",
            content=[BetaTextBlock(type="text", text="ok")],
            model="claude-opus-4-8",
            role="assistant",
            stop_reason="end_turn",
            type="message",
            usage=BetaUsage(input_tokens=1, output_tokens=1),
        )


class FakeBeta:
    def __init__(self, messages):
        self.messages = messages


class FakeAnthropicClient:
    def __init__(self):
        self.base_url = "https://example.invalid"
        self.messages = FakeMessages()
        self.beta = FakeBeta(self.messages)


SYSTEM_BLOCKS = [
    {
        "type": "text",
        "text": "Stable instructions that should be cached.",
        "cache_control": {"type": "ephemeral", "ttl": "1h"},
    },
    {"type": "text", "text": "Dynamic request context that should not be cached."},
]


async def capture_request(*, with_skills):
    transport = FakeAnthropicClient()
    providers = []
    if with_skills:
        skill = InlineSkill(
            frontmatter=SkillFrontmatter(
                name="example-skill",
                description="A generic standalone example skill.",
            ),
            instructions="Use this generic skill when asked for an example.",
        )
        providers.append(
            SkillsProvider(
                [skill],
                disable_load_skill_approval=True,
                disable_read_skill_resource_approval=True,
            )
        )

    client = AnthropicClient(
        anthropic_client=transport,
        model="claude-opus-4-8",
        additional_beta_flags=["extended-cache-ttl-2025-04-11"],
    )
    agent = Agent(
        client=client,
        default_options=ChatOptions(
            model="claude-opus-4-8",
            max_tokens=64,
            instructions=SYSTEM_BLOCKS,
        ),
        context_providers=providers,
    )
    async with agent:
        await agent.run("Hello")
    return transport.messages.requests[0]


def has_structured_cache_control(request):
    system = request.get("system")
    return (
        isinstance(system, list)
        and bool(system)
        and isinstance(system[0], dict)
        and system[0].get("cache_control") == {"type": "ephemeral", "ttl": "1h"}
    )


async def main():
    baseline = await capture_request(with_skills=False)
    with_skills = await capture_request(with_skills=True)

    print("without SkillsProvider:")
    print("  system type:", type(baseline.get("system")).__name__)
    print("  structured cache_control:", has_structured_cache_control(baseline))
    print()
    print("with SkillsProvider:")
    print("  system type:", type(with_skills.get("system")).__name__)
    print("  structured cache_control:", has_structured_cache_control(with_skills))

    assert has_structured_cache_control(baseline)
    assert not has_structured_cache_control(with_skills)
    assert isinstance(with_skills.get("system"), str)


asyncio.run(main())

Run with:

python -m pip install agent-framework-core==1.14.0 agent-framework-anthropic==1.0.0b260730
python repro.py

Error Messages / Stack Traces

No exception is raised. The cache breakpoint is silently converted from structured metadata into ordinary text.

Package Versions

  • agent-framework-core: 1.14.0
  • agent-framework-anthropic: 1.0.0b260730
  • anthropic: 0.116.0

Python Version

Python 3.14.6

Additional Context

The same central merge is present on the current main branch. A regression test should exercise the public Agent.run() path with structured Anthropic instructions both with and without a stock SkillsProvider.

Metadata

Metadata

Labels

pythonUsage: [Issues, PRs], Target: PythonreproducedUsage: [Issues], Target: all issues that can be reproduced by the triage workflow

Type

Projects

Status
No status

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions