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.
Description
Anthropic structured system blocks work through
Agent.run()until the stockSkillsProvidercontributes its generated skill-catalog instructions. At that point,Agent._prepare_session_and_messages()interpolates the existing structuredinstructionsvalue into an f-string, converting the list of Anthropic text blocks into a plain string.The final Anthropic request therefore contains a string-valued
systemparameter. The text still contains the literal characterscache_control, but it is no longer structured metadata, so Anthropic prompt caching is not enabled.Expected
SkillsProviderinstructions should be appended without destroying provider-native structured instructions. In this example, the original cached system block should remain a mapping withcache_control, and the generated skill catalog should be appended as additional system text.Actual
Suspected boundary
Current
Agent._prepare_session_and_messages()does:This is provider-neutral code, but Anthropic legitimately supports
instructionsas 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.
Run with:
Error Messages / Stack Traces
No exception is raised. The cache breakpoint is silently converted from structured metadata into ordinary text.
Package Versions
Python Version
Python 3.14.6
Additional Context
The same central merge is present on the current
mainbranch. A regression test should exercise the publicAgent.run()path with structured Anthropic instructions both with and without a stockSkillsProvider.