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,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
|