wrencode 0.1.4.4__tar.gz → 0.1.4.6__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.6
|
|
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
|
|
@@ -53,6 +53,19 @@ import urllib.error
|
|
|
53
53
|
import urllib.request
|
|
54
54
|
from typing import Any, Callable, Optional
|
|
55
55
|
|
|
56
|
+
# The PyInstaller single-file binary ships no system CA trust store, so urllib's
|
|
57
|
+
# TLS verification fails out of the box ("CERTIFICATE_VERIFY_FAILED") on a clean
|
|
58
|
+
# machine. If certifi is bundled (it is in the binary build), point the SSL
|
|
59
|
+
# defaults at it before any request. Soft import keeps pip/uvx installs
|
|
60
|
+
# dependency-free; setdefault preserves any user-provided override.
|
|
61
|
+
try:
|
|
62
|
+
import certifi
|
|
63
|
+
|
|
64
|
+
os.environ.setdefault("SSL_CERT_FILE", certifi.where())
|
|
65
|
+
os.environ.setdefault("REQUESTS_CA_BUNDLE", certifi.where())
|
|
66
|
+
except ImportError:
|
|
67
|
+
pass
|
|
68
|
+
|
|
56
69
|
# Load .env (next to this script, then the current directory); real env vars win.
|
|
57
70
|
for _dir in (os.path.dirname(os.path.abspath(__file__)), os.getcwd()):
|
|
58
71
|
_env_path = os.path.join(_dir, ".env")
|
|
@@ -72,9 +85,9 @@ for _dir in (os.path.dirname(os.path.abspath(__file__)), os.getcwd()):
|
|
|
72
85
|
os.environ.setdefault(_k.strip(), _v)
|
|
73
86
|
|
|
74
87
|
# -----------------------------------------------------------------------------------------------
|
|
75
|
-
# Backend
|
|
88
|
+
# Backend configuration
|
|
76
89
|
# -----------------------------------------------------------------------------------------------
|
|
77
|
-
WRENCODE_VERSION = "0.1.4.
|
|
90
|
+
WRENCODE_VERSION = "0.1.4.6"
|
|
78
91
|
|
|
79
92
|
# Per-backend defaults. "kind" controls how a backend is treated:
|
|
80
93
|
# api - hosted HTTP API, needs an API key
|
|
@@ -220,7 +233,7 @@ def apply_backend(backend: str, model: str = "", api_key: str = "") -> None:
|
|
|
220
233
|
|
|
221
234
|
|
|
222
235
|
# -----------------------------------------------------------------------------------------------
|
|
223
|
-
# Constants &
|
|
236
|
+
# Constants & environment variables
|
|
224
237
|
# -----------------------------------------------------------------------------------------------
|
|
225
238
|
MAX_TOKENS = int(os.environ.get("MAX_TOKENS", "8192"))
|
|
226
239
|
MAX_READ_BYTES = int(os.environ.get("MAX_READ_BYTES", str(4 * 1024 * 1024)))
|
|
@@ -239,7 +252,7 @@ _GLOB_SKIP: set[str] = {
|
|
|
239
252
|
}
|
|
240
253
|
|
|
241
254
|
# -----------------------------------------------------------------------------------------------
|
|
242
|
-
# Terminal
|
|
255
|
+
# Terminal colors
|
|
243
256
|
# -----------------------------------------------------------------------------------------------
|
|
244
257
|
RESET, BOLD, DIM = "\033[0m", "\033[1m", "\033[2m"
|
|
245
258
|
BLUE, CYAN, GREEN, YELLOW, RED = (
|
|
@@ -505,7 +518,7 @@ WREN_BANNER = f"""{BRIGHT_CYAN}\u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\
|
|
|
505
518
|
|
|
506
519
|
|
|
507
520
|
# -----------------------------------------------------------------------------------------------
|
|
508
|
-
# Path
|
|
521
|
+
# Path helpers
|
|
509
522
|
# -----------------------------------------------------------------------------------------------
|
|
510
523
|
def workspace_root() -> pathlib.Path:
|
|
511
524
|
"""Return the resolved workspace root path from env or cwd."""
|
|
@@ -533,7 +546,7 @@ def resolve_tool_path(raw: Any) -> pathlib.Path:
|
|
|
533
546
|
|
|
534
547
|
|
|
535
548
|
# -----------------------------------------------------------------------------------------------
|
|
536
|
-
# Input
|
|
549
|
+
# Input validation
|
|
537
550
|
# -----------------------------------------------------------------------------------------------
|
|
538
551
|
def _require_str(args: dict[str, Any], key: str) -> str:
|
|
539
552
|
"""Require a non-empty string value from args dict by key."""
|
|
@@ -1031,7 +1044,7 @@ def run_tool(name: str, args: dict[str, Any]) -> str:
|
|
|
1031
1044
|
|
|
1032
1045
|
|
|
1033
1046
|
# -----------------------------------------------------------------------------------------------
|
|
1034
|
-
# Message
|
|
1047
|
+
# Message formatting
|
|
1035
1048
|
# -----------------------------------------------------------------------------------------------
|
|
1036
1049
|
def flatten_content(content: Any) -> str:
|
|
1037
1050
|
"""Flatten Anthropic-style content list to plain string."""
|
|
@@ -1143,7 +1156,7 @@ def thinking_spinner() -> Any:
|
|
|
1143
1156
|
|
|
1144
1157
|
|
|
1145
1158
|
# -----------------------------------------------------------------------------------------------
|
|
1146
|
-
# Token &
|
|
1159
|
+
# Token & output cleaning
|
|
1147
1160
|
# -----------------------------------------------------------------------------------------------
|
|
1148
1161
|
def strip_gptoss_tokens(text: str) -> str:
|
|
1149
1162
|
"""Strip GPT-OSS special tokens and channel markers from model output."""
|
|
@@ -1244,7 +1257,7 @@ def parse_tool_calls(text: str) -> list[dict[str, Any]]:
|
|
|
1244
1257
|
|
|
1245
1258
|
|
|
1246
1259
|
# -----------------------------------------------------------------------------------------------
|
|
1247
|
-
# Anthropic
|
|
1260
|
+
# Anthropic native tools
|
|
1248
1261
|
# -----------------------------------------------------------------------------------------------
|
|
1249
1262
|
_TYPE_MAP: dict[str, str] = {
|
|
1250
1263
|
"string": "string",
|
|
@@ -1300,9 +1313,32 @@ def _warn_if_truncated(data: dict[str, Any]) -> None:
|
|
|
1300
1313
|
)
|
|
1301
1314
|
|
|
1302
1315
|
|
|
1316
|
+
def _log_usage_debug(data: dict[str, Any]) -> None:
|
|
1317
|
+
"""When WRENCODE_DEBUG=1, print one stderr line per turn with token usage.
|
|
1318
|
+
Surfaces cache_creation_input_tokens / cache_read_input_tokens so prompt-
|
|
1319
|
+
cache hit rate is observable without external tooling.
|
|
1320
|
+
"""
|
|
1321
|
+
if not os.environ.get("WRENCODE_DEBUG"):
|
|
1322
|
+
return
|
|
1323
|
+
u = data.get("usage", {})
|
|
1324
|
+
if BACKEND == "anthropic":
|
|
1325
|
+
print(
|
|
1326
|
+
f"{DIM}usage: in={u.get('input_tokens')} out={u.get('output_tokens')} "
|
|
1327
|
+
f"cache_write={u.get('cache_creation_input_tokens', 0)} "
|
|
1328
|
+
f"cache_read={u.get('cache_read_input_tokens', 0)}{RESET}",
|
|
1329
|
+
file=sys.stderr,
|
|
1330
|
+
)
|
|
1331
|
+
else:
|
|
1332
|
+
print(
|
|
1333
|
+
f"{DIM}usage: in={u.get('prompt_tokens')} out={u.get('completion_tokens')}{RESET}",
|
|
1334
|
+
file=sys.stderr,
|
|
1335
|
+
)
|
|
1336
|
+
|
|
1337
|
+
|
|
1303
1338
|
def _parse_native_response(data: dict[str, Any]) -> tuple[str, list["ToolCall"]]:
|
|
1304
1339
|
"""Parse a native (Anthropic / OpenAI) API response into display text and tool calls."""
|
|
1305
1340
|
_warn_if_truncated(data)
|
|
1341
|
+
_log_usage_debug(data)
|
|
1306
1342
|
if BACKEND == "anthropic":
|
|
1307
1343
|
blocks = data.get("content", [])
|
|
1308
1344
|
text = "\n".join(b["text"] for b in blocks if b.get("type") == "text").strip()
|
|
@@ -1386,7 +1422,7 @@ def _append_tool_results(
|
|
|
1386
1422
|
|
|
1387
1423
|
|
|
1388
1424
|
# -----------------------------------------------------------------------------------------------
|
|
1389
|
-
# HTTP
|
|
1425
|
+
# HTTP helper
|
|
1390
1426
|
# -----------------------------------------------------------------------------------------------
|
|
1391
1427
|
def _http_post(
|
|
1392
1428
|
url: str, payload: dict[str, Any], headers: dict[str, str]
|
|
@@ -1415,7 +1451,7 @@ def get_response(
|
|
|
1415
1451
|
for m in messages
|
|
1416
1452
|
]
|
|
1417
1453
|
|
|
1418
|
-
# OpenAI
|
|
1454
|
+
# OpenAI - native function calling
|
|
1419
1455
|
if BACKEND == "openai":
|
|
1420
1456
|
data = _http_post(
|
|
1421
1457
|
API_BASE,
|
|
@@ -1432,7 +1468,7 @@ def get_response(
|
|
|
1432
1468
|
)
|
|
1433
1469
|
return json.dumps(data) # return raw for agent loop to parse natively
|
|
1434
1470
|
|
|
1435
|
-
# OpenRouter / Ollama
|
|
1471
|
+
# OpenRouter / Ollama - OpenAI-compatible chat completions (no native tools)
|
|
1436
1472
|
if BACKEND in {"openrouter", "ollama"}:
|
|
1437
1473
|
data = _http_post(
|
|
1438
1474
|
API_BASE,
|
|
@@ -1447,22 +1483,30 @@ def get_response(
|
|
|
1447
1483
|
)
|
|
1448
1484
|
return str(data["choices"][0]["message"]["content"])
|
|
1449
1485
|
|
|
1450
|
-
# Anthropic
|
|
1486
|
+
# Anthropic - native tool use API
|
|
1451
1487
|
if BACKEND == "anthropic":
|
|
1488
|
+
# Prompt caching: mark the (large, static) system block + tool schemas as
|
|
1489
|
+
# ephemeral. Anthropic caches everything up to and including each marker
|
|
1490
|
+
# for ~5 minutes; subsequent turns in the same session read at ~10% of
|
|
1491
|
+
# normal input cost. Two breakpoints out of the four allowed per request.
|
|
1492
|
+
tools = _build_tool_schemas("anthropic")
|
|
1493
|
+
if tools:
|
|
1494
|
+
tools[-1] = {**tools[-1], "cache_control": {"type": "ephemeral"}}
|
|
1452
1495
|
data = _http_post(
|
|
1453
1496
|
API_BASE,
|
|
1454
1497
|
{
|
|
1455
1498
|
"model": MODEL,
|
|
1456
|
-
"system": system_prompt,
|
|
1499
|
+
"system": [{"type": "text", "text": system_prompt,
|
|
1500
|
+
"cache_control": {"type": "ephemeral"}}],
|
|
1457
1501
|
"messages": messages,
|
|
1458
1502
|
"max_tokens": MAX_TOKENS,
|
|
1459
|
-
"tools":
|
|
1503
|
+
"tools": tools,
|
|
1460
1504
|
},
|
|
1461
1505
|
_anthropic_headers(),
|
|
1462
1506
|
)
|
|
1463
1507
|
return json.dumps(data) # return raw for agent loop to parse natively
|
|
1464
1508
|
|
|
1465
|
-
# Local proxy
|
|
1509
|
+
# Local proxy - Anthropic messages API; tool calls returned as XML <tool_call> tags in text
|
|
1466
1510
|
if BACKEND == "local":
|
|
1467
1511
|
data = _http_post(
|
|
1468
1512
|
API_BASE,
|
|
@@ -1534,7 +1578,7 @@ def get_response(
|
|
|
1534
1578
|
|
|
1535
1579
|
|
|
1536
1580
|
# -----------------------------------------------------------------------------------------------
|
|
1537
|
-
# History
|
|
1581
|
+
# History management
|
|
1538
1582
|
# -----------------------------------------------------------------------------------------------
|
|
1539
1583
|
def history_file_path() -> pathlib.Path:
|
|
1540
1584
|
"""Return the history file path from env override or user-level default."""
|
|
@@ -1634,7 +1678,7 @@ def compact_messages(
|
|
|
1634
1678
|
|
|
1635
1679
|
|
|
1636
1680
|
# -----------------------------------------------------------------------------------------------
|
|
1637
|
-
# Workspace &
|
|
1681
|
+
# Workspace & system prompt
|
|
1638
1682
|
# -----------------------------------------------------------------------------------------------
|
|
1639
1683
|
def git_context() -> str:
|
|
1640
1684
|
"""Return a formatted git status string if inside a git repository."""
|
|
@@ -1686,7 +1730,7 @@ CRITICAL: You MUST use tools for file operations. Never say you can't access fil
|
|
|
1686
1730
|
|
|
1687
1731
|
|
|
1688
1732
|
# -----------------------------------------------------------------------------------------------
|
|
1689
|
-
#
|
|
1733
|
+
# Agentic loop
|
|
1690
1734
|
# -----------------------------------------------------------------------------------------------
|
|
1691
1735
|
def _track_error(
|
|
1692
1736
|
result: str, last: Optional[str], count: int
|
|
@@ -1755,7 +1799,7 @@ def run_agent_turn(
|
|
|
1755
1799
|
|
|
1756
1800
|
|
|
1757
1801
|
# -----------------------------------------------------------------------------------------------
|
|
1758
|
-
# Slash
|
|
1802
|
+
# Slash commands
|
|
1759
1803
|
# -----------------------------------------------------------------------------------------------
|
|
1760
1804
|
def handle_slash_command(
|
|
1761
1805
|
cmd: str,
|
|
@@ -1806,7 +1850,7 @@ def handle_slash_command(
|
|
|
1806
1850
|
|
|
1807
1851
|
|
|
1808
1852
|
# -----------------------------------------------------------------------------------------------
|
|
1809
|
-
# Backend
|
|
1853
|
+
# Backend selection & config persistence
|
|
1810
1854
|
# -----------------------------------------------------------------------------------------------
|
|
1811
1855
|
def is_frozen() -> bool:
|
|
1812
1856
|
"""Set true when running as a PyInstaller standalone binary."""
|
|
@@ -2159,7 +2203,7 @@ def resolve_configuration(force_chooser: bool = False) -> None:
|
|
|
2159
2203
|
|
|
2160
2204
|
|
|
2161
2205
|
# -----------------------------------------------------------------------------------------------
|
|
2162
|
-
# Model
|
|
2206
|
+
# Model loading
|
|
2163
2207
|
# -----------------------------------------------------------------------------------------------
|
|
2164
2208
|
def load_model() -> Optional[tuple[Any, Any]]:
|
|
2165
2209
|
"""Load model for the current backend and return mlx_state (or None for API backends)."""
|
|
@@ -2247,7 +2291,7 @@ def load_model() -> Optional[tuple[Any, Any]]:
|
|
|
2247
2291
|
|
|
2248
2292
|
|
|
2249
2293
|
# -----------------------------------------------------------------------------------------------
|
|
2250
|
-
#
|
|
2294
|
+
# Entrypoint
|
|
2251
2295
|
# -----------------------------------------------------------------------------------------------
|
|
2252
2296
|
def print_help() -> None:
|
|
2253
2297
|
"""Print CLI usage."""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|