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.
Files changed (101) hide show
  1. local_coding_agent/__init__.py +314 -0
  2. local_coding_agent/__main__.py +5 -0
  3. local_coding_agent/acp_server/__init__.py +38 -0
  4. local_coding_agent/acp_server/_codec.py +95 -0
  5. local_coding_agent/acp_server/_protocol.py +13 -0
  6. local_coding_agent/acp_server/_server.py +663 -0
  7. local_coding_agent/acp_server/_session.py +24 -0
  8. local_coding_agent/acp_server/_tools.py +126 -0
  9. local_coding_agent/ast_compactor.py +130 -0
  10. local_coding_agent/atomizer.py +77 -0
  11. local_coding_agent/benchmark/__init__.py +57 -0
  12. local_coding_agent/benchmark/_cases.py +307 -0
  13. local_coding_agent/benchmark/_judge.py +175 -0
  14. local_coding_agent/benchmark/_oracles.py +157 -0
  15. local_coding_agent/benchmark/_runner.py +259 -0
  16. local_coding_agent/benchmark/_stats.py +22 -0
  17. local_coding_agent/benchmark/_summary.py +143 -0
  18. local_coding_agent/benchmark_oracle_worker.py +169 -0
  19. local_coding_agent/calibration.py +154 -0
  20. local_coding_agent/capability.py +450 -0
  21. local_coding_agent/chat_compaction.py +276 -0
  22. local_coding_agent/cli/__init__.py +12 -0
  23. local_coding_agent/cli/_dispatch.py +377 -0
  24. local_coding_agent/cli/_handlers.py +320 -0
  25. local_coding_agent/cli/_handlers2.py +725 -0
  26. local_coding_agent/cli/_input.py +61 -0
  27. local_coding_agent/cli/_main.py +159 -0
  28. local_coding_agent/cli/_parser.py +327 -0
  29. local_coding_agent/context_manager.py +155 -0
  30. local_coding_agent/controller/__init__.py +18 -0
  31. local_coding_agent/controller/_constants.py +108 -0
  32. local_coding_agent/controller/_controller.py +1008 -0
  33. local_coding_agent/controller/_post_apply.py +216 -0
  34. local_coding_agent/delegator.py +180 -0
  35. local_coding_agent/desktop/__init__.py +8 -0
  36. local_coding_agent/desktop/app.py +100 -0
  37. local_coding_agent/desktop/assets/lucide.min.js +12 -0
  38. local_coding_agent/desktop/assets/tailwind.css +1 -0
  39. local_coding_agent/desktop/client_js.py +1271 -0
  40. local_coding_agent/desktop/components.py +555 -0
  41. local_coding_agent/desktop/server/__init__.py +37 -0
  42. local_coding_agent/desktop/server/_handlers.py +2178 -0
  43. local_coding_agent/desktop/server/_models.py +204 -0
  44. local_coding_agent/desktop/server/_server.py +347 -0
  45. local_coding_agent/desktop/server/_telemetry.py +87 -0
  46. local_coding_agent/desktop/styles.py +76 -0
  47. local_coding_agent/desktop/ui.py +53 -0
  48. local_coding_agent/doctor.py +413 -0
  49. local_coding_agent/hooks/__init__.py +19 -0
  50. local_coding_agent/hooks/_adapters.py +458 -0
  51. local_coding_agent/hooks/_bridge.py +346 -0
  52. local_coding_agent/hooks/_decision.py +26 -0
  53. local_coding_agent/lsp/__init__.py +60 -0
  54. local_coding_agent/lsp/_client.py +322 -0
  55. local_coding_agent/lsp/_fallback.py +297 -0
  56. local_coding_agent/lsp/_manager.py +173 -0
  57. local_coding_agent/lsp/_mock.py +200 -0
  58. local_coding_agent/lsp/_types.py +398 -0
  59. local_coding_agent/mcp_config.py +398 -0
  60. local_coding_agent/mcp_server.py +377 -0
  61. local_coding_agent/memory.py +140 -0
  62. local_coding_agent/mode_router.py +200 -0
  63. local_coding_agent/model_scanner.py +525 -0
  64. local_coding_agent/monitor.py +635 -0
  65. local_coding_agent/observation_policy.py +184 -0
  66. local_coding_agent/ollama_adapter.py +736 -0
  67. local_coding_agent/plan_mode.py +653 -0
  68. local_coding_agent/prescriptions.py +278 -0
  69. local_coding_agent/profiles.py +239 -0
  70. local_coding_agent/repository_tools.py +782 -0
  71. local_coding_agent/ripgrep.py +326 -0
  72. local_coding_agent/semantic_linter.py +122 -0
  73. local_coding_agent/service.py +405 -0
  74. local_coding_agent/session_events.py +684 -0
  75. local_coding_agent/session_query.py +578 -0
  76. local_coding_agent/skill_config.py +426 -0
  77. local_coding_agent/smoke.py +268 -0
  78. local_coding_agent/speculative_racing.py +85 -0
  79. local_coding_agent/spill.py +258 -0
  80. local_coding_agent/stats.py +255 -0
  81. local_coding_agent/stdio.py +132 -0
  82. local_coding_agent/subagent.py +516 -0
  83. local_coding_agent/task.py +90 -0
  84. local_coding_agent/task_store.py +142 -0
  85. local_coding_agent/tasks.py +240 -0
  86. local_coding_agent/terminal/__init__.py +35 -0
  87. local_coding_agent/terminal/_manager.py +156 -0
  88. local_coding_agent/terminal/_process.py +148 -0
  89. local_coding_agent/terminal/_session.py +452 -0
  90. local_coding_agent/terminal/_tools.py +233 -0
  91. local_coding_agent/terminal/_types.py +60 -0
  92. local_coding_agent/tool_call_parser.py +178 -0
  93. local_coding_agent/validators.py +563 -0
  94. local_coding_agent/vram_fit.py +240 -0
  95. local_coding_agent/worker_pool.py +516 -0
  96. local_coding_agent-1.1.0.dist-info/METADATA +202 -0
  97. local_coding_agent-1.1.0.dist-info/RECORD +101 -0
  98. local_coding_agent-1.1.0.dist-info/WHEEL +5 -0
  99. local_coding_agent-1.1.0.dist-info/entry_points.txt +3 -0
  100. local_coding_agent-1.1.0.dist-info/licenses/LICENSE +21 -0
  101. 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