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,200 @@
1
+ """Mode router: deterministic heuristic classifier and hybrid mode routing.
2
+
3
+ Routes a user prompt to one of the operational modes: 'chat', 'build' or
4
+ 'plan'. The fast path is a pure heuristic; the hybrid path optionally consults
5
+ an external router (an LLM) on a cadence, in an isolated context, and falls
6
+ back to the heuristic when the router is absent, uncertain or fails.
7
+ """
8
+
9
+ import re
10
+ from enum import Enum
11
+ from typing import Callable
12
+
13
+
14
+ class ModeName(str, Enum):
15
+ CHAT = "chat"
16
+ BUILD = "build"
17
+ PLAN = "plan"
18
+ HYBRID = "hybrid"
19
+
20
+
21
+ MODES = ("chat", "build", "plan", "hybrid") # tuple of valid mode strings
22
+ VALID_ROUTED = ("chat", "build", "plan") # modes a classifier can return
23
+
24
+ # Small-talk / conversational greetings and phrases.
25
+ _GREETINGS = {
26
+ "hi", "hello", "hey", "привет", "здравствуйте", "yo", "sup",
27
+ "thanks", "спасибо", "ok", "help", "test",
28
+ }
29
+ _PHRASES = {
30
+ "how are you", "what's up", "whats up", "wassup", "how are u",
31
+ }
32
+
33
+ # Planning-intent prefixes (explicit phrases) and the Russian verb form
34
+ # (substring match is fine for the verb; the noun is matched by token boundary).
35
+ _PLAN_PREFIXES = (
36
+ "plan ", "plan:", "спланируй ", "спланируй:", "составь план ", "план для ",
37
+ )
38
+
39
+ # Informational / code-inquiry prefixes.
40
+ _INFO_PREFIXES = (
41
+ "read ", "explain ", "what ", "how ", "tell me ", "show ",
42
+ "go to ", "open ", "where is ",
43
+ "опиши ", "прочитай ", "что делает ", "как ", "покажи ", "где ",
44
+ )
45
+
46
+ # Interrogative tokens marking an informational intent anywhere in the prompt
47
+ # ("can u tell me what main.py does?"), not only as a prefix.
48
+ _QUESTION_TOKENS = frozenset({
49
+ "what", "how", "where", "why", "who", "which",
50
+ "что", "где", "почему", "зачем", "какой", "какая",
51
+ })
52
+
53
+ # Imperative verbs marking an actionable build task even when the prompt also
54
+ # contains question wording ("fix how the retry loop counts turns").
55
+ _BUILD_VERB_PREFIXES = (
56
+ "fix ", "add ", "write ", "create ", "implement ", "refactor ",
57
+ "update ", "remove ", "delete ", "change ", "test ", "rename ",
58
+ "optimize ", "clean ", "install ", "make ", "исправь ", "добавь ",
59
+ )
60
+
61
+ _NON_MUTATING_PREFIXES = (
62
+ "review ", "please review", "do not change", "don't change", "no changes",
63
+ "please help", "help me understand", "проверь ", "не изменяй", "не меняй",
64
+ )
65
+
66
+ _TOKEN_STRIP = ".,!?;:'\"()«»"
67
+
68
+
69
+ def classify_fast(prompt: str, current_mode: str | None = None) -> str:
70
+ """Deterministic heuristic classifier. Never returns 'hybrid'."""
71
+ if not prompt or not prompt.strip():
72
+ return "chat"
73
+
74
+ text = prompt.strip().lower()
75
+
76
+ if current_mode in VALID_ROUTED:
77
+ return current_mode
78
+
79
+ # Small-talk / conversational.
80
+ if text in _GREETINGS or text in _PHRASES:
81
+ return "chat"
82
+
83
+ if text.startswith(_NON_MUTATING_PREFIXES):
84
+ return "chat"
85
+
86
+ # Actionable imperative wins over question wording.
87
+ if text.startswith(_BUILD_VERB_PREFIXES):
88
+ return "build"
89
+
90
+ tokens = [tok.strip(_TOKEN_STRIP) for tok in text.split()]
91
+ is_question = text.endswith("?") or any(tok in _QUESTION_TOKENS for tok in tokens)
92
+
93
+ # Informational / code inquiry takes precedence over planning intent so
94
+ # that e.g. "explain the deployment plan" reads as informational.
95
+ if text.startswith(_INFO_PREFIXES) or is_question:
96
+ return "chat"
97
+
98
+ # Planning intent. English "plan" matched at word boundaries (so
99
+ # "planner"/"airplane" don't match); Russian noun "план" as a standalone
100
+ # token; verb "планируй" as a substring.
101
+ if (
102
+ text.startswith(_PLAN_PREFIXES)
103
+ or re.search(r"\bplan\b", text)
104
+ or "планируй" in text
105
+ or "план" in text.split()
106
+ ):
107
+ return "plan"
108
+
109
+ # Ambiguous intent must not acquire mutation permissions merely because no
110
+ # classifier keyword matched. The caller can ask the user for a mode.
111
+ return "chat"
112
+
113
+
114
+ def classify_mode(
115
+ prompt: str,
116
+ *,
117
+ current_mode: str | None = None,
118
+ router=None,
119
+ n_every: int = 3,
120
+ counter: int = 0,
121
+ recent_prompts: list[str] | None = None,
122
+ ) -> str:
123
+ """Route a prompt, optionally consulting an external router on a cadence."""
124
+ if router is None or not callable(router):
125
+ return classify_fast(prompt, current_mode)
126
+
127
+ n_every = max(1, n_every)
128
+ counter = max(0, counter)
129
+
130
+ if counter % n_every == 0:
131
+ try:
132
+ result = router(recent_prompts)
133
+ except Exception:
134
+ result = None
135
+ if result in VALID_ROUTED:
136
+ return result
137
+
138
+ return classify_fast(prompt, current_mode)
139
+
140
+
141
+ DEFAULT_MODE_ROUTER_PROFILE = "qwen2.5-1.5b"
142
+
143
+ # Extremely short system prompt: small models comply best with one clear word.
144
+ _ROUTER_SYSTEM = (
145
+ "Classify the user's intent. Reply with exactly one word: chat, build, or plan."
146
+ )
147
+
148
+
149
+ def build_mode_router(
150
+ profile_name: str = DEFAULT_MODE_ROUTER_PROFILE, *, client=None
151
+ ) -> Callable[[list[str] | None], str | None]:
152
+ """Return a router callable for classify_mode's `router` parameter.
153
+
154
+ The returned callable takes an optional list of recent user prompts (the
155
+ isolated context) and returns one of 'chat'|'build'|'plan', or None if it
156
+ cannot decide or anything fails. It consults a small local model in an
157
+ ISOLATED context (a fresh single-turn completion with no workspace or
158
+ task context, just the classification task). It never runs the Controller
159
+ and never writes to disk.
160
+
161
+ - If `client` is provided, use it directly (for tests/hermetic use).
162
+ - Otherwise build one via build_client(get_profile(profile_name))
163
+ imported lazily inside the factory so this module has no heavy
164
+ dependencies at import time.
165
+ - The completion uses a strict system prompt instructing the model to
166
+ reply with exactly one word: chat, build, or plan.
167
+ - Parse the reply: lowercase, take the first token that matches one of
168
+ VALID_ROUTED; if none matches, return None.
169
+ - Wrap everything in try/except; on ANY exception return None (never raise).
170
+ """
171
+ if client is None:
172
+ # Lazily import the backend so importing mode_router stays dependency-light.
173
+ from local_coding_agent.ollama_adapter import build_client
174
+ from local_coding_agent.profiles import get_profile
175
+
176
+ client = build_client(get_profile(profile_name))
177
+
178
+ def router(recent_prompts: list[str] | None = None) -> str | None:
179
+ try:
180
+ messages: list[dict[str, object]] = [
181
+ {"role": "system", "content": _ROUTER_SYSTEM}
182
+ ]
183
+ user_content = "Recent messages:\n" + "\n".join(
184
+ f"- {p}" for p in (recent_prompts or [])
185
+ )
186
+ if not user_content.strip():
187
+ user_content = "No recent context."
188
+ messages.append({"role": "user", "content": user_content})
189
+
190
+ reply = client.chat(messages)
191
+ content = (reply or {}).get("message", {}).get("content") or ""
192
+ for token in content.lower().split():
193
+ tok = token.strip(".,!?;:")
194
+ if tok in VALID_ROUTED:
195
+ return tok
196
+ return None
197
+ except Exception:
198
+ return None
199
+
200
+ return router