patchpal 0.4.2__tar.gz → 0.4.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. {patchpal-0.4.2/patchpal.egg-info → patchpal-0.4.3}/PKG-INFO +1 -1
  2. {patchpal-0.4.2 → patchpal-0.4.3}/patchpal/__init__.py +1 -1
  3. {patchpal-0.4.2 → patchpal-0.4.3}/patchpal/agent.py +43 -4
  4. {patchpal-0.4.2 → patchpal-0.4.3/patchpal.egg-info}/PKG-INFO +1 -1
  5. {patchpal-0.4.2 → patchpal-0.4.3}/tests/test_agent.py +27 -14
  6. {patchpal-0.4.2 → patchpal-0.4.3}/LICENSE +0 -0
  7. {patchpal-0.4.2 → patchpal-0.4.3}/MANIFEST.in +0 -0
  8. {patchpal-0.4.2 → patchpal-0.4.3}/README.md +0 -0
  9. {patchpal-0.4.2 → patchpal-0.4.3}/patchpal/cli.py +0 -0
  10. {patchpal-0.4.2 → patchpal-0.4.3}/patchpal/context.py +0 -0
  11. {patchpal-0.4.2 → patchpal-0.4.3}/patchpal/permissions.py +0 -0
  12. {patchpal-0.4.2 → patchpal-0.4.3}/patchpal/skills.py +0 -0
  13. {patchpal-0.4.2 → patchpal-0.4.3}/patchpal/system_prompt.md +0 -0
  14. {patchpal-0.4.2 → patchpal-0.4.3}/patchpal/tools.py +0 -0
  15. {patchpal-0.4.2 → patchpal-0.4.3}/patchpal.egg-info/SOURCES.txt +0 -0
  16. {patchpal-0.4.2 → patchpal-0.4.3}/patchpal.egg-info/dependency_links.txt +0 -0
  17. {patchpal-0.4.2 → patchpal-0.4.3}/patchpal.egg-info/entry_points.txt +0 -0
  18. {patchpal-0.4.2 → patchpal-0.4.3}/patchpal.egg-info/requires.txt +0 -0
  19. {patchpal-0.4.2 → patchpal-0.4.3}/patchpal.egg-info/top_level.txt +0 -0
  20. {patchpal-0.4.2 → patchpal-0.4.3}/pyproject.toml +0 -0
  21. {patchpal-0.4.2 → patchpal-0.4.3}/setup.cfg +0 -0
  22. {patchpal-0.4.2 → patchpal-0.4.3}/tests/test_cli.py +0 -0
  23. {patchpal-0.4.2 → patchpal-0.4.3}/tests/test_context.py +0 -0
  24. {patchpal-0.4.2 → patchpal-0.4.3}/tests/test_guardrails.py +0 -0
  25. {patchpal-0.4.2 → patchpal-0.4.3}/tests/test_operational_safety.py +0 -0
  26. {patchpal-0.4.2 → patchpal-0.4.3}/tests/test_skills.py +0 -0
  27. {patchpal-0.4.2 → patchpal-0.4.3}/tests/test_tools.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: patchpal
3
- Version: 0.4.2
3
+ Version: 0.4.3
4
4
  Summary: A lean Claude Code clone in pure Python
5
5
  Author: PatchPal Contributors
6
6
  License-Expression: Apache-2.0
@@ -1,6 +1,6 @@
1
1
  """PatchPal - An open-source Claude Code clone implemented purely in Python."""
2
2
 
3
- __version__ = "0.4.2"
3
+ __version__ = "0.4.3"
4
4
 
5
5
  from patchpal.agent import create_agent
6
6
  from patchpal.tools import (
@@ -756,13 +756,52 @@ def _apply_prompt_caching(messages: List[Dict[str, Any]], model_id: str) -> List
756
756
 
757
757
  # Apply caching to system messages (first 2)
758
758
  for idx in system_messages[:2]:
759
- if "cache_control" not in messages[idx] and "cachePoint" not in messages[idx]:
760
- messages[idx] = {**messages[idx], **cache_marker}
759
+ msg = messages[idx]
760
+ # Skip if already has cache marker at content block level
761
+ if isinstance(msg.get("content"), list):
762
+ # Already structured - check if any block has cache_control/cachePoint
763
+ has_cache = any(
764
+ "cache_control" in block or "cachePoint" in block
765
+ for block in msg["content"]
766
+ if isinstance(block, dict)
767
+ )
768
+ if not has_cache and msg["content"]:
769
+ # Add cache marker to the last content block
770
+ last_block = msg["content"][-1]
771
+ if isinstance(last_block, dict):
772
+ last_block.update(cache_marker)
773
+ else:
774
+ # Convert simple string content to structured format with cache marker
775
+ content_text = msg.get("content", "")
776
+ messages[idx] = {
777
+ **msg,
778
+ "content": [{"type": "text", "text": content_text, **cache_marker}],
779
+ }
761
780
 
762
781
  # Apply caching to last 2 messages
763
782
  for idx in last_two_indices:
764
- if "cache_control" not in messages[idx] and "cachePoint" not in messages[idx]:
765
- messages[idx] = {**messages[idx], **cache_marker}
783
+ msg = messages[idx]
784
+ # Skip if already has cache marker at content block level
785
+ if isinstance(msg.get("content"), list):
786
+ # Already structured - check if any block has cache_control/cachePoint
787
+ has_cache = any(
788
+ "cache_control" in block or "cachePoint" in block
789
+ for block in msg["content"]
790
+ if isinstance(block, dict)
791
+ )
792
+ if not has_cache and msg["content"]:
793
+ # Add cache marker to the last content block
794
+ last_block = msg["content"][-1]
795
+ if isinstance(last_block, dict):
796
+ last_block.update(cache_marker)
797
+ else:
798
+ # Convert simple string content to structured format with cache marker
799
+ content_text = msg.get("content", "")
800
+ if content_text: # Only convert non-empty content
801
+ messages[idx] = {
802
+ **msg,
803
+ "content": [{"type": "text", "text": content_text, **cache_marker}],
804
+ }
766
805
 
767
806
  return messages
768
807
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: patchpal
3
- Version: 0.4.2
3
+ Version: 0.4.3
4
4
  Summary: A lean Claude Code clone in pure Python
5
5
  Author: PatchPal Contributors
6
6
  License-Expression: Apache-2.0
@@ -441,13 +441,17 @@ def test_prompt_caching_application_anthropic():
441
441
  # Test with direct Anthropic API
442
442
  cached_messages = _apply_prompt_caching(messages.copy(), "anthropic/claude-sonnet-4-5")
443
443
 
444
- # System message should have cache_control
445
- assert "cache_control" in cached_messages[0]
446
- assert cached_messages[0]["cache_control"] == {"type": "ephemeral"}
444
+ # System message should have cache_control inside content block
445
+ assert isinstance(cached_messages[0]["content"], list)
446
+ assert cached_messages[0]["content"][0]["type"] == "text"
447
+ assert "cache_control" in cached_messages[0]["content"][0]
448
+ assert cached_messages[0]["content"][0]["cache_control"] == {"type": "ephemeral"}
447
449
 
448
- # Last 2 messages should have cache_control
449
- assert "cache_control" in cached_messages[-1] # Last user message
450
- assert "cache_control" in cached_messages[-2] # Last assistant message
450
+ # Last 2 messages should have cache_control inside content blocks
451
+ assert isinstance(cached_messages[-1]["content"], list) # Last user message
452
+ assert "cache_control" in cached_messages[-1]["content"][0]
453
+ assert isinstance(cached_messages[-2]["content"], list) # Last assistant message
454
+ assert "cache_control" in cached_messages[-2]["content"][0]
451
455
 
452
456
 
453
457
  def test_prompt_caching_application_bedrock():
@@ -466,13 +470,17 @@ def test_prompt_caching_application_bedrock():
466
470
  messages.copy(), "bedrock/anthropic.claude-sonnet-4-5-v1:0"
467
471
  )
468
472
 
469
- # System message should have cachePoint (Bedrock format)
470
- assert "cachePoint" in cached_messages[0]
471
- assert cached_messages[0]["cachePoint"] == {"type": "ephemeral"}
473
+ # System message should have cachePoint inside content block (Bedrock format)
474
+ assert isinstance(cached_messages[0]["content"], list)
475
+ assert cached_messages[0]["content"][0]["type"] == "text"
476
+ assert "cachePoint" in cached_messages[0]["content"][0]
477
+ assert cached_messages[0]["content"][0]["cachePoint"] == {"type": "ephemeral"}
472
478
 
473
- # Last 2 messages should have cachePoint
474
- assert "cachePoint" in cached_messages[-1]
475
- assert "cachePoint" in cached_messages[-2]
479
+ # Last 2 messages should have cachePoint inside content blocks
480
+ assert isinstance(cached_messages[-1]["content"], list)
481
+ assert "cachePoint" in cached_messages[-1]["content"][0]
482
+ assert isinstance(cached_messages[-2]["content"], list)
483
+ assert "cachePoint" in cached_messages[-2]["content"][0]
476
484
 
477
485
 
478
486
  def test_prompt_caching_no_modification_for_unsupported():
@@ -506,5 +514,10 @@ def test_prompt_caching_idempotent():
506
514
  cached_once = _apply_prompt_caching(messages.copy(), "anthropic/claude-sonnet-4-5")
507
515
  cached_twice = _apply_prompt_caching(cached_once.copy(), "anthropic/claude-sonnet-4-5")
508
516
 
509
- # Should be the same after second application
510
- assert cached_once == cached_twice
517
+ # Should have the same structure after second application
518
+ assert cached_once[0]["content"][0] == cached_twice[0]["content"][0]
519
+ assert cached_once[1]["content"][0] == cached_twice[1]["content"][0]
520
+
521
+ # Should only have one cache_control marker per message
522
+ assert len([k for k in cached_twice[0]["content"][0].keys() if "cache" in k]) == 1
523
+ assert len([k for k in cached_twice[1]["content"][0].keys() if "cache" in k]) == 1
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes