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,346 @@
1
+ """Lifecycle hook bridge: registry and dispatcher (R30)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ import threading
7
+ import uuid
8
+ from dataclasses import asdict, dataclass
9
+ from typing import Any, Callable
10
+
11
+ from ._decision import HookDecision
12
+
13
+
14
+ @dataclass
15
+ class _HookRegistration:
16
+ hook_id: str
17
+ point: str
18
+ handler: Callable[..., Any]
19
+ matcher: str | None
20
+ priority: int = 0
21
+
22
+
23
+ class HookBridge:
24
+ """Central registry and dispatcher for lifecycle hooks."""
25
+
26
+ POINT_PRE_TOOL_CALL = "on_pre_tool_call"
27
+ POINT_POST_TOOL_CALL = "on_post_tool_call"
28
+ POINT_TURN_START = "on_turn_start"
29
+ POINT_TURN_END = "on_turn_end"
30
+ POINT_SESSION_FINISH = "on_session_finish"
31
+
32
+ _POINT_ALIASES = {
33
+ "PreToolUse": POINT_PRE_TOOL_CALL,
34
+ "pre_tool_call": POINT_PRE_TOOL_CALL,
35
+ "on_pre_tool_call": POINT_PRE_TOOL_CALL,
36
+ "PostToolUse": POINT_POST_TOOL_CALL,
37
+ "post_tool_call": POINT_POST_TOOL_CALL,
38
+ "on_post_tool_call": POINT_POST_TOOL_CALL,
39
+ "UserPromptSubmit": POINT_TURN_START,
40
+ "turn_start": POINT_TURN_START,
41
+ "on_turn_start": POINT_TURN_START,
42
+ "TurnEnd": POINT_TURN_END,
43
+ "turn_end": POINT_TURN_END,
44
+ "on_turn_end": POINT_TURN_END,
45
+ "SessionStart": "on_session_start",
46
+ "session_start": "on_session_start",
47
+ "on_session_start": "on_session_start",
48
+ "Stop": POINT_SESSION_FINISH,
49
+ "session_finish": POINT_SESSION_FINISH,
50
+ "on_session_finish": POINT_SESSION_FINISH,
51
+ "SubagentStart": "on_subagent_start",
52
+ "SubagentStop": "on_subagent_stop",
53
+ }
54
+
55
+ def __init__(self) -> None:
56
+ self._lock = threading.RLock()
57
+ self._hooks: dict[str, list[_HookRegistration]] = {}
58
+
59
+ def _normalize_point(self, point: str) -> str:
60
+ return self._POINT_ALIASES.get(point, point)
61
+
62
+ def register(
63
+ self,
64
+ point: str,
65
+ handler: Callable[..., Any],
66
+ *,
67
+ matcher: str | None = None,
68
+ priority: int = 0,
69
+ ) -> str:
70
+ """Register a hook handler for a lifecycle point."""
71
+ norm_point = self._normalize_point(point)
72
+ hook_id = f"hook-{uuid.uuid4().hex[:8]}"
73
+ reg = _HookRegistration(
74
+ hook_id=hook_id,
75
+ point=norm_point,
76
+ handler=handler,
77
+ matcher=matcher,
78
+ priority=priority,
79
+ )
80
+ with self._lock:
81
+ if norm_point not in self._hooks:
82
+ self._hooks[norm_point] = []
83
+ self._hooks[norm_point].append(reg)
84
+ self._hooks[norm_point].sort(key=lambda h: h.priority, reverse=True)
85
+ return hook_id
86
+
87
+ def unregister(self, hook_id: str) -> bool:
88
+ """Unregister a hook handler by its ID."""
89
+ with self._lock:
90
+ for point, reg_list in self._hooks.items():
91
+ for idx, reg in enumerate(reg_list):
92
+ if reg.hook_id == hook_id:
93
+ reg_list.pop(idx)
94
+ return True
95
+ return False
96
+
97
+ def on_pre_tool_call(self, matcher: str | None = None, priority: int = 0) -> Callable:
98
+ """Decorator for registering pre-tool-call hooks."""
99
+ def decorator(fn: Callable) -> Callable:
100
+ self.register(self.POINT_PRE_TOOL_CALL, fn, matcher=matcher, priority=priority)
101
+ return fn
102
+ return decorator
103
+
104
+ def on_post_tool_call(self, matcher: str | None = None, priority: int = 0) -> Callable:
105
+ """Decorator for registering post-tool-call hooks."""
106
+ def decorator(fn: Callable) -> Callable:
107
+ self.register(self.POINT_POST_TOOL_CALL, fn, matcher=matcher, priority=priority)
108
+ return fn
109
+ return decorator
110
+
111
+ def on_turn_start(self, matcher: str | None = None, priority: int = 0) -> Callable:
112
+ """Decorator for registering turn-start hooks."""
113
+ def decorator(fn: Callable) -> Callable:
114
+ self.register(self.POINT_TURN_START, fn, matcher=matcher, priority=priority)
115
+ return fn
116
+ return decorator
117
+
118
+ def on_turn_end(self, matcher: str | None = None, priority: int = 0) -> Callable:
119
+ """Decorator for registering turn-end hooks."""
120
+ def decorator(fn: Callable) -> Callable:
121
+ self.register(self.POINT_TURN_END, fn, matcher=matcher, priority=priority)
122
+ return fn
123
+ return decorator
124
+
125
+ def on_session_finish(self, priority: int = 0) -> Callable:
126
+ """Decorator for registering session-finish hooks."""
127
+ def decorator(fn: Callable) -> Callable:
128
+ self.register(self.POINT_SESSION_FINISH, fn, priority=priority)
129
+ return fn
130
+ return decorator
131
+
132
+ def _matches(self, pattern: str | None, query: str) -> bool:
133
+ if pattern is None or pattern == "" or pattern == "*":
134
+ return True
135
+ try:
136
+ return bool(re.search(pattern, query))
137
+ except re.error:
138
+ return pattern == query
139
+
140
+ def trigger(
141
+ self,
142
+ point: str,
143
+ match_target: str = "",
144
+ payload: dict[str, Any] | None = None,
145
+ **kwargs: Any,
146
+ ) -> HookDecision:
147
+ """Execute all matching hooks for a given point and combine decisions."""
148
+ norm_point = self._normalize_point(point)
149
+ with self._lock:
150
+ candidates = list(self._hooks.get(norm_point, []))
151
+
152
+ decision = HookDecision()
153
+ merged_payload = dict(payload or {})
154
+ merged_payload.update(kwargs)
155
+
156
+ for reg in candidates:
157
+ if not self._matches(reg.matcher, match_target):
158
+ continue
159
+
160
+ try:
161
+ import inspect
162
+ sig = inspect.signature(reg.handler)
163
+ param_count = len([
164
+ p for p in sig.parameters.values()
165
+ if p.kind in (inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD)
166
+ ])
167
+ var_pos = any(p.kind == inspect.Parameter.VAR_POSITIONAL for p in sig.parameters.values())
168
+ if var_pos or param_count >= 2:
169
+ out = reg.handler(match_target, merged_payload)
170
+ elif param_count == 1:
171
+ out = reg.handler(merged_payload)
172
+ else:
173
+ out = reg.handler()
174
+ except Exception as exc:
175
+ out = {"decision": "error", "reason": f"hook handler error: {exc}"}
176
+
177
+ self._fold_outcome(decision, out)
178
+ if decision.is_blocking():
179
+ # Short-circuit on blocking decision
180
+ break
181
+
182
+ return decision
183
+
184
+ def trigger_pre_tool_call(
185
+ self,
186
+ tool_name: str,
187
+ arguments: dict[str, Any],
188
+ context: dict[str, Any] | None = None,
189
+ ) -> HookDecision:
190
+ """Convenience method for on_pre_tool_call point."""
191
+ payload = {
192
+ "tool_name": tool_name,
193
+ "tool_input": arguments,
194
+ "context": context or {},
195
+ }
196
+ return self.trigger(self.POINT_PRE_TOOL_CALL, match_target=tool_name, payload=payload)
197
+
198
+ def trigger_post_tool_call(
199
+ self,
200
+ tool_name: str,
201
+ arguments: dict[str, Any],
202
+ result: dict[str, Any],
203
+ context: dict[str, Any] | None = None,
204
+ ) -> HookDecision:
205
+ """Convenience method for on_post_tool_call point."""
206
+ payload = {
207
+ "tool_name": tool_name,
208
+ "tool_input": arguments,
209
+ "tool_output": result,
210
+ "context": context or {},
211
+ }
212
+ return self.trigger(self.POINT_POST_TOOL_CALL, match_target=tool_name, payload=payload)
213
+
214
+ def trigger_turn_start(
215
+ self,
216
+ turn: int,
217
+ prompt: str,
218
+ context: dict[str, Any] | None = None,
219
+ ) -> HookDecision:
220
+ """Convenience method for on_turn_start point."""
221
+ payload = {
222
+ "turn": turn,
223
+ "turn_id": str(turn),
224
+ "prompt": prompt,
225
+ "context": context or {},
226
+ }
227
+ return self.trigger(self.POINT_TURN_START, match_target="", payload=payload)
228
+
229
+ def trigger_turn_end(
230
+ self,
231
+ turn: int,
232
+ response: Any,
233
+ context: dict[str, Any] | None = None,
234
+ ) -> HookDecision:
235
+ """Convenience method for on_turn_end point."""
236
+ payload = {
237
+ "turn": turn,
238
+ "turn_id": str(turn),
239
+ "response": response,
240
+ "context": context or {},
241
+ }
242
+ return self.trigger(self.POINT_TURN_END, match_target="", payload=payload)
243
+
244
+ def trigger_session_finish(
245
+ self,
246
+ session_id: str,
247
+ summary: str = "",
248
+ status: str = "completed",
249
+ context: dict[str, Any] | None = None,
250
+ ) -> HookDecision:
251
+ """Convenience method for on_session_finish point."""
252
+ payload = {
253
+ "session_id": session_id,
254
+ "summary": summary,
255
+ "status": status,
256
+ "context": context or {},
257
+ }
258
+ return self.trigger(self.POINT_SESSION_FINISH, match_target="", payload=payload)
259
+
260
+ def _fold_outcome(self, decision: HookDecision, outcome: Any) -> None:
261
+ """Fold an individual hook outcome into the aggregate HookDecision."""
262
+ if outcome is None:
263
+ return
264
+
265
+ if isinstance(outcome, HookDecision):
266
+ raw = asdict(outcome) if hasattr(outcome, "__dataclass_fields__") else {}
267
+ if outcome.is_blocking():
268
+ decision.decision = outcome.decision
269
+ decision.allowed = False
270
+ decision.reason = outcome.reason or decision.reason
271
+ if outcome.additional_context:
272
+ decision.additional_context.extend(outcome.additional_context)
273
+ if outcome.feedback:
274
+ decision.feedback.extend(outcome.feedback)
275
+ if outcome.system_messages:
276
+ decision.system_messages.extend(outcome.system_messages)
277
+ if outcome.updated_input:
278
+ decision.updated_input = outcome.updated_input
279
+ if outcome.stop:
280
+ decision.stop = True
281
+ decision.stop_reason = outcome.stop_reason or decision.stop_reason
282
+ decision.raw_outputs.append(raw)
283
+ return
284
+
285
+ if isinstance(outcome, dict):
286
+ decision.raw_outputs.append(outcome)
287
+ # Permission / blocking decision check
288
+ dec = outcome.get("decision")
289
+ perm_dec = (
290
+ outcome.get("hookSpecificOutput", {}).get("permissionDecision")
291
+ if isinstance(outcome.get("hookSpecificOutput"), dict)
292
+ else None
293
+ )
294
+ selected_dec = perm_dec or dec
295
+
296
+ if selected_dec in ("deny", "block", "error"):
297
+ decision.decision = selected_dec
298
+ decision.allowed = False
299
+ reason = outcome.get("reason") or (
300
+ outcome.get("hookSpecificOutput", {}).get("reason")
301
+ if isinstance(outcome.get("hookSpecificOutput"), dict)
302
+ else None
303
+ )
304
+ if reason:
305
+ decision.reason = str(reason)
306
+ elif selected_dec in ("allow", "approve", "ask"):
307
+ if not decision.is_blocking():
308
+ decision.decision = selected_dec
309
+
310
+ # Additional context
311
+ ctx = outcome.get("additionalContext") or outcome.get("additional_context")
312
+ if not ctx and isinstance(outcome.get("hookSpecificOutput"), dict):
313
+ ctx = outcome.get("hookSpecificOutput", {}).get("additionalContext")
314
+ if isinstance(ctx, str) and ctx.strip():
315
+ decision.additional_context.append(ctx.strip())
316
+ elif isinstance(ctx, list):
317
+ decision.additional_context.extend([str(c) for c in ctx if c])
318
+
319
+ # Feedback
320
+ fb = outcome.get("feedback")
321
+ if not fb and isinstance(outcome.get("hookSpecificOutput"), dict):
322
+ fb = outcome.get("hookSpecificOutput", {}).get("feedback")
323
+ if isinstance(fb, str) and fb.strip():
324
+ decision.feedback.append(fb.strip())
325
+ elif isinstance(fb, list):
326
+ decision.feedback.extend([str(f) for f in fb if f])
327
+
328
+ # System message
329
+ sys_msg = outcome.get("systemMessage") or outcome.get("system_message")
330
+ if not sys_msg and isinstance(outcome.get("hookSpecificOutput"), dict):
331
+ sys_msg = outcome.get("hookSpecificOutput", {}).get("systemMessage")
332
+ if isinstance(sys_msg, str) and sys_msg.strip():
333
+ decision.system_messages.append(sys_msg.strip())
334
+
335
+ # Updated input
336
+ updated = outcome.get("updatedInput") or outcome.get("updated_input")
337
+ if not updated and isinstance(outcome.get("hookSpecificOutput"), dict):
338
+ updated = outcome.get("hookSpecificOutput", {}).get("updatedInput")
339
+ if isinstance(updated, dict):
340
+ decision.updated_input = updated
341
+
342
+ # Stop
343
+ cont = outcome.get("continue")
344
+ if cont is False:
345
+ decision.stop = True
346
+ decision.stop_reason = outcome.get("stopReason") or outcome.get("stop_reason")
@@ -0,0 +1,26 @@
1
+ """Hook decision model (R30)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Any
7
+
8
+
9
+ @dataclass
10
+ class HookDecision:
11
+ """Consolidated outcome of hook execution at an interception point."""
12
+
13
+ decision: str = "allow" # "allow", "deny", "ask", "approve", "block"
14
+ allowed: bool = True
15
+ reason: str | None = None
16
+ additional_context: list[str] = field(default_factory=list)
17
+ feedback: list[str] = field(default_factory=list)
18
+ system_messages: list[str] = field(default_factory=list)
19
+ updated_input: dict[str, Any] | None = None
20
+ stop: bool = False
21
+ stop_reason: str | None = None
22
+ raw_outputs: list[dict[str, Any]] = field(default_factory=list)
23
+
24
+ def is_blocking(self) -> bool:
25
+ """True if the hook explicitly denied or blocked the operation."""
26
+ return not self.allowed or self.decision in ("deny", "block")
@@ -0,0 +1,60 @@
1
+ """Generic LSP Stdio Code Intelligence Seam & Language Server Navigation (R25).
2
+
3
+ Adapted from DeepSeek Harness @deepseek-ai/dsh-lsp and @deepseek-ai/dsh-tool-lsp.
4
+ Provides standardized JSON-RPC stdio language server communication, process lifecycle,
5
+ protocol translation, mock servers, and AST/regex fallback intelligence.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from ._types import (
11
+ DEFAULT_SERVER_CANDIDATES,
12
+ EXTENSION_TO_LANGUAGE,
13
+ HEADER_SEPARATOR,
14
+ LspConnectionError,
15
+ LspError,
16
+ LspHoverResult,
17
+ LspLocation,
18
+ LspPosition,
19
+ LspRange,
20
+ LspResponseError,
21
+ LspSymbol,
22
+ LspTimeoutError,
23
+ MAX_HEADER_BYTES,
24
+ MAX_MESSAGE_BYTES,
25
+ MessageDecoder,
26
+ SYMBOL_KIND_NAMES,
27
+ encode_message,
28
+ path_to_uri,
29
+ uri_to_path,
30
+ )
31
+ from ._client import LspClient
32
+ from ._fallback import FallbackLspEngine
33
+ from ._manager import LspManager
34
+ from ._mock import MockLspServer
35
+
36
+ __all__ = [
37
+ "DEFAULT_SERVER_CANDIDATES",
38
+ "EXTENSION_TO_LANGUAGE",
39
+ "FallbackLspEngine",
40
+ "HEADER_SEPARATOR",
41
+ "LspClient",
42
+ "LspConnectionError",
43
+ "LspError",
44
+ "LspHoverResult",
45
+ "LspLocation",
46
+ "LspManager",
47
+ "LspPosition",
48
+ "LspRange",
49
+ "LspResponseError",
50
+ "LspSymbol",
51
+ "LspTimeoutError",
52
+ "MAX_HEADER_BYTES",
53
+ "MAX_MESSAGE_BYTES",
54
+ "MessageDecoder",
55
+ "MockLspServer",
56
+ "SYMBOL_KIND_NAMES",
57
+ "encode_message",
58
+ "path_to_uri",
59
+ "uri_to_path",
60
+ ]