@@ -485,3 +485,151 @@ async def test_error_mid_conversation_leaves_history_unchanged() -> None:
485485 await agent .send ("Second message" )
486486
487487 assert agent .history == history_after_first
488+
489+
490+ # ---------------------------------------------------------------------------
491+ # Prompt caching: static breakpoints (system + last tool, 1h TTL)
492+ # ---------------------------------------------------------------------------
493+
494+
495+ async def test_system_prompt_sent_as_block_with_static_cache_control () -> None :
496+ resp = make_response ("end_turn" , [make_text_block ("ok" )])
497+ agent , create_mock = make_agent (mock_response = resp )
498+
499+ await agent .send ("Hi" )
500+
501+ assert create_mock .call_args .kwargs ["system" ] == [
502+ {
503+ "type" : "text" ,
504+ "text" : "You are helpful." ,
505+ "cache_control" : {"type" : "ephemeral" , "ttl" : "1h" },
506+ }
507+ ]
508+
509+
510+ @pytest .mark .parametrize (
511+ "tool_names" ,
512+ [["only" ], ["a" , "b" ], ["a" , "b" , "c" , "d" ]],
513+ ids = ["single_tool" , "two_tools" , "four_tools" ],
514+ )
515+ async def test_only_last_tool_carries_static_cache_control (tool_names : list [str ]) -> None :
516+ registry = ToolRegistry ([FakeTool (n ) for n in tool_names ])
517+ resp = make_response ("end_turn" , [make_text_block ("ok" )])
518+ agent , create_mock = make_agent (tools = registry , mock_response = resp )
519+
520+ await agent .send ("Hi" )
521+
522+ sent_tools = create_mock .call_args .kwargs ["tools" ]
523+ assert all ("cache_control" not in t for t in sent_tools [:- 1 ])
524+ assert sent_tools [- 1 ]["cache_control" ] == {"type" : "ephemeral" , "ttl" : "1h" }
525+
526+
527+ async def test_allowed_tools_subset_places_cache_control_on_last_of_subset () -> None :
528+ registry = ToolRegistry ([FakeTool (n ) for n in ["a" , "b" , "c" ]])
529+ resp = make_response ("end_turn" , [make_text_block ("ok" )])
530+ agent , create_mock = make_agent (tools = registry , mock_response = resp )
531+
532+ await agent .send ("Hi" , allowed_tools = ["a" , "b" ])
533+
534+ sent_tools = create_mock .call_args .kwargs ["tools" ]
535+ assert [t ["name" ] for t in sent_tools ] == ["a" , "b" ]
536+ assert "cache_control" not in sent_tools [0 ]
537+ assert sent_tools [- 1 ]["cache_control" ] == {"type" : "ephemeral" , "ttl" : "1h" }
538+
539+
540+ # ---------------------------------------------------------------------------
541+ # Prompt caching: sliding breakpoint on the last user message block (default TTL)
542+ # ---------------------------------------------------------------------------
543+
544+
545+ @pytest .mark .parametrize (
546+ "content" ,
547+ [
548+ pytest .param ("Hi there" , id = "str" ),
549+ pytest .param (
550+ [ToolResultMessage (tool_call_id = "t1" , result = ToolResult (success = True , data = "r1" ))],
551+ id = "single_tool_result" ,
552+ ),
553+ pytest .param (
554+ [ToolResultMessage (tool_call_id = f"t{ i } " , result = ToolResult (success = True , data = f"r{ i } " )) for i in range (3 )],
555+ id = "multiple_tool_results" ,
556+ ),
557+ ],
558+ )
559+ async def test_sliding_cache_control_on_last_user_block_only (
560+ content : str | list [ToolResultMessage ],
561+ ) -> None :
562+ resp = make_response ("end_turn" , [make_text_block ("ok" )])
563+ agent , create_mock = make_agent (mock_response = resp )
564+
565+ await agent .send (content )
566+
567+ blocks = create_mock .call_args .kwargs ["messages" ][- 1 ]["content" ]
568+ assert isinstance (blocks , list ) and blocks
569+ assert all ("cache_control" not in b for b in blocks [:- 1 ])
570+ assert blocks [- 1 ]["cache_control" ] == {"type" : "ephemeral" }
571+ assert "ttl" not in blocks [- 1 ]["cache_control" ]
572+
573+
574+ async def test_empty_tool_results_produces_empty_content_block () -> None :
575+ resp = make_response ("end_turn" , [make_text_block ("ok" )])
576+ agent , create_mock = make_agent (mock_response = resp )
577+
578+ await agent .send ([])
579+
580+ blocks = create_mock .call_args .kwargs ["messages" ][- 1 ]["content" ]
581+ assert blocks == []
582+
583+
584+ # ---------------------------------------------------------------------------
585+ # Prompt caching: history must not retain cache_control markers
586+ # (otherwise multi-turn requests would exceed the 4-marker limit)
587+ # ---------------------------------------------------------------------------
588+
589+
590+ async def test_history_str_message_keeps_raw_str_form () -> None :
591+ resp = make_response ("end_turn" , [make_text_block ("ok" )])
592+ agent , _ = make_agent (mock_response = resp )
593+
594+ await agent .send ("Hi" )
595+
596+ assert agent .history [0 ] == {"role" : "user" , "content" : "Hi" }
597+
598+
599+ async def test_history_tool_result_blocks_have_no_cache_control () -> None :
600+ resp = make_response ("end_turn" , [make_text_block ("ok" )])
601+ agent , _ = make_agent (mock_response = resp )
602+
603+ tool_results = [
604+ ToolResultMessage (tool_call_id = f"t{ i } " , result = ToolResult (success = True , data = f"r{ i } " )) for i in range (2 )
605+ ]
606+ await agent .send (tool_results )
607+
608+ blocks = agent .history [0 ]["content" ]
609+ assert all ("cache_control" not in b for b in blocks )
610+
611+
612+ async def test_multi_turn_only_latest_user_message_in_request_has_cache_control () -> None :
613+ first_resp = make_response ("tool_use" , [make_tool_use_block (id = "t1" )])
614+ second_resp = make_response ("end_turn" , [make_text_block ("done" )])
615+
616+ client = MagicMock (spec = anthropic .AsyncAnthropic )
617+ client .messages = MagicMock ()
618+ client .messages .create = AsyncMock (side_effect = [first_resp , second_resp ])
619+ client .models = MagicMock ()
620+ client .models .retrieve = AsyncMock (return_value = SimpleNamespace (max_input_tokens = FAKE_CONTEXT_WINDOW ))
621+ agent = AnthropicAgent (client = client , tools = ToolRegistry ([]), system_prompt = "sp" , name = "t" )
622+
623+ await agent .send ("First" )
624+ await agent .send ([ToolResultMessage (tool_call_id = "t1" , result = ToolResult (success = True , data = "r" ))])
625+
626+ first_call_messages = client .messages .create .call_args_list [0 ].kwargs ["messages" ]
627+ assert first_call_messages [- 1 ]["content" ] == [
628+ {"type" : "text" , "text" : "First" , "cache_control" : {"type" : "ephemeral" }}
629+ ]
630+
631+ second_call_messages = client .messages .create .call_args_list [1 ].kwargs ["messages" ]
632+ assert second_call_messages [0 ] == {"role" : "user" , "content" : "First" }
633+ latest_blocks = second_call_messages [- 1 ]["content" ]
634+ assert all ("cache_control" not in b for b in latest_blocks [:- 1 ])
635+ assert latest_blocks [- 1 ]["cache_control" ] == {"type" : "ephemeral" }
0 commit comments