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,635 @@
|
|
|
1
|
+
"""Lightweight stdlib-only HTTP monitoring server and HTML dashboard.
|
|
2
|
+
|
|
3
|
+
Provides real-time visibility into delegation performance, worker pool load,
|
|
4
|
+
queue depth, latency distributions, and task outcomes.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import html
|
|
10
|
+
import json
|
|
11
|
+
import threading
|
|
12
|
+
import time
|
|
13
|
+
from http import HTTPStatus
|
|
14
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
15
|
+
from typing import Any
|
|
16
|
+
from urllib.parse import urlparse
|
|
17
|
+
|
|
18
|
+
from .stats import DelegationStats, append_stats, load_stats, merge_stats_snapshots
|
|
19
|
+
from .worker_pool import BoundedWorkerPool
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MonitorRequestHandler(BaseHTTPRequestHandler):
|
|
23
|
+
"""Handler for HTTP metrics and live dashboard."""
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def monitor(self) -> Any:
|
|
27
|
+
return getattr(self.server, "monitor_server", self.server)
|
|
28
|
+
|
|
29
|
+
def do_GET(self) -> None:
|
|
30
|
+
parsed = urlparse(self.path)
|
|
31
|
+
path = parsed.path.rstrip("/")
|
|
32
|
+
|
|
33
|
+
if path in {"", "/dashboard"}:
|
|
34
|
+
self._handle_dashboard()
|
|
35
|
+
elif path == "/workbench":
|
|
36
|
+
self._handle_workbench()
|
|
37
|
+
elif path == "/health":
|
|
38
|
+
self._handle_health()
|
|
39
|
+
elif path == "/stats":
|
|
40
|
+
self._handle_stats()
|
|
41
|
+
elif path == "/tasks":
|
|
42
|
+
self._handle_tasks()
|
|
43
|
+
elif path in {"/api/events", "/events"}:
|
|
44
|
+
self._handle_events()
|
|
45
|
+
else:
|
|
46
|
+
self._send_response(HTTPStatus.NOT_FOUND, "text/plain; charset=utf-8", b"404 Not Found\n")
|
|
47
|
+
|
|
48
|
+
def do_POST(self) -> None:
|
|
49
|
+
parsed = urlparse(self.path)
|
|
50
|
+
path = parsed.path.rstrip("/")
|
|
51
|
+
if path in {"/api/delegate", "/delegate"}:
|
|
52
|
+
self._handle_api_delegate()
|
|
53
|
+
else:
|
|
54
|
+
self._send_response(HTTPStatus.NOT_FOUND, "text/plain; charset=utf-8", b"404 Not Found\n")
|
|
55
|
+
|
|
56
|
+
def _handle_api_delegate(self) -> None:
|
|
57
|
+
content_length = int(self.headers.get("Content-Length", 0))
|
|
58
|
+
body = self.rfile.read(content_length).decode("utf-8") if content_length > 0 else "{}"
|
|
59
|
+
try:
|
|
60
|
+
data = json.loads(body)
|
|
61
|
+
raw_task = data.get("task", {})
|
|
62
|
+
profile_name = data.get("profile", "qwen2.5-1.5b")
|
|
63
|
+
apply_flag = bool(data.get("apply", False))
|
|
64
|
+
|
|
65
|
+
from pathlib import Path
|
|
66
|
+
from .controller import Controller
|
|
67
|
+
from .ollama_adapter import build_client
|
|
68
|
+
from .profiles import get_profile
|
|
69
|
+
from .task import TaskEnvelope
|
|
70
|
+
|
|
71
|
+
task = TaskEnvelope.from_mapping(raw_task)
|
|
72
|
+
profile = get_profile(profile_name)
|
|
73
|
+
client = build_client(profile)
|
|
74
|
+
controller = Controller(client, str(Path.cwd()))
|
|
75
|
+
started_ns = time.monotonic_ns()
|
|
76
|
+
result = controller.run(task, apply=apply_flag)
|
|
77
|
+
if getattr(self.monitor, "stats_path", None):
|
|
78
|
+
append_stats(
|
|
79
|
+
self.monitor.stats_path,
|
|
80
|
+
result,
|
|
81
|
+
model=profile_name,
|
|
82
|
+
latency_ns=time.monotonic_ns() - started_ns,
|
|
83
|
+
)
|
|
84
|
+
self._send_json(result)
|
|
85
|
+
except Exception as error:
|
|
86
|
+
self._send_json({"status": "failed", "error": str(error)})
|
|
87
|
+
|
|
88
|
+
def _handle_events(self) -> None:
|
|
89
|
+
events = self.monitor.get_recent_events()
|
|
90
|
+
payload = {
|
|
91
|
+
"uptime_seconds": round(time.monotonic() - self.monitor.started_at, 2),
|
|
92
|
+
"events": events,
|
|
93
|
+
"count": len(events),
|
|
94
|
+
}
|
|
95
|
+
self._send_json(payload)
|
|
96
|
+
|
|
97
|
+
def _handle_health(self) -> None:
|
|
98
|
+
payload = {
|
|
99
|
+
"status": "ok",
|
|
100
|
+
"uptime_seconds": round(time.monotonic() - self.monitor.started_at, 2),
|
|
101
|
+
}
|
|
102
|
+
self._send_json(payload)
|
|
103
|
+
|
|
104
|
+
def _stats_snapshot(self) -> dict[str, Any]:
|
|
105
|
+
"""Aggregate in-process stats with the cross-process JSONL journal."""
|
|
106
|
+
if self.monitor.stats is None:
|
|
107
|
+
return {}
|
|
108
|
+
snapshot = self.monitor.stats.snapshot()
|
|
109
|
+
stats_path = getattr(self.monitor, "stats_path", None)
|
|
110
|
+
if stats_path:
|
|
111
|
+
snapshot = merge_stats_snapshots(load_stats(stats_path).snapshot(), snapshot)
|
|
112
|
+
return snapshot
|
|
113
|
+
|
|
114
|
+
def _handle_stats(self) -> None:
|
|
115
|
+
pool_data = self.monitor.worker_pool.status() if self.monitor.worker_pool is not None else {}
|
|
116
|
+
payload = {
|
|
117
|
+
"uptime_seconds": round(time.monotonic() - self.monitor.started_at, 2),
|
|
118
|
+
"stats": self._stats_snapshot(),
|
|
119
|
+
"worker_pool": pool_data,
|
|
120
|
+
}
|
|
121
|
+
self._send_json(payload)
|
|
122
|
+
|
|
123
|
+
def _handle_tasks(self) -> None:
|
|
124
|
+
pool_data = self.monitor.worker_pool.status() if self.monitor.worker_pool is not None else {}
|
|
125
|
+
payload = {
|
|
126
|
+
"jobs": pool_data.get("jobs", []),
|
|
127
|
+
"total": pool_data.get("total_jobs", 0),
|
|
128
|
+
"queued": pool_data.get("queued_jobs", 0),
|
|
129
|
+
"active": pool_data.get("active_workers", 0),
|
|
130
|
+
}
|
|
131
|
+
self._send_json(payload)
|
|
132
|
+
|
|
133
|
+
def _handle_dashboard(self) -> None:
|
|
134
|
+
stats_data = self._stats_snapshot()
|
|
135
|
+
pool_data = self.monitor.worker_pool.status() if self.monitor.worker_pool is not None else {}
|
|
136
|
+
dashboard_html = self._render_dashboard_html(stats_data, pool_data)
|
|
137
|
+
self._send_response(HTTPStatus.OK, "text/html; charset=utf-8", dashboard_html.encode("utf-8"))
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _render_dashboard_html(self, stats: dict[str, Any], pool: dict[str, Any]) -> str:
|
|
141
|
+
total = stats.get("total", 0)
|
|
142
|
+
by_status = stats.get("by_status", {})
|
|
143
|
+
accepted = by_status.get("accepted", 0)
|
|
144
|
+
rejected = by_status.get("rejected", 0)
|
|
145
|
+
failed = by_status.get("failed", 0)
|
|
146
|
+
escaped = by_status.get("escalated", 0)
|
|
147
|
+
success_rate = f"{(accepted / total * 100):.1f}%" if total > 0 else "N/A"
|
|
148
|
+
|
|
149
|
+
active_workers = pool.get("active_workers", 0)
|
|
150
|
+
max_workers = pool.get("max_workers", 1)
|
|
151
|
+
queued_jobs = pool.get("queued_jobs", 0)
|
|
152
|
+
max_queue = pool.get("max_queue", 16)
|
|
153
|
+
stopping = pool.get("stopping", False)
|
|
154
|
+
pool_status = "Stopping" if stopping else ("Busy" if active_workers >= max_workers else "Healthy")
|
|
155
|
+
|
|
156
|
+
latency = stats.get("latency", {})
|
|
157
|
+
avg_latency = f"{latency.get('avg_ms', 0):.1f} ms" if latency.get("avg_ms") is not None else "N/A"
|
|
158
|
+
min_latency = f"{latency.get('min_ms', 0):.1f} ms" if latency.get("min_ms") is not None else "N/A"
|
|
159
|
+
max_latency = f"{latency.get('max_ms', 0):.1f} ms" if latency.get("max_ms") is not None else "N/A"
|
|
160
|
+
|
|
161
|
+
model_calls = stats.get("model_calls", 0)
|
|
162
|
+
tool_calls = stats.get("tool_calls", 0)
|
|
163
|
+
|
|
164
|
+
jobs = pool.get("jobs", [])
|
|
165
|
+
jobs_rows = ""
|
|
166
|
+
for job in jobs[-15:]:
|
|
167
|
+
state = html.escape(str(job.get("state", "")))
|
|
168
|
+
badge_class = "badge-success" if state == "completed" else ("badge-warning" if state == "running" else "badge-info")
|
|
169
|
+
jobs_rows += f"""
|
|
170
|
+
<tr>
|
|
171
|
+
<td class="mono">{html.escape(str(job.get('job_id', '')))}</td>
|
|
172
|
+
<td>{html.escape(str(job.get('caller_id', '')))}</td>
|
|
173
|
+
<td><span class="badge {badge_class}">{state}</span></td>
|
|
174
|
+
<td class="mono small">{html.escape(str(job.get('created_at', '')))}</td>
|
|
175
|
+
<td class="mono small">{html.escape(str(job.get('updated_at', '')))}</td>
|
|
176
|
+
</tr>
|
|
177
|
+
"""
|
|
178
|
+
if not jobs:
|
|
179
|
+
jobs_rows = "<tr><td colspan='5' class='empty'>No delegation jobs recorded yet.</td></tr>"
|
|
180
|
+
|
|
181
|
+
errors = stats.get("by_error_kind", {})
|
|
182
|
+
errors_rows = ""
|
|
183
|
+
for kind, count in sorted(errors.items(), key=lambda x: x[1], reverse=True):
|
|
184
|
+
errors_rows += f"<tr><td class='mono'>{html.escape(str(kind))}</td><td class='numeric'>{count}</td></tr>"
|
|
185
|
+
if not errors:
|
|
186
|
+
errors_rows = "<tr><td colspan='2' class='empty'>No errors recorded.</td></tr>"
|
|
187
|
+
|
|
188
|
+
return f"""<!DOCTYPE html>
|
|
189
|
+
<html lang="en">
|
|
190
|
+
<head>
|
|
191
|
+
<meta charset="UTF-8">
|
|
192
|
+
<meta http-equiv="refresh" content="3">
|
|
193
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
194
|
+
<title>Local Coding Agent Monitor</title>
|
|
195
|
+
<style>
|
|
196
|
+
:root {{
|
|
197
|
+
--bg: #0f172a;
|
|
198
|
+
--card-bg: #1e293b;
|
|
199
|
+
--border: #334155;
|
|
200
|
+
--text: #f8fafc;
|
|
201
|
+
--text-muted: #94a3b8;
|
|
202
|
+
--accent: #38bdf8;
|
|
203
|
+
--success: #4ade80;
|
|
204
|
+
--warning: #facc15;
|
|
205
|
+
--danger: #f87171;
|
|
206
|
+
}}
|
|
207
|
+
* {{ box-sizing: border-box; margin: 0; padding: 0; }}
|
|
208
|
+
body {{
|
|
209
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
210
|
+
background: var(--bg);
|
|
211
|
+
color: var(--text);
|
|
212
|
+
line-height: 1.5;
|
|
213
|
+
padding: 24px;
|
|
214
|
+
}}
|
|
215
|
+
header {{
|
|
216
|
+
display: flex;
|
|
217
|
+
justify-content: space-between;
|
|
218
|
+
align-items: center;
|
|
219
|
+
margin-bottom: 24px;
|
|
220
|
+
padding-bottom: 16px;
|
|
221
|
+
border-bottom: 1px solid var(--border);
|
|
222
|
+
}}
|
|
223
|
+
h1 {{ font-size: 1.5rem; font-weight: 700; color: var(--accent); }}
|
|
224
|
+
.status-badge {{
|
|
225
|
+
display: inline-flex;
|
|
226
|
+
align-items: center;
|
|
227
|
+
padding: 4px 12px;
|
|
228
|
+
border-radius: 9999px;
|
|
229
|
+
font-size: 0.875rem;
|
|
230
|
+
font-weight: 600;
|
|
231
|
+
background: #064e3b;
|
|
232
|
+
color: var(--success);
|
|
233
|
+
}}
|
|
234
|
+
.grid {{
|
|
235
|
+
display: grid;
|
|
236
|
+
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
|
237
|
+
gap: 16px;
|
|
238
|
+
margin-bottom: 24px;
|
|
239
|
+
}}
|
|
240
|
+
.card {{
|
|
241
|
+
background: var(--card-bg);
|
|
242
|
+
border: 1px solid var(--border);
|
|
243
|
+
border-radius: 8px;
|
|
244
|
+
padding: 16px;
|
|
245
|
+
}}
|
|
246
|
+
.card-title {{ font-size: 0.8125rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-muted); }}
|
|
247
|
+
.card-value {{ font-size: 1.75rem; font-weight: 700; margin-top: 4px; }}
|
|
248
|
+
.card-sub {{ font-size: 0.75rem; color: var(--text-muted); margin-top: 4px; }}
|
|
249
|
+
table {{
|
|
250
|
+
width: 100%;
|
|
251
|
+
border-collapse: collapse;
|
|
252
|
+
font-size: 0.875rem;
|
|
253
|
+
}}
|
|
254
|
+
th, td {{
|
|
255
|
+
padding: 10px 14px;
|
|
256
|
+
text-align: left;
|
|
257
|
+
border-bottom: 1px solid var(--border);
|
|
258
|
+
}}
|
|
259
|
+
th {{ background: #182234; color: var(--text-muted); font-weight: 600; }}
|
|
260
|
+
.mono {{ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 0.8125rem; }}
|
|
261
|
+
.small {{ font-size: 0.75rem; color: var(--text-muted); }}
|
|
262
|
+
.numeric {{ text-align: right; }}
|
|
263
|
+
.empty {{ text-align: center; color: var(--text-muted); padding: 20px; }}
|
|
264
|
+
.badge {{
|
|
265
|
+
padding: 2px 8px;
|
|
266
|
+
border-radius: 4px;
|
|
267
|
+
font-size: 0.75rem;
|
|
268
|
+
font-weight: 600;
|
|
269
|
+
text-transform: uppercase;
|
|
270
|
+
}}
|
|
271
|
+
.badge-success {{ background: #065f46; color: #a7f3d0; }}
|
|
272
|
+
.badge-warning {{ background: #713f12; color: #fef08a; }}
|
|
273
|
+
.badge-info {{ background: #1e3a8a; color: #bfdbfe; }}
|
|
274
|
+
.section-title {{ font-size: 1.125rem; font-weight: 600; margin-bottom: 12px; }}
|
|
275
|
+
.two-col {{ display: grid; grid-template-columns: 2fr 1fr; gap: 16px; }}
|
|
276
|
+
@media (max-width: 900px) {{ .two-col {{ grid-template-columns: 1fr; }} }}
|
|
277
|
+
</style>
|
|
278
|
+
</head>
|
|
279
|
+
<body>
|
|
280
|
+
<header>
|
|
281
|
+
<div>
|
|
282
|
+
<h1>Local Coding Agent Monitor</h1>
|
|
283
|
+
<p class="card-sub">Ollama Autonomous Delegation Service & Bounded Worker Pool</p>
|
|
284
|
+
</div>
|
|
285
|
+
<span class="status-badge">{pool_status}</span>
|
|
286
|
+
</header>
|
|
287
|
+
|
|
288
|
+
<div class="grid">
|
|
289
|
+
<div class="card">
|
|
290
|
+
<div class="card-title">Worker Capacity</div>
|
|
291
|
+
<div class="card-value">{active_workers} / {max_workers}</div>
|
|
292
|
+
<div class="card-sub">Active workers running jobs</div>
|
|
293
|
+
</div>
|
|
294
|
+
<div class="card">
|
|
295
|
+
<div class="card-title">Queue Depth</div>
|
|
296
|
+
<div class="card-value">{queued_jobs} / {max_queue}</div>
|
|
297
|
+
<div class="card-sub">Pending delegation requests</div>
|
|
298
|
+
</div>
|
|
299
|
+
<div class="card">
|
|
300
|
+
<div class="card-title">Success Rate</div>
|
|
301
|
+
<div class="card-value" style="color: var(--success);">{success_rate}</div>
|
|
302
|
+
<div class="card-sub">{accepted} accepted / {total} total</div>
|
|
303
|
+
</div>
|
|
304
|
+
<div class="card">
|
|
305
|
+
<div class="card-title">Avg Latency</div>
|
|
306
|
+
<div class="card-value">{avg_latency}</div>
|
|
307
|
+
<div class="card-sub">Min: {min_latency} | Max: {max_latency}</div>
|
|
308
|
+
</div>
|
|
309
|
+
<div class="card">
|
|
310
|
+
<div class="card-title">Calls & Tools</div>
|
|
311
|
+
<div class="card-value">{model_calls} / {tool_calls}</div>
|
|
312
|
+
<div class="card-sub">Model inferences / Tool executions</div>
|
|
313
|
+
</div>
|
|
314
|
+
</div>
|
|
315
|
+
|
|
316
|
+
<div class="two-col">
|
|
317
|
+
<div class="card">
|
|
318
|
+
<div class="section-title">Recent Delegation Jobs</div>
|
|
319
|
+
<table>
|
|
320
|
+
<thead>
|
|
321
|
+
<tr>
|
|
322
|
+
<th>Job ID</th>
|
|
323
|
+
<th>Caller</th>
|
|
324
|
+
<th>State</th>
|
|
325
|
+
<th>Created</th>
|
|
326
|
+
<th>Updated</th>
|
|
327
|
+
</tr>
|
|
328
|
+
</thead>
|
|
329
|
+
<tbody>
|
|
330
|
+
{jobs_rows}
|
|
331
|
+
</tbody>
|
|
332
|
+
</table>
|
|
333
|
+
</div>
|
|
334
|
+
<div class="card">
|
|
335
|
+
<div class="section-title">Failure Categories</div>
|
|
336
|
+
<table>
|
|
337
|
+
<thead>
|
|
338
|
+
<tr>
|
|
339
|
+
<th>Error Kind</th>
|
|
340
|
+
<th class="numeric">Count</th>
|
|
341
|
+
</tr>
|
|
342
|
+
</thead>
|
|
343
|
+
<tbody>
|
|
344
|
+
{errors_rows}
|
|
345
|
+
</tbody>
|
|
346
|
+
</table>
|
|
347
|
+
</div>
|
|
348
|
+
</div>
|
|
349
|
+
</body>
|
|
350
|
+
</html>
|
|
351
|
+
"""
|
|
352
|
+
|
|
353
|
+
def _handle_workbench(self) -> None:
|
|
354
|
+
workbench_html = self._render_workbench_html()
|
|
355
|
+
self._send_response(HTTPStatus.OK, "text/html; charset=utf-8", workbench_html.encode("utf-8"))
|
|
356
|
+
|
|
357
|
+
def _render_workbench_html(self) -> str:
|
|
358
|
+
return """<!DOCTYPE html>
|
|
359
|
+
<html lang="en">
|
|
360
|
+
<head>
|
|
361
|
+
<meta charset="UTF-8">
|
|
362
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
363
|
+
<title>Local Coding Agent — Interactive Coding Workbench</title>
|
|
364
|
+
<style>
|
|
365
|
+
:root {
|
|
366
|
+
--bg: #0f172a;
|
|
367
|
+
--card-bg: #1e293b;
|
|
368
|
+
--border: #334155;
|
|
369
|
+
--text: #f8fafc;
|
|
370
|
+
--text-muted: #94a3b8;
|
|
371
|
+
--accent: #38bdf8;
|
|
372
|
+
--success: #4ade80;
|
|
373
|
+
--warning: #facc15;
|
|
374
|
+
--danger: #f87171;
|
|
375
|
+
}
|
|
376
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
377
|
+
body {
|
|
378
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
379
|
+
background: var(--bg);
|
|
380
|
+
color: var(--text);
|
|
381
|
+
line-height: 1.5;
|
|
382
|
+
padding: 24px;
|
|
383
|
+
}
|
|
384
|
+
header {
|
|
385
|
+
display: flex;
|
|
386
|
+
justify-content: space-between;
|
|
387
|
+
align-items: center;
|
|
388
|
+
margin-bottom: 24px;
|
|
389
|
+
padding-bottom: 16px;
|
|
390
|
+
border-bottom: 1px solid var(--border);
|
|
391
|
+
}
|
|
392
|
+
h1 { font-size: 1.5rem; font-weight: 700; color: var(--accent); }
|
|
393
|
+
nav a {
|
|
394
|
+
color: var(--text-muted);
|
|
395
|
+
text-decoration: none;
|
|
396
|
+
margin-left: 16px;
|
|
397
|
+
font-size: 0.875rem;
|
|
398
|
+
font-weight: 500;
|
|
399
|
+
}
|
|
400
|
+
nav a.active { color: var(--accent); border-bottom: 2px solid var(--accent); padding-bottom: 4px; }
|
|
401
|
+
.two-col {
|
|
402
|
+
display: grid;
|
|
403
|
+
grid-template-columns: 1fr 1fr;
|
|
404
|
+
gap: 20px;
|
|
405
|
+
}
|
|
406
|
+
@media (max-width: 900px) { .two-col { grid-template-columns: 1fr; } }
|
|
407
|
+
.card {
|
|
408
|
+
background: var(--card-bg);
|
|
409
|
+
border: 1px solid var(--border);
|
|
410
|
+
border-radius: 8px;
|
|
411
|
+
padding: 20px;
|
|
412
|
+
}
|
|
413
|
+
.form-group { margin-bottom: 16px; }
|
|
414
|
+
label { display: block; font-size: 0.8125rem; text-transform: uppercase; color: var(--text-muted); margin-bottom: 6px; }
|
|
415
|
+
input, textarea, select {
|
|
416
|
+
width: 100%;
|
|
417
|
+
background: #0b1120;
|
|
418
|
+
border: 1px solid var(--border);
|
|
419
|
+
border-radius: 6px;
|
|
420
|
+
color: var(--text);
|
|
421
|
+
padding: 8px 12px;
|
|
422
|
+
font-family: inherit;
|
|
423
|
+
font-size: 0.875rem;
|
|
424
|
+
}
|
|
425
|
+
input:focus, textarea:focus, select:focus { outline: none; border-color: var(--accent); }
|
|
426
|
+
button {
|
|
427
|
+
background: var(--accent);
|
|
428
|
+
color: #0f172a;
|
|
429
|
+
border: none;
|
|
430
|
+
border-radius: 6px;
|
|
431
|
+
padding: 10px 18px;
|
|
432
|
+
font-weight: 600;
|
|
433
|
+
cursor: pointer;
|
|
434
|
+
font-size: 0.875rem;
|
|
435
|
+
}
|
|
436
|
+
button:hover { opacity: 0.9; }
|
|
437
|
+
pre {
|
|
438
|
+
background: #0b1120;
|
|
439
|
+
border: 1px solid var(--border);
|
|
440
|
+
border-radius: 6px;
|
|
441
|
+
padding: 12px;
|
|
442
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
443
|
+
font-size: 0.8125rem;
|
|
444
|
+
overflow-x: auto;
|
|
445
|
+
max-height: 480px;
|
|
446
|
+
}
|
|
447
|
+
</style>
|
|
448
|
+
</head>
|
|
449
|
+
<body>
|
|
450
|
+
<header>
|
|
451
|
+
<div>
|
|
452
|
+
<div style="display: flex; align-items: center; gap: 10px;">
|
|
453
|
+
<h1>Local Coding Agent</h1>
|
|
454
|
+
<span style="background: #854d0e; color: #fef08a; font-size: 0.7rem; font-weight: 700; padding: 2px 8px; border-radius: 4px; text-transform: uppercase;">Experimental Preview</span>
|
|
455
|
+
</div>
|
|
456
|
+
<p style="color: var(--text-muted); font-size: 0.8125rem;">Interactive Coding Workbench (Standalone Desktop Harness Redesign Underway)</p>
|
|
457
|
+
</div>
|
|
458
|
+
<nav>
|
|
459
|
+
<a href="/dashboard">Dashboard</a>
|
|
460
|
+
<a href="/workbench" class="active">Workbench</a>
|
|
461
|
+
</nav>
|
|
462
|
+
</header>
|
|
463
|
+
|
|
464
|
+
<div class="two-col">
|
|
465
|
+
<div class="card">
|
|
466
|
+
<h2 style="font-size: 1.1rem; margin-bottom: 16px;">Task Envelope</h2>
|
|
467
|
+
<form id="taskForm">
|
|
468
|
+
<div class="form-group">
|
|
469
|
+
<label>Task ID</label>
|
|
470
|
+
<input type="text" id="taskId" value="atomic-fix-1" required>
|
|
471
|
+
</div>
|
|
472
|
+
<div class="form-group">
|
|
473
|
+
<label>Goal</label>
|
|
474
|
+
<input type="text" id="taskGoal" placeholder="Describe the single atomic change" required>
|
|
475
|
+
</div>
|
|
476
|
+
<div class="form-group">
|
|
477
|
+
<label>Target Files (comma-separated)</label>
|
|
478
|
+
<input type="text" id="taskFiles" placeholder="src/module.py" required>
|
|
479
|
+
</div>
|
|
480
|
+
<div class="form-group">
|
|
481
|
+
<label>Target Checks (comma-separated)</label>
|
|
482
|
+
<input type="text" id="taskChecks" placeholder="pytest tests/test_module.py">
|
|
483
|
+
</div>
|
|
484
|
+
<div class="form-group">
|
|
485
|
+
<label>Model Profile</label>
|
|
486
|
+
<select id="taskProfile">
|
|
487
|
+
<option value="qwen2.5-1.5b">qwen2.5-1.5b</option>
|
|
488
|
+
<option value="ling-3.0-tiny-q6k">ling-3.0-tiny-q6k (llama-server:8080)</option>
|
|
489
|
+
<option value="qwen3-8b-q6k">qwen3-8b-q6k</option>
|
|
490
|
+
<option value="qwen3.8-27b-q4">qwen3.8-27b-q4</option>
|
|
491
|
+
</select>
|
|
492
|
+
</div>
|
|
493
|
+
<button type="submit" id="btnSubmit">Delegate Task</button>
|
|
494
|
+
</form>
|
|
495
|
+
</div>
|
|
496
|
+
|
|
497
|
+
<div class="card">
|
|
498
|
+
<h2 style="font-size: 1.1rem; margin-bottom: 16px;">Execution Result & Diff</h2>
|
|
499
|
+
<div id="statusBadge" style="margin-bottom: 12px; font-size: 0.875rem; color: var(--text-muted);">Ready. Submit a task envelope to run.</div>
|
|
500
|
+
<pre id="outputPre">// Result will appear here...</pre>
|
|
501
|
+
</div>
|
|
502
|
+
</div>
|
|
503
|
+
|
|
504
|
+
<script>
|
|
505
|
+
document.getElementById('taskForm').addEventListener('submit', async (e) => {
|
|
506
|
+
e.preventDefault();
|
|
507
|
+
const btn = document.getElementById('btnSubmit');
|
|
508
|
+
const badge = document.getElementById('statusBadge');
|
|
509
|
+
const output = document.getElementById('outputPre');
|
|
510
|
+
|
|
511
|
+
btn.disabled = true;
|
|
512
|
+
btn.textContent = 'Running...';
|
|
513
|
+
badge.textContent = 'Delegating task to local model executor...';
|
|
514
|
+
|
|
515
|
+
const task = {
|
|
516
|
+
id: document.getElementById('taskId').value,
|
|
517
|
+
goal: document.getElementById('taskGoal').value,
|
|
518
|
+
files: document.getElementById('taskFiles').value.split(',').map(s => s.trim()).filter(Boolean),
|
|
519
|
+
checks: document.getElementById('taskChecks').value.split(',').map(s => s.trim()).filter(Boolean)
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
try {
|
|
523
|
+
const res = await fetch('/api/delegate', {
|
|
524
|
+
method: 'POST',
|
|
525
|
+
headers: { 'Content-Type': 'application/json' },
|
|
526
|
+
body: JSON.stringify({ task, profile: document.getElementById('taskProfile').value })
|
|
527
|
+
});
|
|
528
|
+
const data = await res.json();
|
|
529
|
+
badge.textContent = 'Status: ' + (data.status || 'done');
|
|
530
|
+
output.textContent = JSON.stringify(data, null, 2);
|
|
531
|
+
} catch (err) {
|
|
532
|
+
badge.textContent = 'Error executing task';
|
|
533
|
+
output.textContent = String(err);
|
|
534
|
+
} finally {
|
|
535
|
+
btn.disabled = false;
|
|
536
|
+
btn.textContent = 'Delegate Task';
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
</script>
|
|
540
|
+
</body>
|
|
541
|
+
</html>"""
|
|
542
|
+
|
|
543
|
+
def _send_json(self, data: Any) -> None:
|
|
544
|
+
body = json.dumps(data, ensure_ascii=False).encode("utf-8")
|
|
545
|
+
self._send_response(HTTPStatus.OK, "application/json; charset=utf-8", body)
|
|
546
|
+
|
|
547
|
+
def _send_response(self, status: HTTPStatus, content_type: str, body: bytes) -> None:
|
|
548
|
+
self.send_response(status)
|
|
549
|
+
self.send_header("Content-Type", content_type)
|
|
550
|
+
self.send_header("Content-Length", str(len(body)))
|
|
551
|
+
self.send_header("Cache-Control", "no-cache, no-store, must-revalidate")
|
|
552
|
+
self.end_headers()
|
|
553
|
+
self.wfile.write(body)
|
|
554
|
+
|
|
555
|
+
def log_message(self, format: str, *args: Any) -> None:
|
|
556
|
+
# Ponytail / minimal: suppress default stderr request logging
|
|
557
|
+
del format, args
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
class MonitorServer:
|
|
561
|
+
"""Thread-safe, daemonized monitoring HTTP server."""
|
|
562
|
+
|
|
563
|
+
def __init__(
|
|
564
|
+
self,
|
|
565
|
+
host: str = "127.0.0.1",
|
|
566
|
+
port: int = 0,
|
|
567
|
+
*,
|
|
568
|
+
worker_pool: BoundedWorkerPool | None = None,
|
|
569
|
+
stats: DelegationStats | None = None,
|
|
570
|
+
stats_path: str | Path | None = None,
|
|
571
|
+
) -> None:
|
|
572
|
+
self.host = host
|
|
573
|
+
self.port = port
|
|
574
|
+
self.worker_pool = worker_pool
|
|
575
|
+
self.stats = stats
|
|
576
|
+
self.stats_path = stats_path
|
|
577
|
+
self.started_at = time.monotonic()
|
|
578
|
+
self._events: list[dict[str, Any]] = []
|
|
579
|
+
self._events_lock = threading.Lock()
|
|
580
|
+
self._max_events = 200
|
|
581
|
+
self._httpd = ThreadingHTTPServer((host, port), MonitorRequestHandler)
|
|
582
|
+
self._httpd.monitor_server = self # type: ignore[attr-defined]
|
|
583
|
+
self._thread: threading.Thread | None = None
|
|
584
|
+
|
|
585
|
+
def emit_event(self, event: dict[str, Any]) -> None:
|
|
586
|
+
"""Emit a telemetry or lifecycle event."""
|
|
587
|
+
with self._events_lock:
|
|
588
|
+
event_with_ts = {"timestamp": time.time(), **event}
|
|
589
|
+
self._events.append(event_with_ts)
|
|
590
|
+
if len(self._events) > self._max_events:
|
|
591
|
+
self._events = self._events[-self._max_events :]
|
|
592
|
+
|
|
593
|
+
def get_recent_events(self) -> list[dict[str, Any]]:
|
|
594
|
+
"""Return a copy of recent telemetry events."""
|
|
595
|
+
with self._events_lock:
|
|
596
|
+
return list(self._events)
|
|
597
|
+
|
|
598
|
+
@property
|
|
599
|
+
def server_address(self) -> tuple[str, int]:
|
|
600
|
+
return self._httpd.server_address
|
|
601
|
+
|
|
602
|
+
@property
|
|
603
|
+
def actual_port(self) -> int:
|
|
604
|
+
return self._httpd.server_address[1]
|
|
605
|
+
|
|
606
|
+
@property
|
|
607
|
+
def url(self) -> str:
|
|
608
|
+
return f"http://{self.host}:{self.actual_port}"
|
|
609
|
+
|
|
610
|
+
def start(self) -> None:
|
|
611
|
+
"""Start serving in a background daemon thread."""
|
|
612
|
+
if self._thread is not None:
|
|
613
|
+
return
|
|
614
|
+
self._thread = threading.Thread(
|
|
615
|
+
target=self._httpd.serve_forever,
|
|
616
|
+
name="local-agent-monitor-http",
|
|
617
|
+
daemon=True,
|
|
618
|
+
)
|
|
619
|
+
self._thread.start()
|
|
620
|
+
|
|
621
|
+
def stop(self) -> None:
|
|
622
|
+
"""Shut down the HTTP server and release port."""
|
|
623
|
+
if self._thread is None:
|
|
624
|
+
return
|
|
625
|
+
self._httpd.shutdown()
|
|
626
|
+
self._httpd.server_close()
|
|
627
|
+
self._thread.join(timeout=3.0)
|
|
628
|
+
self._thread = None
|
|
629
|
+
|
|
630
|
+
def __enter__(self) -> MonitorServer:
|
|
631
|
+
self.start()
|
|
632
|
+
return self
|
|
633
|
+
|
|
634
|
+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
635
|
+
self.stop()
|