local-coding-agent 1.1.0__py3-none-any.whl
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.
- local_coding_agent/__init__.py +314 -0
- local_coding_agent/__main__.py +5 -0
- local_coding_agent/acp_server/__init__.py +38 -0
- local_coding_agent/acp_server/_codec.py +95 -0
- local_coding_agent/acp_server/_protocol.py +13 -0
- local_coding_agent/acp_server/_server.py +663 -0
- local_coding_agent/acp_server/_session.py +24 -0
- local_coding_agent/acp_server/_tools.py +126 -0
- local_coding_agent/ast_compactor.py +130 -0
- local_coding_agent/atomizer.py +77 -0
- local_coding_agent/benchmark/__init__.py +57 -0
- local_coding_agent/benchmark/_cases.py +307 -0
- local_coding_agent/benchmark/_judge.py +175 -0
- local_coding_agent/benchmark/_oracles.py +157 -0
- local_coding_agent/benchmark/_runner.py +259 -0
- local_coding_agent/benchmark/_stats.py +22 -0
- local_coding_agent/benchmark/_summary.py +143 -0
- local_coding_agent/benchmark_oracle_worker.py +169 -0
- local_coding_agent/calibration.py +154 -0
- local_coding_agent/capability.py +450 -0
- local_coding_agent/chat_compaction.py +276 -0
- local_coding_agent/cli/__init__.py +12 -0
- local_coding_agent/cli/_dispatch.py +377 -0
- local_coding_agent/cli/_handlers.py +320 -0
- local_coding_agent/cli/_handlers2.py +725 -0
- local_coding_agent/cli/_input.py +61 -0
- local_coding_agent/cli/_main.py +159 -0
- local_coding_agent/cli/_parser.py +327 -0
- local_coding_agent/context_manager.py +155 -0
- local_coding_agent/controller/__init__.py +18 -0
- local_coding_agent/controller/_constants.py +108 -0
- local_coding_agent/controller/_controller.py +1008 -0
- local_coding_agent/controller/_post_apply.py +216 -0
- local_coding_agent/delegator.py +180 -0
- local_coding_agent/desktop/__init__.py +8 -0
- local_coding_agent/desktop/app.py +100 -0
- local_coding_agent/desktop/assets/lucide.min.js +12 -0
- local_coding_agent/desktop/assets/tailwind.css +1 -0
- local_coding_agent/desktop/client_js.py +1271 -0
- local_coding_agent/desktop/components.py +555 -0
- local_coding_agent/desktop/server/__init__.py +37 -0
- local_coding_agent/desktop/server/_handlers.py +2178 -0
- local_coding_agent/desktop/server/_models.py +204 -0
- local_coding_agent/desktop/server/_server.py +347 -0
- local_coding_agent/desktop/server/_telemetry.py +87 -0
- local_coding_agent/desktop/styles.py +76 -0
- local_coding_agent/desktop/ui.py +53 -0
- local_coding_agent/doctor.py +413 -0
- local_coding_agent/hooks/__init__.py +19 -0
- local_coding_agent/hooks/_adapters.py +458 -0
- local_coding_agent/hooks/_bridge.py +346 -0
- local_coding_agent/hooks/_decision.py +26 -0
- local_coding_agent/lsp/__init__.py +60 -0
- local_coding_agent/lsp/_client.py +322 -0
- local_coding_agent/lsp/_fallback.py +297 -0
- local_coding_agent/lsp/_manager.py +173 -0
- local_coding_agent/lsp/_mock.py +200 -0
- local_coding_agent/lsp/_types.py +398 -0
- local_coding_agent/mcp_config.py +398 -0
- local_coding_agent/mcp_server.py +377 -0
- local_coding_agent/memory.py +140 -0
- local_coding_agent/mode_router.py +200 -0
- local_coding_agent/model_scanner.py +525 -0
- local_coding_agent/monitor.py +635 -0
- local_coding_agent/observation_policy.py +184 -0
- local_coding_agent/ollama_adapter.py +736 -0
- local_coding_agent/plan_mode.py +653 -0
- local_coding_agent/prescriptions.py +278 -0
- local_coding_agent/profiles.py +239 -0
- local_coding_agent/repository_tools.py +782 -0
- local_coding_agent/ripgrep.py +326 -0
- local_coding_agent/semantic_linter.py +122 -0
- local_coding_agent/service.py +405 -0
- local_coding_agent/session_events.py +684 -0
- local_coding_agent/session_query.py +578 -0
- local_coding_agent/skill_config.py +426 -0
- local_coding_agent/smoke.py +268 -0
- local_coding_agent/speculative_racing.py +85 -0
- local_coding_agent/spill.py +258 -0
- local_coding_agent/stats.py +255 -0
- local_coding_agent/stdio.py +132 -0
- local_coding_agent/subagent.py +516 -0
- local_coding_agent/task.py +90 -0
- local_coding_agent/task_store.py +142 -0
- local_coding_agent/tasks.py +240 -0
- local_coding_agent/terminal/__init__.py +35 -0
- local_coding_agent/terminal/_manager.py +156 -0
- local_coding_agent/terminal/_process.py +148 -0
- local_coding_agent/terminal/_session.py +452 -0
- local_coding_agent/terminal/_tools.py +233 -0
- local_coding_agent/terminal/_types.py +60 -0
- local_coding_agent/tool_call_parser.py +178 -0
- local_coding_agent/validators.py +563 -0
- local_coding_agent/vram_fit.py +240 -0
- local_coding_agent/worker_pool.py +516 -0
- local_coding_agent-1.1.0.dist-info/METADATA +202 -0
- local_coding_agent-1.1.0.dist-info/RECORD +101 -0
- local_coding_agent-1.1.0.dist-info/WHEEL +5 -0
- local_coding_agent-1.1.0.dist-info/entry_points.txt +3 -0
- local_coding_agent-1.1.0.dist-info/licenses/LICENSE +21 -0
- local_coding_agent-1.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""GGUF header parsing and VRAM-aware context-window fitting.
|
|
2
|
+
|
|
3
|
+
The desktop harness uses these helpers to pick ``llama-server -c`` from the
|
|
4
|
+
model's own metadata instead of guessing 8192: parse the GGUF header for the
|
|
5
|
+
KV-cache geometry, then compute the largest context whose KV cache fits the
|
|
6
|
+
free VRAM left next to the weights.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import struct
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from typing import Literal
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"ContextFit",
|
|
18
|
+
"fit_context",
|
|
19
|
+
"kv_bytes_per_token",
|
|
20
|
+
"max_fitting_ctx",
|
|
21
|
+
"read_gguf_ctx_params",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
_CTX_ALIGN = 256
|
|
25
|
+
_DEFAULT_CAP = 32768
|
|
26
|
+
_MAX_KEY_BYTES = 1 << 20 # real GGUF keys are <100B; anything larger is corrupt
|
|
27
|
+
_CHUNK = 1 << 20
|
|
28
|
+
|
|
29
|
+
# GGUF container (all little-endian):
|
|
30
|
+
# b"GGUF" | u32 version (2 or 3) | u64 tensor_count | u64 kv_count |
|
|
31
|
+
# kv_count records of: u64 key_len | key utf-8 | u32 value_type | value
|
|
32
|
+
#
|
|
33
|
+
# value_type codes:
|
|
34
|
+
# 0 u8, 1 i8, 2 u16, 3 i16, 4 u32, 5 i32, 6 f32, 7 bool(1B),
|
|
35
|
+
# 8 string (u64 len + bytes),
|
|
36
|
+
# 9 array (u32 elem_type + u64 count + elems),
|
|
37
|
+
# 10 u64, 11 i64, 12 f64
|
|
38
|
+
_SCALAR_FMTS = {
|
|
39
|
+
0: "<B", 1: "<b", 2: "<H", 3: "<h", 4: "<I", 5: "<i",
|
|
40
|
+
6: "<f", 7: "<?", 10: "<Q", 11: "<q", 12: "<d",
|
|
41
|
+
}
|
|
42
|
+
_STRING_TYPE = 8
|
|
43
|
+
_ARRAY_TYPE = 9
|
|
44
|
+
_INT_TYPES = frozenset(_SCALAR_FMTS) - {6, 12}
|
|
45
|
+
|
|
46
|
+
_RESULT_KEYS = ("n_layers", "n_head_kv", "head_dim", "native_context_length")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True)
|
|
50
|
+
class ContextFit:
|
|
51
|
+
"""Evidence-backed result of a KV-cache context fit calculation.
|
|
52
|
+
|
|
53
|
+
``fits`` means that at least ``min_ctx`` tokens fit and ``context`` is the
|
|
54
|
+
largest aligned context that fits. ``does_not_fit`` means the available
|
|
55
|
+
resources are known but cannot hold the requested minimum. ``unknown``
|
|
56
|
+
means the geometry or telemetry is not valid enough to make that claim.
|
|
57
|
+
A numeric floor is deliberately never returned for the latter two states.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
status: Literal["fits", "does_not_fit", "unknown"]
|
|
61
|
+
context: int | None = None
|
|
62
|
+
reason: str | None = None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def read_gguf_ctx_params(gguf_path: str | os.PathLike) -> dict[str, int | None]:
|
|
66
|
+
"""Header-only parse of a GGUF file; never raises.
|
|
67
|
+
|
|
68
|
+
Returns dict with keys ``n_layers``, ``n_head_kv``, ``head_dim``,
|
|
69
|
+
``native_context_length``; each value is None when the key is absent or
|
|
70
|
+
the file is missing/unreadable/corrupt.
|
|
71
|
+
"""
|
|
72
|
+
try:
|
|
73
|
+
return _parse_header(gguf_path)
|
|
74
|
+
except Exception:
|
|
75
|
+
return dict.fromkeys(_RESULT_KEYS)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def kv_bytes_per_token(n_layers: int, n_kv_heads: int, head_dim: int, bytes_per_elem: int = 2) -> int:
|
|
79
|
+
"""K+V cache bytes per token: 2 * n_layers * n_kv_heads * head_dim * bytes_per_elem."""
|
|
80
|
+
return 2 * n_layers * n_kv_heads * head_dim * bytes_per_elem
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def max_fitting_ctx(
|
|
84
|
+
free_vram_bytes: int,
|
|
85
|
+
weights_bytes: int,
|
|
86
|
+
kv_per_token: int,
|
|
87
|
+
*,
|
|
88
|
+
min_ctx: int = 512,
|
|
89
|
+
max_ctx: int | None = None,
|
|
90
|
+
reserve_fraction: float = 0.15,
|
|
91
|
+
) -> int | None:
|
|
92
|
+
"""Compatibility wrapper returning a context only when the fit is proven.
|
|
93
|
+
|
|
94
|
+
``None`` means either that the minimum context cannot fit or that the
|
|
95
|
+
inputs are insufficient to make a safe calculation. Callers that need to
|
|
96
|
+
distinguish those cases should use :func:`fit_context` directly.
|
|
97
|
+
"""
|
|
98
|
+
result = fit_context(
|
|
99
|
+
free_vram_bytes,
|
|
100
|
+
weights_bytes,
|
|
101
|
+
kv_per_token,
|
|
102
|
+
min_ctx=min_ctx,
|
|
103
|
+
max_ctx=max_ctx,
|
|
104
|
+
reserve_fraction=reserve_fraction,
|
|
105
|
+
)
|
|
106
|
+
return result.context if result.status == "fits" else None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def fit_context(
|
|
110
|
+
free_vram_bytes: int,
|
|
111
|
+
weights_bytes: int,
|
|
112
|
+
kv_per_token: int,
|
|
113
|
+
*,
|
|
114
|
+
min_ctx: int = 512,
|
|
115
|
+
max_ctx: int | None = None,
|
|
116
|
+
reserve_fraction: float = 0.15,
|
|
117
|
+
) -> ContextFit:
|
|
118
|
+
"""Calculate the largest context with an explicit evidence status.
|
|
119
|
+
|
|
120
|
+
``usable = free_vram_bytes * (1 - reserve_fraction) - weights_bytes``.
|
|
121
|
+
A malformed KV geometry is ``unknown``; known insufficient capacity is
|
|
122
|
+
``does_not_fit``. Contexts are rounded down to a 256-token boundary, but
|
|
123
|
+
that alignment must not turn an under-sized result into a positive fit.
|
|
124
|
+
"""
|
|
125
|
+
if not 0 <= reserve_fraction < 1:
|
|
126
|
+
raise ValueError("reserve_fraction must satisfy 0 <= rf < 1")
|
|
127
|
+
if min_ctx <= 0:
|
|
128
|
+
raise ValueError("min_ctx must be positive")
|
|
129
|
+
if kv_per_token <= 0:
|
|
130
|
+
return ContextFit("unknown", reason="KV-cache geometry is unavailable or invalid")
|
|
131
|
+
cap = _DEFAULT_CAP if max_ctx is None else max_ctx
|
|
132
|
+
if cap <= 0:
|
|
133
|
+
raise ValueError("max_ctx must be positive")
|
|
134
|
+
if free_vram_bytes < 0 or weights_bytes < 0:
|
|
135
|
+
return ContextFit("unknown", reason="VRAM or model weight size is invalid")
|
|
136
|
+
if cap < min_ctx:
|
|
137
|
+
return ContextFit("does_not_fit", reason="Context cap is below the minimum launch context")
|
|
138
|
+
usable = free_vram_bytes * (1 - reserve_fraction) - weights_bytes
|
|
139
|
+
if usable <= 0:
|
|
140
|
+
return ContextFit("does_not_fit", reason="No VRAM remains for the KV cache")
|
|
141
|
+
raw_ctx = min(int(usable) // kv_per_token, cap)
|
|
142
|
+
if raw_ctx < min_ctx:
|
|
143
|
+
return ContextFit("does_not_fit", reason="Available VRAM cannot hold the minimum context")
|
|
144
|
+
ctx = raw_ctx - raw_ctx % _CTX_ALIGN
|
|
145
|
+
return ContextFit("fits", max(ctx, min_ctx))
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _read_exact(fh, n: int) -> bytes:
|
|
149
|
+
data = fh.read(n)
|
|
150
|
+
if len(data) != n:
|
|
151
|
+
raise ValueError("truncated gguf header")
|
|
152
|
+
return data
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _skip(fh, n: int) -> None:
|
|
156
|
+
while n > 0:
|
|
157
|
+
chunk = fh.read(min(n, _CHUNK))
|
|
158
|
+
if not chunk:
|
|
159
|
+
raise ValueError("truncated gguf header")
|
|
160
|
+
n -= len(chunk)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _read_key(fh) -> str:
|
|
164
|
+
klen = struct.unpack("<Q", _read_exact(fh, 8))[0]
|
|
165
|
+
if klen > _MAX_KEY_BYTES:
|
|
166
|
+
raise ValueError("implausible gguf key length")
|
|
167
|
+
parts = []
|
|
168
|
+
while klen > 0:
|
|
169
|
+
chunk = fh.read(min(klen, _CHUNK))
|
|
170
|
+
if not chunk:
|
|
171
|
+
raise ValueError("truncated gguf header")
|
|
172
|
+
parts.append(chunk)
|
|
173
|
+
klen -= len(chunk)
|
|
174
|
+
return b"".join(parts).decode("utf-8", errors="replace")
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _consume_value(fh, vtype: int) -> None:
|
|
178
|
+
fmt = _SCALAR_FMTS.get(vtype)
|
|
179
|
+
if fmt is not None:
|
|
180
|
+
_read_exact(fh, struct.calcsize(fmt))
|
|
181
|
+
elif vtype == _STRING_TYPE:
|
|
182
|
+
slen = struct.unpack("<Q", _read_exact(fh, 8))[0]
|
|
183
|
+
_skip(fh, slen)
|
|
184
|
+
elif vtype == _ARRAY_TYPE:
|
|
185
|
+
elem_type = struct.unpack("<I", _read_exact(fh, 4))[0]
|
|
186
|
+
count = struct.unpack("<Q", _read_exact(fh, 8))[0]
|
|
187
|
+
elem_fmt = _SCALAR_FMTS.get(elem_type)
|
|
188
|
+
if elem_fmt is not None:
|
|
189
|
+
_skip(fh, count * struct.calcsize(elem_fmt))
|
|
190
|
+
else:
|
|
191
|
+
for _ in range(count):
|
|
192
|
+
_consume_value(fh, elem_type)
|
|
193
|
+
else:
|
|
194
|
+
raise ValueError(f"unknown gguf value type {vtype}")
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _maybe_int(fh, vtype: int) -> int | None:
|
|
198
|
+
if vtype not in _INT_TYPES:
|
|
199
|
+
_consume_value(fh, vtype)
|
|
200
|
+
return None
|
|
201
|
+
value = struct.unpack(_SCALAR_FMTS[vtype], _read_exact(fh, struct.calcsize(_SCALAR_FMTS[vtype])))[0]
|
|
202
|
+
return value if isinstance(value, int) and value > 0 else None
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _parse_header(gguf_path: str | os.PathLike) -> dict[str, int | None]:
|
|
206
|
+
out: dict[str, int | None] = dict.fromkeys(_RESULT_KEYS)
|
|
207
|
+
emb_len: int | None = None
|
|
208
|
+
head_count: int | None = None
|
|
209
|
+
with open(gguf_path, "rb") as fh:
|
|
210
|
+
if fh.read(4) != b"GGUF":
|
|
211
|
+
raise ValueError("bad gguf magic")
|
|
212
|
+
version = struct.unpack("<I", _read_exact(fh, 4))[0]
|
|
213
|
+
if version not in (2, 3):
|
|
214
|
+
raise ValueError(f"unsupported gguf version {version}")
|
|
215
|
+
_read_exact(fh, 8) # tensor_count, unused here
|
|
216
|
+
kv_count = struct.unpack("<Q", _read_exact(fh, 8))[0]
|
|
217
|
+
for _ in range(kv_count):
|
|
218
|
+
key = _read_key(fh)
|
|
219
|
+
vtype = struct.unpack("<I", _read_exact(fh, 4))[0]
|
|
220
|
+
# arch prefix varies (llama., qwen2., phi3., ...); match by suffix
|
|
221
|
+
if key.endswith(".context_length"):
|
|
222
|
+
out["native_context_length"] = _maybe_int(fh, vtype)
|
|
223
|
+
elif key.endswith(".block_count"):
|
|
224
|
+
out["n_layers"] = _maybe_int(fh, vtype)
|
|
225
|
+
elif key.endswith(".attention.head_count_kv"):
|
|
226
|
+
out["n_head_kv"] = _maybe_int(fh, vtype)
|
|
227
|
+
elif key.endswith(".attention.key_length"):
|
|
228
|
+
out["head_dim"] = _maybe_int(fh, vtype)
|
|
229
|
+
elif key.endswith(".embedding_length"):
|
|
230
|
+
emb_len = _maybe_int(fh, vtype)
|
|
231
|
+
elif key.endswith(".attention.head_count"):
|
|
232
|
+
head_count = _maybe_int(fh, vtype)
|
|
233
|
+
else:
|
|
234
|
+
_consume_value(fh, vtype)
|
|
235
|
+
# Parse every KV record. GGUF metadata is not ordered: an explicit
|
|
236
|
+
# attention.key_length may follow the values needed to derive a
|
|
237
|
+
# provisional head dimension, and must still override that value.
|
|
238
|
+
if out["head_dim"] is None and emb_len and head_count:
|
|
239
|
+
out["head_dim"] = emb_len // head_count
|
|
240
|
+
return out
|