wrencode 0.1.4.4__tar.gz → 0.1.4.5__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wrencode
3
- Version: 0.1.4.4
3
+ Version: 0.1.4.5
4
4
  Summary: A minimal agentic coding assistant in a single Python file
5
5
  Project-URL: Homepage, https://github.com/almostly/wrencode
6
6
  Project-URL: Repository, https://github.com/almostly/wrencode
@@ -72,9 +72,9 @@ for _dir in (os.path.dirname(os.path.abspath(__file__)), os.getcwd()):
72
72
  os.environ.setdefault(_k.strip(), _v)
73
73
 
74
74
  # -----------------------------------------------------------------------------------------------
75
- # Backend Configuration
75
+ # Backend configuration
76
76
  # -----------------------------------------------------------------------------------------------
77
- WRENCODE_VERSION = "0.1.4.4"
77
+ WRENCODE_VERSION = "0.1.4.5"
78
78
 
79
79
  # Per-backend defaults. "kind" controls how a backend is treated:
80
80
  # api - hosted HTTP API, needs an API key
@@ -220,7 +220,7 @@ def apply_backend(backend: str, model: str = "", api_key: str = "") -> None:
220
220
 
221
221
 
222
222
  # -----------------------------------------------------------------------------------------------
223
- # Constants & Environment Variables
223
+ # Constants & environment variables
224
224
  # -----------------------------------------------------------------------------------------------
225
225
  MAX_TOKENS = int(os.environ.get("MAX_TOKENS", "8192"))
226
226
  MAX_READ_BYTES = int(os.environ.get("MAX_READ_BYTES", str(4 * 1024 * 1024)))
@@ -239,7 +239,7 @@ _GLOB_SKIP: set[str] = {
239
239
  }
240
240
 
241
241
  # -----------------------------------------------------------------------------------------------
242
- # Terminal Colors
242
+ # Terminal colors
243
243
  # -----------------------------------------------------------------------------------------------
244
244
  RESET, BOLD, DIM = "\033[0m", "\033[1m", "\033[2m"
245
245
  BLUE, CYAN, GREEN, YELLOW, RED = (
@@ -505,7 +505,7 @@ WREN_BANNER = f"""{BRIGHT_CYAN}\u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\
505
505
 
506
506
 
507
507
  # -----------------------------------------------------------------------------------------------
508
- # Path Helpers
508
+ # Path helpers
509
509
  # -----------------------------------------------------------------------------------------------
510
510
  def workspace_root() -> pathlib.Path:
511
511
  """Return the resolved workspace root path from env or cwd."""
@@ -533,7 +533,7 @@ def resolve_tool_path(raw: Any) -> pathlib.Path:
533
533
 
534
534
 
535
535
  # -----------------------------------------------------------------------------------------------
536
- # Input Validation
536
+ # Input validation
537
537
  # -----------------------------------------------------------------------------------------------
538
538
  def _require_str(args: dict[str, Any], key: str) -> str:
539
539
  """Require a non-empty string value from args dict by key."""
@@ -1031,7 +1031,7 @@ def run_tool(name: str, args: dict[str, Any]) -> str:
1031
1031
 
1032
1032
 
1033
1033
  # -----------------------------------------------------------------------------------------------
1034
- # Message Formatting
1034
+ # Message formatting
1035
1035
  # -----------------------------------------------------------------------------------------------
1036
1036
  def flatten_content(content: Any) -> str:
1037
1037
  """Flatten Anthropic-style content list to plain string."""
@@ -1143,7 +1143,7 @@ def thinking_spinner() -> Any:
1143
1143
 
1144
1144
 
1145
1145
  # -----------------------------------------------------------------------------------------------
1146
- # Token & Output Cleaning
1146
+ # Token & output cleaning
1147
1147
  # -----------------------------------------------------------------------------------------------
1148
1148
  def strip_gptoss_tokens(text: str) -> str:
1149
1149
  """Strip GPT-OSS special tokens and channel markers from model output."""
@@ -1244,7 +1244,7 @@ def parse_tool_calls(text: str) -> list[dict[str, Any]]:
1244
1244
 
1245
1245
 
1246
1246
  # -----------------------------------------------------------------------------------------------
1247
- # Anthropic Native Tools
1247
+ # Anthropic native tools
1248
1248
  # -----------------------------------------------------------------------------------------------
1249
1249
  _TYPE_MAP: dict[str, str] = {
1250
1250
  "string": "string",
@@ -1300,9 +1300,32 @@ def _warn_if_truncated(data: dict[str, Any]) -> None:
1300
1300
  )
1301
1301
 
1302
1302
 
1303
+ def _log_usage_debug(data: dict[str, Any]) -> None:
1304
+ """When WRENCODE_DEBUG=1, print one stderr line per turn with token usage.
1305
+ Surfaces cache_creation_input_tokens / cache_read_input_tokens so prompt-
1306
+ cache hit rate is observable without external tooling.
1307
+ """
1308
+ if not os.environ.get("WRENCODE_DEBUG"):
1309
+ return
1310
+ u = data.get("usage", {})
1311
+ if BACKEND == "anthropic":
1312
+ print(
1313
+ f"{DIM}usage: in={u.get('input_tokens')} out={u.get('output_tokens')} "
1314
+ f"cache_write={u.get('cache_creation_input_tokens', 0)} "
1315
+ f"cache_read={u.get('cache_read_input_tokens', 0)}{RESET}",
1316
+ file=sys.stderr,
1317
+ )
1318
+ else:
1319
+ print(
1320
+ f"{DIM}usage: in={u.get('prompt_tokens')} out={u.get('completion_tokens')}{RESET}",
1321
+ file=sys.stderr,
1322
+ )
1323
+
1324
+
1303
1325
  def _parse_native_response(data: dict[str, Any]) -> tuple[str, list["ToolCall"]]:
1304
1326
  """Parse a native (Anthropic / OpenAI) API response into display text and tool calls."""
1305
1327
  _warn_if_truncated(data)
1328
+ _log_usage_debug(data)
1306
1329
  if BACKEND == "anthropic":
1307
1330
  blocks = data.get("content", [])
1308
1331
  text = "\n".join(b["text"] for b in blocks if b.get("type") == "text").strip()
@@ -1386,7 +1409,7 @@ def _append_tool_results(
1386
1409
 
1387
1410
 
1388
1411
  # -----------------------------------------------------------------------------------------------
1389
- # HTTP Helper
1412
+ # HTTP helper
1390
1413
  # -----------------------------------------------------------------------------------------------
1391
1414
  def _http_post(
1392
1415
  url: str, payload: dict[str, Any], headers: dict[str, str]
@@ -1415,7 +1438,7 @@ def get_response(
1415
1438
  for m in messages
1416
1439
  ]
1417
1440
 
1418
- # OpenAI — native function calling
1441
+ # OpenAI - native function calling
1419
1442
  if BACKEND == "openai":
1420
1443
  data = _http_post(
1421
1444
  API_BASE,
@@ -1432,7 +1455,7 @@ def get_response(
1432
1455
  )
1433
1456
  return json.dumps(data) # return raw for agent loop to parse natively
1434
1457
 
1435
- # OpenRouter / Ollama — OpenAI-compatible chat completions (no native tools)
1458
+ # OpenRouter / Ollama - OpenAI-compatible chat completions (no native tools)
1436
1459
  if BACKEND in {"openrouter", "ollama"}:
1437
1460
  data = _http_post(
1438
1461
  API_BASE,
@@ -1447,22 +1470,30 @@ def get_response(
1447
1470
  )
1448
1471
  return str(data["choices"][0]["message"]["content"])
1449
1472
 
1450
- # Anthropic — native tool use API
1473
+ # Anthropic - native tool use API
1451
1474
  if BACKEND == "anthropic":
1475
+ # Prompt caching: mark the (large, static) system block + tool schemas as
1476
+ # ephemeral. Anthropic caches everything up to and including each marker
1477
+ # for ~5 minutes; subsequent turns in the same session read at ~10% of
1478
+ # normal input cost. Two breakpoints out of the four allowed per request.
1479
+ tools = _build_tool_schemas("anthropic")
1480
+ if tools:
1481
+ tools[-1] = {**tools[-1], "cache_control": {"type": "ephemeral"}}
1452
1482
  data = _http_post(
1453
1483
  API_BASE,
1454
1484
  {
1455
1485
  "model": MODEL,
1456
- "system": system_prompt,
1486
+ "system": [{"type": "text", "text": system_prompt,
1487
+ "cache_control": {"type": "ephemeral"}}],
1457
1488
  "messages": messages,
1458
1489
  "max_tokens": MAX_TOKENS,
1459
- "tools": _build_tool_schemas("anthropic"),
1490
+ "tools": tools,
1460
1491
  },
1461
1492
  _anthropic_headers(),
1462
1493
  )
1463
1494
  return json.dumps(data) # return raw for agent loop to parse natively
1464
1495
 
1465
- # Local proxy — Anthropic messages API; tool calls returned as XML <tool_call> tags in text
1496
+ # Local proxy - Anthropic messages API; tool calls returned as XML <tool_call> tags in text
1466
1497
  if BACKEND == "local":
1467
1498
  data = _http_post(
1468
1499
  API_BASE,
@@ -1534,7 +1565,7 @@ def get_response(
1534
1565
 
1535
1566
 
1536
1567
  # -----------------------------------------------------------------------------------------------
1537
- # History Management
1568
+ # History management
1538
1569
  # -----------------------------------------------------------------------------------------------
1539
1570
  def history_file_path() -> pathlib.Path:
1540
1571
  """Return the history file path from env override or user-level default."""
@@ -1634,7 +1665,7 @@ def compact_messages(
1634
1665
 
1635
1666
 
1636
1667
  # -----------------------------------------------------------------------------------------------
1637
- # Workspace & System Prompt
1668
+ # Workspace & system prompt
1638
1669
  # -----------------------------------------------------------------------------------------------
1639
1670
  def git_context() -> str:
1640
1671
  """Return a formatted git status string if inside a git repository."""
@@ -1686,7 +1717,7 @@ CRITICAL: You MUST use tools for file operations. Never say you can't access fil
1686
1717
 
1687
1718
 
1688
1719
  # -----------------------------------------------------------------------------------------------
1689
- # Agent Loop
1720
+ # Agentic loop
1690
1721
  # -----------------------------------------------------------------------------------------------
1691
1722
  def _track_error(
1692
1723
  result: str, last: Optional[str], count: int
@@ -1755,7 +1786,7 @@ def run_agent_turn(
1755
1786
 
1756
1787
 
1757
1788
  # -----------------------------------------------------------------------------------------------
1758
- # Slash Commands
1789
+ # Slash commands
1759
1790
  # -----------------------------------------------------------------------------------------------
1760
1791
  def handle_slash_command(
1761
1792
  cmd: str,
@@ -1806,7 +1837,7 @@ def handle_slash_command(
1806
1837
 
1807
1838
 
1808
1839
  # -----------------------------------------------------------------------------------------------
1809
- # Backend Selection & Config Persistence
1840
+ # Backend selection & config persistence
1810
1841
  # -----------------------------------------------------------------------------------------------
1811
1842
  def is_frozen() -> bool:
1812
1843
  """Set true when running as a PyInstaller standalone binary."""
@@ -2159,7 +2190,7 @@ def resolve_configuration(force_chooser: bool = False) -> None:
2159
2190
 
2160
2191
 
2161
2192
  # -----------------------------------------------------------------------------------------------
2162
- # Model Loading
2193
+ # Model loading
2163
2194
  # -----------------------------------------------------------------------------------------------
2164
2195
  def load_model() -> Optional[tuple[Any, Any]]:
2165
2196
  """Load model for the current backend and return mlx_state (or None for API backends)."""
@@ -2247,7 +2278,7 @@ def load_model() -> Optional[tuple[Any, Any]]:
2247
2278
 
2248
2279
 
2249
2280
  # -----------------------------------------------------------------------------------------------
2250
- # Entry Point
2281
+ # Entrypoint
2251
2282
  # -----------------------------------------------------------------------------------------------
2252
2283
  def print_help() -> None:
2253
2284
  """Print CLI usage."""
File without changes
File without changes
File without changes