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,320 @@
|
|
|
1
|
+
"""Subcommand dispatch for the local-coding-agent CLI (part 1)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from ..atomizer import TaskBudget, decompose, preflight
|
|
13
|
+
from ..calibration import calibrate_for_model
|
|
14
|
+
from ..controller import Controller, mediated_apply
|
|
15
|
+
from ..delegator import BY_FILES, PER_FILE
|
|
16
|
+
from ..memory import ModelMemoryManager
|
|
17
|
+
from ..ollama_adapter import BACKEND_OFFLINE_HINT, OllamaError, build_client, classify_backend_error
|
|
18
|
+
from ..profiles import get_profile, list_profiles
|
|
19
|
+
from ..stats import append_stats, default_stats_path
|
|
20
|
+
from ..task import TaskEnvelope
|
|
21
|
+
|
|
22
|
+
from ._input import load_task_input
|
|
23
|
+
|
|
24
|
+
_BACKEND_HINTS = {
|
|
25
|
+
"backend_offline": BACKEND_OFFLINE_HINT,
|
|
26
|
+
"backend_error": "The model backend returned an error. Check backend logs or verify the model is installed (`ollama list`).",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _annotate_backend_error(result: dict) -> None:
|
|
31
|
+
error = result.get("error")
|
|
32
|
+
if isinstance(error, dict):
|
|
33
|
+
hint = _BACKEND_HINTS.get(error.get("kind"))
|
|
34
|
+
if hint:
|
|
35
|
+
error["hint"] = hint
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _persist_proposal(result: dict, task_id: str) -> None:
|
|
39
|
+
"""Write an accepted/candidate patch to disk so it survives the process."""
|
|
40
|
+
patch = result.get("patch")
|
|
41
|
+
if not isinstance(patch, str) or not patch.strip():
|
|
42
|
+
return
|
|
43
|
+
safe_id = "".join(c if c.isalnum() or c in "._-" else "_" for c in (task_id or "proposal"))[:80]
|
|
44
|
+
try:
|
|
45
|
+
proposals_dir = Path(".local-run") / "proposals"
|
|
46
|
+
proposals_dir.mkdir(parents=True, exist_ok=True)
|
|
47
|
+
proposal_path = proposals_dir / f"{safe_id}.patch"
|
|
48
|
+
proposal_path.write_text(patch, encoding="utf-8")
|
|
49
|
+
result["patch_file"] = str(proposal_path)
|
|
50
|
+
except OSError:
|
|
51
|
+
pass # ponytail: persistence is best-effort; never fail a finished run.
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _handle_delegate(args: argparse.Namespace) -> int:
|
|
55
|
+
started_ns = time.monotonic_ns()
|
|
56
|
+
try:
|
|
57
|
+
task = load_task_input(args.task, getattr(args, "task_file", None))
|
|
58
|
+
if getattr(args, "speculative_drafts", 1) > 1:
|
|
59
|
+
if args.apply:
|
|
60
|
+
# Each racer runs against the same checkout. Applying here
|
|
61
|
+
# would let losing drafts mutate files before a winner is
|
|
62
|
+
# selected; speculative mode is proposal-only until a single
|
|
63
|
+
# result is chosen and explicitly mediated elsewhere.
|
|
64
|
+
print(
|
|
65
|
+
json.dumps(
|
|
66
|
+
{
|
|
67
|
+
"status": "rejected",
|
|
68
|
+
"error": {
|
|
69
|
+
"kind": "speculative_apply_unsupported",
|
|
70
|
+
"message": "--apply cannot be combined with --speculative-drafts; speculative drafts are proposal-only",
|
|
71
|
+
},
|
|
72
|
+
"applied": False,
|
|
73
|
+
},
|
|
74
|
+
ensure_ascii=False,
|
|
75
|
+
indent=2,
|
|
76
|
+
)
|
|
77
|
+
)
|
|
78
|
+
return 1
|
|
79
|
+
from threading import Event
|
|
80
|
+
from ..speculative_racing import SpeculativeRacer
|
|
81
|
+
|
|
82
|
+
def _make_runner(draft_idx: int):
|
|
83
|
+
def _run(cancel_ev: Event) -> dict[str, Any]:
|
|
84
|
+
overrides: dict[str, Any] = {}
|
|
85
|
+
if args.endpoint:
|
|
86
|
+
overrides["endpoint"] = args.endpoint
|
|
87
|
+
if args.num_ctx is not None:
|
|
88
|
+
overrides["num_ctx"] = args.num_ctx
|
|
89
|
+
if getattr(args, "model", None):
|
|
90
|
+
overrides["model"] = args.model
|
|
91
|
+
temp = 0.0 if draft_idx == 0 else min(0.15 * draft_idx, 0.7)
|
|
92
|
+
overrides["temperature"] = temp
|
|
93
|
+
prof = get_profile(args.profile, **overrides)
|
|
94
|
+
cl = build_client(prof)
|
|
95
|
+
return Controller(
|
|
96
|
+
cl,
|
|
97
|
+
args.workspace,
|
|
98
|
+
max_turns=args.max_turns,
|
|
99
|
+
cancel_event=cancel_ev,
|
|
100
|
+
).run(task, apply=False)
|
|
101
|
+
|
|
102
|
+
return _run
|
|
103
|
+
|
|
104
|
+
runners = [_make_runner(i) for i in range(args.speculative_drafts)]
|
|
105
|
+
racer = SpeculativeRacer()
|
|
106
|
+
result = racer.run(runners)
|
|
107
|
+
else:
|
|
108
|
+
overrides = {}
|
|
109
|
+
if args.endpoint:
|
|
110
|
+
overrides["endpoint"] = args.endpoint
|
|
111
|
+
if args.num_ctx is not None:
|
|
112
|
+
overrides["num_ctx"] = args.num_ctx
|
|
113
|
+
if getattr(args, "model", None):
|
|
114
|
+
overrides["model"] = args.model
|
|
115
|
+
profile = get_profile(args.profile, **overrides)
|
|
116
|
+
client = build_client(profile)
|
|
117
|
+
result = Controller(
|
|
118
|
+
client,
|
|
119
|
+
args.workspace,
|
|
120
|
+
max_turns=args.max_turns,
|
|
121
|
+
).run(task, apply=args.apply)
|
|
122
|
+
except OllamaError as error:
|
|
123
|
+
# Normalize to the same vocabulary the controller model boundary uses.
|
|
124
|
+
kind = {
|
|
125
|
+
"offline": "backend_offline",
|
|
126
|
+
"server_error": "backend_error",
|
|
127
|
+
}.get(classify_backend_error(error) or "", "backend_error")
|
|
128
|
+
payload: dict[str, Any] = {
|
|
129
|
+
"status": "failed",
|
|
130
|
+
"error": {
|
|
131
|
+
"kind": kind,
|
|
132
|
+
"message": str(error),
|
|
133
|
+
},
|
|
134
|
+
}
|
|
135
|
+
if kind == "offline":
|
|
136
|
+
payload["error"]["hint"] = BACKEND_OFFLINE_HINT
|
|
137
|
+
append_stats(
|
|
138
|
+
default_stats_path(),
|
|
139
|
+
{"status": "failed", "error": {"kind": kind}},
|
|
140
|
+
model=getattr(args, "model", None) or args.profile,
|
|
141
|
+
)
|
|
142
|
+
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
143
|
+
return 2
|
|
144
|
+
except (OSError, UnicodeError, json.JSONDecodeError, ValueError) as error:
|
|
145
|
+
print(json.dumps({"status": "failed", "error": {"kind": "input", "message": str(error)}}, ensure_ascii=False, indent=2))
|
|
146
|
+
return 2
|
|
147
|
+
append_stats(
|
|
148
|
+
default_stats_path(),
|
|
149
|
+
result,
|
|
150
|
+
model=getattr(args, "model", None) or args.profile,
|
|
151
|
+
latency_ns=time.monotonic_ns() - started_ns,
|
|
152
|
+
)
|
|
153
|
+
_annotate_backend_error(result)
|
|
154
|
+
_persist_proposal(result, task.id)
|
|
155
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
156
|
+
return 0 if result.get("status") == "accepted" else 1
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _handle_decompose(args: argparse.Namespace) -> int:
|
|
160
|
+
try:
|
|
161
|
+
task = load_task_input(args.task, getattr(args, "task_file", None))
|
|
162
|
+
budget = TaskBudget(
|
|
163
|
+
max_files=args.budget_files,
|
|
164
|
+
max_context_bytes=args.budget_bytes,
|
|
165
|
+
max_checks=args.budget_checks,
|
|
166
|
+
)
|
|
167
|
+
pre = preflight(task, budget)
|
|
168
|
+
if args.strategy == "per_file":
|
|
169
|
+
children = PER_FILE.split(task, budget)
|
|
170
|
+
else:
|
|
171
|
+
children = BY_FILES.split(task, budget)
|
|
172
|
+
payload = {
|
|
173
|
+
"task_id": task.id,
|
|
174
|
+
"preflight": {
|
|
175
|
+
"accepted": pre.accepted,
|
|
176
|
+
"reason": pre.reason,
|
|
177
|
+
"issues": list(pre.issues),
|
|
178
|
+
},
|
|
179
|
+
"strategy": args.strategy,
|
|
180
|
+
"count": len(children),
|
|
181
|
+
"children": [c.as_dict() for c in children],
|
|
182
|
+
}
|
|
183
|
+
except (OSError, UnicodeError, json.JSONDecodeError, ValueError) as error:
|
|
184
|
+
print(json.dumps({"status": "failed", "error": {"kind": "input", "message": str(error)}}, ensure_ascii=False, indent=2))
|
|
185
|
+
return 2
|
|
186
|
+
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
187
|
+
return 0 if payload["children"] else 1
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _handle_profiles(args: argparse.Namespace) -> int:
|
|
191
|
+
action = getattr(args, "profile_action", "list") or "list"
|
|
192
|
+
if action == "get":
|
|
193
|
+
if not args.name:
|
|
194
|
+
print(json.dumps({"error": "profile name required for 'get'"}, ensure_ascii=False))
|
|
195
|
+
return 2
|
|
196
|
+
try:
|
|
197
|
+
prof = get_profile(args.name)
|
|
198
|
+
print(json.dumps(prof.__dict__, ensure_ascii=False, indent=2))
|
|
199
|
+
return 0
|
|
200
|
+
except ValueError as error:
|
|
201
|
+
print(json.dumps({"error": str(error)}, ensure_ascii=False))
|
|
202
|
+
return 1
|
|
203
|
+
|
|
204
|
+
# list
|
|
205
|
+
profile_names = list_profiles()
|
|
206
|
+
installed_map: dict[str, bool] = {}
|
|
207
|
+
if args.check_ollama:
|
|
208
|
+
try:
|
|
209
|
+
c = build_client(get_profile("qwen2.5-1.5b", endpoint=args.endpoint))
|
|
210
|
+
tags = c.available_models().get("models", [])
|
|
211
|
+
tag_names = {t.get("name") for t in tags if isinstance(t, dict) and "name" in t}
|
|
212
|
+
for p_name in profile_names:
|
|
213
|
+
p_obj = get_profile(p_name)
|
|
214
|
+
installed_map[p_name] = p_obj.model in tag_names
|
|
215
|
+
except Exception:
|
|
216
|
+
pass
|
|
217
|
+
|
|
218
|
+
items = []
|
|
219
|
+
for p_name in profile_names:
|
|
220
|
+
p_obj = get_profile(p_name)
|
|
221
|
+
item = {
|
|
222
|
+
"name": p_name,
|
|
223
|
+
"model": p_obj.model,
|
|
224
|
+
"provider": getattr(p_obj, "provider", "ollama"),
|
|
225
|
+
"num_ctx": p_obj.num_ctx,
|
|
226
|
+
"num_predict": p_obj.num_predict,
|
|
227
|
+
"max_context_length": p_obj.max_context_length,
|
|
228
|
+
"policy_context_cap": p_obj.max_context_length,
|
|
229
|
+
"model_context_limit": p_obj.model_context_limit,
|
|
230
|
+
"think": p_obj.think,
|
|
231
|
+
}
|
|
232
|
+
if args.check_ollama:
|
|
233
|
+
item["installed_locally"] = installed_map.get(p_name, False)
|
|
234
|
+
items.append(item)
|
|
235
|
+
|
|
236
|
+
if args.json:
|
|
237
|
+
print(json.dumps({"profiles": items}, ensure_ascii=False, indent=2))
|
|
238
|
+
else:
|
|
239
|
+
print(f"{'PROFILE':<25} {'MODEL':<35} {'CTX':<8} {'THINK':<6}")
|
|
240
|
+
print("-" * 76)
|
|
241
|
+
for it in items:
|
|
242
|
+
print(f"{it['name']:<25} {it['model']:<35} {it['num_ctx']:<8} {str(it['think']):<6}")
|
|
243
|
+
return 0
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _handle_memory(args: argparse.Namespace) -> int:
|
|
247
|
+
action = getattr(args, "memory_action", "status")
|
|
248
|
+
try:
|
|
249
|
+
profile = get_profile(args.profile, endpoint=args.endpoint)
|
|
250
|
+
client = build_client(profile)
|
|
251
|
+
manager = ModelMemoryManager(client)
|
|
252
|
+
if action == "unload":
|
|
253
|
+
if not args.model:
|
|
254
|
+
print(json.dumps({"error": "model name required for 'unload'"}, ensure_ascii=False))
|
|
255
|
+
return 2
|
|
256
|
+
snapshot = manager.unload_model(args.model)
|
|
257
|
+
elif action == "unload-all":
|
|
258
|
+
snapshot = manager.unload_all()
|
|
259
|
+
elif action == "enforce":
|
|
260
|
+
if args.limit is None:
|
|
261
|
+
print(json.dumps({"error": "--limit required for 'enforce'"}, ensure_ascii=False))
|
|
262
|
+
return 2
|
|
263
|
+
snapshot = manager.enforce_limit(args.limit, keep=tuple(args.keep or []))
|
|
264
|
+
else:
|
|
265
|
+
snapshot = manager.snapshot()
|
|
266
|
+
res = {"status": "memory_reconciled", "action": action, "memory": snapshot.as_dict()}
|
|
267
|
+
print(json.dumps(res, ensure_ascii=False, indent=2))
|
|
268
|
+
return 0
|
|
269
|
+
except Exception as error:
|
|
270
|
+
print(json.dumps({"status": "failed", "error": str(error)}, ensure_ascii=False, indent=2))
|
|
271
|
+
return 1
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _handle_calibrate(args: argparse.Namespace) -> int:
|
|
275
|
+
try:
|
|
276
|
+
overrides = {}
|
|
277
|
+
if args.endpoint:
|
|
278
|
+
overrides["endpoint"] = args.endpoint
|
|
279
|
+
profile = get_profile(args.profile, **overrides)
|
|
280
|
+
client = build_client(profile)
|
|
281
|
+
report = calibrate_for_model(
|
|
282
|
+
client,
|
|
283
|
+
profile.model,
|
|
284
|
+
vram_budget_bytes=args.vram_bytes,
|
|
285
|
+
per_worker_context_bytes=args.parallel_context_bytes,
|
|
286
|
+
)
|
|
287
|
+
print(json.dumps({"status": "calibrated", "report": report}, ensure_ascii=False, indent=2))
|
|
288
|
+
return 0
|
|
289
|
+
except Exception as error:
|
|
290
|
+
print(json.dumps({"status": "failed", "error": str(error)}, ensure_ascii=False, indent=2))
|
|
291
|
+
return 1
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _handle_apply(args: argparse.Namespace) -> int:
|
|
295
|
+
if args.patch_file:
|
|
296
|
+
patch = Path(args.patch_file).read_text(encoding="utf-8-sig")
|
|
297
|
+
elif args.patch:
|
|
298
|
+
patch = args.patch
|
|
299
|
+
else:
|
|
300
|
+
print(json.dumps({"error": "--patch or --patch-file is required"}, ensure_ascii=False))
|
|
301
|
+
return 2
|
|
302
|
+
|
|
303
|
+
workspace = Path(args.workspace).resolve()
|
|
304
|
+
try:
|
|
305
|
+
# TaskEnvelope owns the policy checks (non-empty, workspace-relative
|
|
306
|
+
# scope), so a caller mistake surfaces as one JSON error, not a traceback.
|
|
307
|
+
task = TaskEnvelope(
|
|
308
|
+
id="cli-apply",
|
|
309
|
+
goal="apply reviewed patch",
|
|
310
|
+
files=tuple(args.files),
|
|
311
|
+
checks=tuple(args.checks),
|
|
312
|
+
)
|
|
313
|
+
except ValueError as error:
|
|
314
|
+
res = {"status": "rejected", "error": {"kind": "apply_policy", "message": str(error)}, "applied": False}
|
|
315
|
+
print(json.dumps(res, ensure_ascii=False, indent=2))
|
|
316
|
+
return 1
|
|
317
|
+
|
|
318
|
+
result = mediated_apply(workspace, patch, task)
|
|
319
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
320
|
+
return 0 if result.get("applied") is True else 1
|