patchpal 0.25.3__tar.gz → 0.25.4__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.
- {patchpal-0.25.3/patchpal.egg-info → patchpal-0.25.4}/PKG-INFO +1 -1
- {patchpal-0.25.3 → patchpal-0.25.4}/patchpal/__init__.py +1 -1
- {patchpal-0.25.3 → patchpal-0.25.4}/patchpal/agent/function_calling.py +45 -9
- {patchpal-0.25.3 → patchpal-0.25.4/patchpal.egg-info}/PKG-INFO +1 -1
- {patchpal-0.25.3 → patchpal-0.25.4}/tests/test_agent.py +72 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/LICENSE +0 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/MANIFEST.in +0 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/README.md +0 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/patchpal/agent/__init__.py +0 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/patchpal/agent/bedrock_profile_utils.py +0 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/patchpal/agent/react.py +0 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/patchpal/cli/__init__.py +0 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/patchpal/cli/autopilot.py +0 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/patchpal/cli/interactive.py +0 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/patchpal/cli/mcp.py +0 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/patchpal/cli/sandbox.py +0 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/patchpal/cli/streaming.py +0 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/patchpal/config.py +0 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/patchpal/context.py +0 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/patchpal/permissions.py +0 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/patchpal/prompts/react_prompt.md +0 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/patchpal/prompts/system_prompt.md +0 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/patchpal/skills.py +0 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/patchpal/tools/__init__.py +0 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/patchpal/tools/audit.py +0 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/patchpal/tools/browser_tools.py +0 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/patchpal/tools/code_analysis.py +0 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/patchpal/tools/common.py +0 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/patchpal/tools/definitions.py +0 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/patchpal/tools/file_reading.py +0 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/patchpal/tools/file_writing.py +0 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/patchpal/tools/find_tool.py +0 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/patchpal/tools/grep_tool.py +0 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/patchpal/tools/image_handler.py +0 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/patchpal/tools/mcp.py +0 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/patchpal/tools/repo_map.py +0 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/patchpal/tools/shell_tools.py +0 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/patchpal/tools/todo_tools.py +0 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/patchpal/tools/tool_schema.py +0 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/patchpal/tools/user_interaction.py +0 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/patchpal/tools/web_tools.py +0 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/patchpal.egg-info/SOURCES.txt +0 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/patchpal.egg-info/dependency_links.txt +0 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/patchpal.egg-info/entry_points.txt +0 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/patchpal.egg-info/requires.txt +0 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/patchpal.egg-info/top_level.txt +0 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/pyproject.toml +0 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/setup.cfg +0 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/tests/test_cli.py +0 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/tests/test_config_dynamic.py +0 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/tests/test_context.py +0 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/tests/test_custom_tools.py +0 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/tests/test_enabled_tools.py +0 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/tests/test_find_tool.py +0 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/tests/test_guardrails.py +0 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/tests/test_image_blocking.py +0 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/tests/test_maximum_security.py +0 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/tests/test_mcp_config.py +0 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/tests/test_memory.py +0 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/tests/test_memory_locations.py +0 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/tests/test_operational_safety.py +0 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/tests/test_optional_tools.py +0 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/tests/test_permissions.py +0 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/tests/test_react.py +0 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/tests/test_reasoning_content.py +0 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/tests/test_repo_map.py +0 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/tests/test_simplified_prompt.py +0 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/tests/test_skills.py +0 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/tests/test_streaming.py +0 -0
- {patchpal-0.25.3 → patchpal-0.25.4}/tests/test_tools.py +0 -0
|
@@ -311,7 +311,9 @@ def _apply_prompt_caching(messages: List[Dict[str, Any]], model_id: str) -> List
|
|
|
311
311
|
"""Apply prompt caching markers to messages following OpenCode's strategy.
|
|
312
312
|
|
|
313
313
|
Caches:
|
|
314
|
-
-
|
|
314
|
+
- The FIRST system message only (the static SYSTEM_PROMPT). The volatile
|
|
315
|
+
per-call date/time is appended as a trailing system message and is
|
|
316
|
+
deliberately NOT cached (its content changes every call).
|
|
315
317
|
- Last 2 non-system messages (recent context, any role except system)
|
|
316
318
|
|
|
317
319
|
IMPORTANT: AWS Bedrock enforces a hard limit of 4 cache control blocks per request.
|
|
@@ -389,6 +391,13 @@ def _apply_prompt_caching(messages: List[Dict[str, Any]], model_id: str) -> List
|
|
|
389
391
|
# Find system messages (usually at the start)
|
|
390
392
|
system_messages = [i for i, msg in enumerate(messages) if msg.get("role") == "system"]
|
|
391
393
|
|
|
394
|
+
# Only the FIRST system message (the static SYSTEM_PROMPT) is a stable cache
|
|
395
|
+
# prefix. A trailing system message (the per-call date/time, appended at the
|
|
396
|
+
# end of the list) must NOT be cached: its content changes every call, so a
|
|
397
|
+
# breakpoint there is wasted and can invalidate the prefix. Restrict caching
|
|
398
|
+
# to the first system message only.
|
|
399
|
+
system_messages = system_messages[:1]
|
|
400
|
+
|
|
392
401
|
# Find last 2 non-system messages (recent context)
|
|
393
402
|
non_system_messages = [i for i, msg in enumerate(messages) if msg.get("role") != "system"]
|
|
394
403
|
last_two_indices = (
|
|
@@ -401,8 +410,10 @@ def _apply_prompt_caching(messages: List[Dict[str, Any]], model_id: str) -> List
|
|
|
401
410
|
# Build list of candidate indices to cache (prioritize system messages)
|
|
402
411
|
candidates = []
|
|
403
412
|
|
|
404
|
-
# Add system
|
|
405
|
-
|
|
413
|
+
# Add the (single, static) system message first. `system_messages` was
|
|
414
|
+
# already restricted to the first system message above; the trailing
|
|
415
|
+
# per-call datetime is deliberately excluded.
|
|
416
|
+
for idx in system_messages:
|
|
406
417
|
msg = messages[idx]
|
|
407
418
|
# Check if this message already has any cache markers
|
|
408
419
|
has_cache = False
|
|
@@ -1176,12 +1187,19 @@ Repository root takes priority if both exist."""
|
|
|
1176
1187
|
# Show thinking message
|
|
1177
1188
|
print("\033[2m🤔 Thinking...\033[0m", flush=True)
|
|
1178
1189
|
|
|
1179
|
-
# Prepare messages with system prompt and dynamic date/time
|
|
1180
|
-
#
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1190
|
+
# Prepare messages with system prompt and dynamic date/time.
|
|
1191
|
+
# The static SYSTEM_PROMPT is kept FIRST so it (plus the tools block)
|
|
1192
|
+
# forms a stable, byte-identical cacheable prefix across every call.
|
|
1193
|
+
# The volatile date/time is appended at the END instead of in a second
|
|
1194
|
+
# cached system block: placing a per-call-changing string inside the
|
|
1195
|
+
# cached prefix previously invalidated the prefix every call and wasted
|
|
1196
|
+
# a breakpoint (see COST_RECOMMENDATIONS_RANKED.md, Tier 2 fix).
|
|
1197
|
+
# Date/time is regenerated on each call to prevent staleness in long sessions.
|
|
1198
|
+
messages = (
|
|
1199
|
+
[{"role": "system", "content": SYSTEM_PROMPT}]
|
|
1200
|
+
+ self.messages
|
|
1201
|
+
+ [{"role": "system", "content": _get_current_datetime_message()}]
|
|
1202
|
+
)
|
|
1185
1203
|
|
|
1186
1204
|
# Filter images if BLOCK_IMAGES is enabled (for non-vision models or user preference)
|
|
1187
1205
|
messages = self.image_handler.filter_images_if_blocked(messages)
|
|
@@ -1268,6 +1286,24 @@ Repository root takes priority if both exist."""
|
|
|
1268
1286
|
):
|
|
1269
1287
|
self.cumulative_cache_read_tokens += response.usage.cache_read_input_tokens
|
|
1270
1288
|
|
|
1289
|
+
# Per-call cache instrumentation (Tier 2 #6): logs this call's
|
|
1290
|
+
# write/read split so the suffix-rewrite / datetime-breakpoint
|
|
1291
|
+
# hypotheses can be confirmed with data instead of inference.
|
|
1292
|
+
# Zero overhead unless PATCHPAL_DEBUG_CACHE=true.
|
|
1293
|
+
if os.environ.get("PATCHPAL_DEBUG_CACHE") == "true":
|
|
1294
|
+
_call_writes = (
|
|
1295
|
+
getattr(response.usage, "cache_creation_input_tokens", 0) or 0
|
|
1296
|
+
)
|
|
1297
|
+
_call_reads = getattr(response.usage, "cache_read_input_tokens", 0) or 0
|
|
1298
|
+
_call_prompt = getattr(response.usage, "prompt_tokens", 0) or 0
|
|
1299
|
+
_denom = _call_writes + _call_reads
|
|
1300
|
+
_hit = (_call_reads / _denom * 100) if _denom else 0.0
|
|
1301
|
+
print(
|
|
1302
|
+
f"[CACHE DEBUG] call #{self.total_llm_calls}: "
|
|
1303
|
+
f"prompt={_call_prompt} write={_call_writes} "
|
|
1304
|
+
f"read={_call_reads} this-call-hit={_hit:.1f}%"
|
|
1305
|
+
)
|
|
1306
|
+
|
|
1271
1307
|
# Track OpenAI cache tokens (prompt_tokens_details.cached_tokens)
|
|
1272
1308
|
# Only track for OpenAI models to avoid LiteLLM cross-contamination
|
|
1273
1309
|
if self._is_openai_model() and hasattr(response.usage, "prompt_tokens_details"):
|
|
@@ -645,6 +645,78 @@ def test_prompt_caching_idempotent():
|
|
|
645
645
|
assert len([k for k in cached_twice[1]["content"][0].keys() if "cache" in k]) == 1
|
|
646
646
|
|
|
647
647
|
|
|
648
|
+
def _has_cache_marker(message):
|
|
649
|
+
"""Return True if a message's content carries a cache_control/cachePoint marker."""
|
|
650
|
+
content = message.get("content")
|
|
651
|
+
if not isinstance(content, list):
|
|
652
|
+
return False
|
|
653
|
+
return any(
|
|
654
|
+
isinstance(block, dict) and ("cache_control" in block or "cachePoint" in block)
|
|
655
|
+
for block in content
|
|
656
|
+
)
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
def test_prompt_caching_excludes_trailing_datetime_system_message():
|
|
660
|
+
"""Regression test for the prompt-caching cost fix.
|
|
661
|
+
|
|
662
|
+
The volatile per-call date/time is appended as a TRAILING system message
|
|
663
|
+
(after the conversation), while the static SYSTEM_PROMPT is the FIRST
|
|
664
|
+
system message. Only the first system message forms a stable, cacheable
|
|
665
|
+
prefix; a breakpoint on the trailing date/time would change every call,
|
|
666
|
+
invalidating the cached prefix and wasting a (Bedrock-limited) breakpoint.
|
|
667
|
+
|
|
668
|
+
This test locks in that invariant: _apply_prompt_caching must mark the
|
|
669
|
+
first system message but must NOT mark a trailing system message.
|
|
670
|
+
See DATETIME_CACHE_PREFIX_FIX.md / COST_RECOMMENDATIONS_RANKED.md (Tier 2).
|
|
671
|
+
"""
|
|
672
|
+
from patchpal.agent.function_calling import _apply_prompt_caching
|
|
673
|
+
|
|
674
|
+
# Mirrors the message assembly in PatchPalAgent.run(): static SYSTEM_PROMPT
|
|
675
|
+
# first, conversation in the middle, volatile date/time appended last.
|
|
676
|
+
messages = [
|
|
677
|
+
{"role": "system", "content": "STATIC SYSTEM PROMPT (cacheable)"},
|
|
678
|
+
{"role": "user", "content": "Hello"},
|
|
679
|
+
{"role": "assistant", "content": "Hi there!"},
|
|
680
|
+
{"role": "user", "content": "How are you?"},
|
|
681
|
+
{"role": "system", "content": "## Current Date and Time\nToday is ..."},
|
|
682
|
+
]
|
|
683
|
+
|
|
684
|
+
cached = _apply_prompt_caching(messages.copy(), "anthropic/claude-sonnet-4-5")
|
|
685
|
+
|
|
686
|
+
# First (static) system message MUST be cached.
|
|
687
|
+
assert cached[0]["role"] == "system"
|
|
688
|
+
assert _has_cache_marker(cached[0]), "static SYSTEM_PROMPT should be cached"
|
|
689
|
+
|
|
690
|
+
# Trailing (volatile) date/time system message MUST NOT be cached.
|
|
691
|
+
assert cached[-1]["role"] == "system"
|
|
692
|
+
assert not _has_cache_marker(cached[-1]), (
|
|
693
|
+
"trailing per-call date/time must never receive a cache breakpoint"
|
|
694
|
+
)
|
|
695
|
+
|
|
696
|
+
# Exactly one system message should carry a marker (the static prefix).
|
|
697
|
+
cached_system_count = sum(
|
|
698
|
+
1 for m in cached if m.get("role") == "system" and _has_cache_marker(m)
|
|
699
|
+
)
|
|
700
|
+
assert cached_system_count == 1
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
def test_datetime_message_distinct_from_system_prompt():
|
|
704
|
+
"""The date/time message must be a separate string from SYSTEM_PROMPT.
|
|
705
|
+
|
|
706
|
+
The fix depends on keeping the volatile date/time OUT of the static
|
|
707
|
+
SYSTEM_PROMPT so the latter stays byte-identical (and cacheable) across
|
|
708
|
+
calls. This guards against a future refactor folding the date/time back
|
|
709
|
+
into the system prompt.
|
|
710
|
+
"""
|
|
711
|
+
from patchpal.agent.function_calling import SYSTEM_PROMPT, _get_current_datetime_message
|
|
712
|
+
|
|
713
|
+
datetime_msg = _get_current_datetime_message()
|
|
714
|
+
|
|
715
|
+
assert "Current Date and Time" in datetime_msg
|
|
716
|
+
# The static prompt must not embed the volatile date/time header.
|
|
717
|
+
assert "Current Date and Time" not in SYSTEM_PROMPT
|
|
718
|
+
|
|
719
|
+
|
|
648
720
|
def test_agent_handles_keyboard_interrupt_during_tool_execution(monkeypatch):
|
|
649
721
|
"""Test that agent properly cleans up conversation state when interrupted during tool execution."""
|
|
650
722
|
from patchpal.agent import create_agent
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|