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.
|
|
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
|
|
75
|
+
# Backend configuration
|
|
76
76
|
# -----------------------------------------------------------------------------------------------
|
|
77
|
-
WRENCODE_VERSION = "0.1.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 &
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 &
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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":
|
|
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
|
|
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
|
|
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 &
|
|
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
|
-
#
|
|
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
|
|
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
|
|
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
|
|
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
|
-
#
|
|
2281
|
+
# Entrypoint
|
|
2251
2282
|
# -----------------------------------------------------------------------------------------------
|
|
2252
2283
|
def print_help() -> None:
|
|
2253
2284
|
"""Print CLI usage."""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|