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,278 @@
|
|
|
1
|
+
"""Deterministic Pinpointed Prescriptions Engine for Small Local Models.
|
|
2
|
+
|
|
3
|
+
Translates validation issues, JSON parse failures, tool policy violations,
|
|
4
|
+
SEARCH/REPLACE mismatches, and test failures into precise, actionable,
|
|
5
|
+
laser-focused instructions that small models (2B-4B) can follow deterministically.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from typing import Any, Mapping, Optional, Sequence
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DiagnosticKind(str, Enum):
|
|
17
|
+
JSON_SYNTAX_ERROR = "JSON_SYNTAX_ERROR"
|
|
18
|
+
SCHEMA_CHECKS_TYPE_ERROR = "SCHEMA_CHECKS_TYPE_ERROR"
|
|
19
|
+
SCHEMA_RISKS_TYPE_ERROR = "SCHEMA_RISKS_TYPE_ERROR"
|
|
20
|
+
SCHEMA_MISSING_REQUIRED = "SCHEMA_MISSING_REQUIRED"
|
|
21
|
+
TOOL_DUAL_PATCH_AND_EDITS = "TOOL_DUAL_PATCH_AND_EDITS"
|
|
22
|
+
TOOL_MISSING_PATCH_AND_EDITS = "TOOL_MISSING_PATCH_AND_EDITS"
|
|
23
|
+
TOOL_FORBIDDEN_FILE = "TOOL_FORBIDDEN_FILE"
|
|
24
|
+
SEARCH_BLOCK_NOT_FOUND = "SEARCH_BLOCK_NOT_FOUND"
|
|
25
|
+
SEARCH_NOT_LINE_ALIGNED = "SEARCH_NOT_LINE_ALIGNED"
|
|
26
|
+
SEARCH_AMBIGUOUS = "SEARCH_AMBIGUOUS"
|
|
27
|
+
DUPLICATE_EDIT_FILE = "DUPLICATE_EDIT_FILE"
|
|
28
|
+
DIFF_CORRUPT_HUNK = "DIFF_CORRUPT_HUNK"
|
|
29
|
+
DIFF_APPLY_FAILED = "DIFF_APPLY_FAILED"
|
|
30
|
+
TEST_CHECK_FAILED = "TEST_CHECK_FAILED"
|
|
31
|
+
GENERIC_DIAGNOSTIC = "GENERIC_DIAGNOSTIC"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class Prescription:
|
|
36
|
+
kind: DiagnosticKind
|
|
37
|
+
code: str
|
|
38
|
+
summary: str
|
|
39
|
+
instruction: str
|
|
40
|
+
fix_example: Optional[str] = None
|
|
41
|
+
lang: str = "ru"
|
|
42
|
+
|
|
43
|
+
def format_message(self) -> str:
|
|
44
|
+
if self.fix_example:
|
|
45
|
+
example_label = "Example:" if self.lang == "en" else "Пример:"
|
|
46
|
+
return f"{self.instruction} {example_label} {self.fix_example}"
|
|
47
|
+
return self.instruction
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def prescribe_issue(
|
|
51
|
+
issue: str,
|
|
52
|
+
*,
|
|
53
|
+
lang: str = "ru",
|
|
54
|
+
context: Optional[Mapping[str, Any]] = None,
|
|
55
|
+
) -> Prescription:
|
|
56
|
+
"""Analyze a single diagnostic issue string and return an actionable prescription."""
|
|
57
|
+
raw = str(issue or "").strip()
|
|
58
|
+
lower = raw.lower()
|
|
59
|
+
is_en = lang == "en"
|
|
60
|
+
|
|
61
|
+
# 1. Checks schema issues (e.g. checks must be a list of objects)
|
|
62
|
+
if "check" in lower and ("must be an object" in lower or "must be a list" in lower or "string" in lower or "type" in lower):
|
|
63
|
+
return Prescription(
|
|
64
|
+
kind=DiagnosticKind.SCHEMA_CHECKS_TYPE_ERROR,
|
|
65
|
+
code="ERR_CHECKS_TYPE",
|
|
66
|
+
summary="Field 'checks' has invalid type" if is_en else "Поле 'checks' заполнено неверным типом",
|
|
67
|
+
instruction=(
|
|
68
|
+
"The 'checks' field must not contain strings or arbitrary text. Provide an empty array 'checks': [] (or test execution objects)."
|
|
69
|
+
if is_en
|
|
70
|
+
else "Поле 'checks' не должно содержать строки или произвольный текст. Укажи пустой массив 'checks': [] (или объекты результатов тестов)."
|
|
71
|
+
),
|
|
72
|
+
fix_example='"checks": []',
|
|
73
|
+
lang=lang,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# 2. Risks schema issues
|
|
77
|
+
if "risk" in lower and ("must be a list" in lower or "not-a-list" in lower or "type" in lower):
|
|
78
|
+
return Prescription(
|
|
79
|
+
kind=DiagnosticKind.SCHEMA_RISKS_TYPE_ERROR,
|
|
80
|
+
code="ERR_RISKS_TYPE",
|
|
81
|
+
summary="Field 'risks' must be a list" if is_en else "Поле 'risks' должно быть списком",
|
|
82
|
+
instruction=(
|
|
83
|
+
"The 'risks' field must be an array of strings or an empty array 'risks': []."
|
|
84
|
+
if is_en
|
|
85
|
+
else "Поле 'risks' должно быть массивом строк или пустым массивом 'risks': []."
|
|
86
|
+
),
|
|
87
|
+
fix_example='"risks": []',
|
|
88
|
+
lang=lang,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# 3. Tool Policy: Dual format (both patch and edits)
|
|
92
|
+
if "not both" in lower or ("patch" in lower and "edits" in lower and "both" in lower):
|
|
93
|
+
return Prescription(
|
|
94
|
+
kind=DiagnosticKind.TOOL_DUAL_PATCH_AND_EDITS,
|
|
95
|
+
code="ERR_DUAL_FORMAT",
|
|
96
|
+
summary="Both patch and edits provided" if is_en else "Переданы одновременно и patch, и edits",
|
|
97
|
+
instruction=(
|
|
98
|
+
"Do not provide 'patch' and 'edits' simultaneously. Provide ONLY the 'edits' list with structure [{\"file\": \"...\", \"search\": \"...\", \"replace\": \"...\"}], omitting the 'patch' field."
|
|
99
|
+
if is_en
|
|
100
|
+
else "Не передавай 'patch' и 'edits' одновременно. Передай ТОЛЬКО список 'edits' со структурой [{\"file\": \"...\", \"search\": \"...\", \"replace\": \"...\"}], удалив поле 'patch'."
|
|
101
|
+
),
|
|
102
|
+
fix_example='{"edits": [{"file": "src/a.py", "search": "old_code", "replace": "new_code"}]}',
|
|
103
|
+
lang=lang,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# 4. Tool Policy: Missing patch/edits
|
|
107
|
+
if "must be provided" in lower or "no patch nor edits" in lower or "no resolved patch" in lower:
|
|
108
|
+
return Prescription(
|
|
109
|
+
kind=DiagnosticKind.TOOL_MISSING_PATCH_AND_EDITS,
|
|
110
|
+
code="ERR_MISSING_CHANGES",
|
|
111
|
+
summary="No changes provided" if is_en else "Не указаны изменения",
|
|
112
|
+
instruction=(
|
|
113
|
+
"You must provide the list of changes in 'edits': [{\"file\": \"...\", \"search\": \"...\", \"replace\": \"...\"}]."
|
|
114
|
+
if is_en
|
|
115
|
+
else "Необходимо передать список изменений 'edits': [{\"file\": \"...\", \"search\": \"...\", \"replace\": \"...\"}]."
|
|
116
|
+
),
|
|
117
|
+
fix_example='{"edits": [{"file": "src/a.py", "search": "old_code", "replace": "new_code"}]}',
|
|
118
|
+
lang=lang,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# 5. File Allowlist violations
|
|
122
|
+
if "allowlist" in lower or "outside" in lower or "forbidden" in lower:
|
|
123
|
+
file_match = re.search(r"([a-zA-Z0-9_\-\./\\]+\.[a-zA-Z0-9]+)", raw)
|
|
124
|
+
filename = file_match.group(1) if file_match else ("the specified file" if is_en else "указанный файл")
|
|
125
|
+
return Prescription(
|
|
126
|
+
kind=DiagnosticKind.TOOL_FORBIDDEN_FILE,
|
|
127
|
+
code="ERR_FILE_FORBIDDEN",
|
|
128
|
+
summary=f"File {filename} is outside task allowlist" if is_en else f"Файл {filename} вне task allowlist",
|
|
129
|
+
instruction=(
|
|
130
|
+
f"File '{filename}' is not in the task allowlist. Work only on allowlisted files."
|
|
131
|
+
if is_en
|
|
132
|
+
else f"Файл '{filename}' не входит в разрешенный список задачи. Работай только с файлами из task allowlist."
|
|
133
|
+
),
|
|
134
|
+
lang=lang,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# 6. Search block not found
|
|
138
|
+
if "search block not found" in lower or ("search" in lower and "not found" in lower):
|
|
139
|
+
file_match = re.search(r":\s*([a-zA-Z0-9_\-\./\\]+)", raw)
|
|
140
|
+
filename = file_match.group(1) if file_match else ("file" if is_en else "файле")
|
|
141
|
+
return Prescription(
|
|
142
|
+
kind=DiagnosticKind.SEARCH_BLOCK_NOT_FOUND,
|
|
143
|
+
code="ERR_SEARCH_NOT_FOUND",
|
|
144
|
+
summary=f"Search block not found in {filename}" if is_en else f"Блок search не найден в {filename}",
|
|
145
|
+
instruction=(
|
|
146
|
+
f"The 'search' block does not match the content of '{filename}'. Call the read_file tool and copy exact lines character-for-character, including leading whitespace and indentation."
|
|
147
|
+
if is_en
|
|
148
|
+
else f"Блок 'search' не совпадает с содержимым файла '{filename}'. Вызови инструмент read_file и скопируй точные строки символ-в-символ, включая пробелы и отступы в начале каждой строки."
|
|
149
|
+
),
|
|
150
|
+
lang=lang,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# 7. Search not line-aligned
|
|
154
|
+
if "line-aligned" in lower or "line aligned" in lower:
|
|
155
|
+
return Prescription(
|
|
156
|
+
kind=DiagnosticKind.SEARCH_NOT_LINE_ALIGNED,
|
|
157
|
+
code="ERR_SEARCH_ALIGNMENT",
|
|
158
|
+
summary="Search block is not line-aligned" if is_en else "Блок search не выровнен по границам строк",
|
|
159
|
+
instruction=(
|
|
160
|
+
"The 'search' block must begin at the very start of a line and end at a line boundary (do not slice fragments from the middle of a line)."
|
|
161
|
+
if is_en
|
|
162
|
+
else "Блок 'search' должен начинаться с самого начала строки файла и заканчиваться концом строки (не вырезай фрагменты из середины строки)."
|
|
163
|
+
),
|
|
164
|
+
lang=lang,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
# 8. Search ambiguous
|
|
168
|
+
if "ambiguous" in lower:
|
|
169
|
+
return Prescription(
|
|
170
|
+
kind=DiagnosticKind.SEARCH_AMBIGUOUS,
|
|
171
|
+
code="ERR_SEARCH_AMBIGUOUS",
|
|
172
|
+
summary="Search block matches multiple locations" if is_en else "Блок search встречается в файле несколько раз",
|
|
173
|
+
instruction=(
|
|
174
|
+
"The 'search' block is ambiguous (multiple matches found). Include 2-3 unique lines of surrounding context above and below the replacement."
|
|
175
|
+
if is_en
|
|
176
|
+
else "Блок 'search' неоднозначен (найдено несколько совпадений). Добавь 2-3 уникальные строки контекста выше и ниже заменяемого кода."
|
|
177
|
+
),
|
|
178
|
+
lang=lang,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
# 9. Duplicate edit file
|
|
182
|
+
if "duplicate edit file" in lower:
|
|
183
|
+
return Prescription(
|
|
184
|
+
kind=DiagnosticKind.DUPLICATE_EDIT_FILE,
|
|
185
|
+
code="ERR_DUPLICATE_FILE_EDIT",
|
|
186
|
+
summary="Multiple edit blocks for the same file" if is_en else "Несколько блоков правок для одного файла",
|
|
187
|
+
instruction=(
|
|
188
|
+
"Consolidate all edits for this file into a single 'search'/'replace' block."
|
|
189
|
+
if is_en
|
|
190
|
+
else "Объедини все изменения в одном файле в единый блок 'search'/'replace'."
|
|
191
|
+
),
|
|
192
|
+
lang=lang,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
# 10. Corrupt hunk / diff syntax issues
|
|
196
|
+
if "hunk" in lower or "corrupt patch" in lower or "invalid hunk" in lower or "diff header" in lower:
|
|
197
|
+
return Prescription(
|
|
198
|
+
kind=DiagnosticKind.DIFF_CORRUPT_HUNK,
|
|
199
|
+
code="ERR_DIFF_SYNTAX",
|
|
200
|
+
summary="Invalid diff / hunk header syntax" if is_en else "Неверный синтаксис diff / hunk header",
|
|
201
|
+
instruction=(
|
|
202
|
+
"Syntax error in diff hunk header (line number mismatch). It is recommended to use the 'edits' format with [{file, search, replace}], where line numbers are not required."
|
|
203
|
+
if is_en
|
|
204
|
+
else "Ошибка синтаксиса в diff hunk header (не совпали номера строк). Рекомендуется использовать формат 'edits' со списком {file, search, replace}, где номера строк не требуются."
|
|
205
|
+
),
|
|
206
|
+
fix_example='"edits": [{"file": "src/a.py", "search": "...", "replace": "..."}]',
|
|
207
|
+
lang=lang,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# 11. Patch does not apply cleanly
|
|
211
|
+
if "cleanly" in lower or "apply" in lower:
|
|
212
|
+
return Prescription(
|
|
213
|
+
kind=DiagnosticKind.DIFF_APPLY_FAILED,
|
|
214
|
+
code="ERR_PATCH_APPLY",
|
|
215
|
+
summary="Patch does not apply cleanly to current files" if is_en else "Патч не накладывается на текущее состояние файлов",
|
|
216
|
+
instruction=(
|
|
217
|
+
"Patch does not apply to the current file state. Call read_file to inspect fresh content and formulate SEARCH/REPLACE in 'edits'."
|
|
218
|
+
if is_en
|
|
219
|
+
else "Патч не накладывается на актуальную версию файла. Вызови read_file, чтобы получить свежий код, и сформируй SEARCH/REPLACE блок в поле 'edits'."
|
|
220
|
+
),
|
|
221
|
+
lang=lang,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
# Fallback generic prescription
|
|
225
|
+
return Prescription(
|
|
226
|
+
kind=DiagnosticKind.GENERIC_DIAGNOSTIC,
|
|
227
|
+
code="ERR_GENERIC",
|
|
228
|
+
summary=raw,
|
|
229
|
+
instruction=raw,
|
|
230
|
+
lang=lang,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def prescribe_all(
|
|
235
|
+
issues: Sequence[str],
|
|
236
|
+
*,
|
|
237
|
+
lang: str = "ru",
|
|
238
|
+
context: Optional[Mapping[str, Any]] = None,
|
|
239
|
+
) -> str:
|
|
240
|
+
"""Build a unified, de-duplicated prescription string from multiple issues."""
|
|
241
|
+
if not issues:
|
|
242
|
+
return "Please fix response structure and return valid JSON." if lang == "en" else "Исправь структуру ответа и верни корректный JSON."
|
|
243
|
+
prescriptions = [prescribe_issue(issue, lang=lang, context=context) for issue in issues]
|
|
244
|
+
formatted = [p.format_message() for p in prescriptions]
|
|
245
|
+
# Deduplicate preserving order
|
|
246
|
+
unique_messages = list(dict.fromkeys(formatted))
|
|
247
|
+
return " ".join(unique_messages)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def json_syntax_prescription(error: Optional[str] = None, *, lang: str = "ru") -> str:
|
|
251
|
+
"""Return an exact prescriptive prompt when a model outputs malformed non-JSON."""
|
|
252
|
+
err_detail = f" ({error})" if error else ""
|
|
253
|
+
if lang == "en":
|
|
254
|
+
return (
|
|
255
|
+
f"JSON SYNTAX ERROR: Your response could not be parsed as valid JSON{err_detail}. "
|
|
256
|
+
"Return STRICTLY one JSON object without markdown fences (no ```json), without leading or trailing prose: "
|
|
257
|
+
'{"status": "candidate", "summary": "brief summary", "patch": "", "checks": [], "risks": []} '
|
|
258
|
+
'or with "edits": [{"file": "...", "search": "...", "replace": "..."}].'
|
|
259
|
+
)
|
|
260
|
+
return (
|
|
261
|
+
f"ОШИБКА JSON: Твой ответ не удалось разобрать как валидный JSON{err_detail}. "
|
|
262
|
+
"Верни СТРОГО один JSON-объект без markdown-разметки (без ```json), без текста до и после: "
|
|
263
|
+
'{"status": "candidate", "summary": "краткое описание", "patch": "", "checks": [], "risks": []} '
|
|
264
|
+
'или вместо "patch" передай "edits": [{"file": "...", "search": "...", "replace": "..."}].'
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def tool_policy_prescription(tool_name: str, error: str, *, lang: str = "ru") -> dict[str, Any]:
|
|
269
|
+
"""Return a structured error payload with code, summary, and actionable hint for tool policy errors."""
|
|
270
|
+
prescription = prescribe_issue(error, lang=lang)
|
|
271
|
+
return {
|
|
272
|
+
"ok": False,
|
|
273
|
+
"status": "error",
|
|
274
|
+
"error_code": prescription.code,
|
|
275
|
+
"error": error,
|
|
276
|
+
"hint": prescription.instruction,
|
|
277
|
+
"diagnostic_kind": prescription.kind.value,
|
|
278
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""Named model profiles kept separate from controller logic."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import replace
|
|
6
|
+
|
|
7
|
+
from .ollama_adapter import ModelProfile
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
_PROFILES = {
|
|
11
|
+
"qwen2.5-1.5b": ModelProfile(
|
|
12
|
+
name="qwen2.5-1.5b",
|
|
13
|
+
model="qwen2.5:1.5b",
|
|
14
|
+
think=False,
|
|
15
|
+
temperature=0,
|
|
16
|
+
num_ctx=4096,
|
|
17
|
+
num_predict=256,
|
|
18
|
+
keep_alive="10m",
|
|
19
|
+
max_context_length=32_768,
|
|
20
|
+
),
|
|
21
|
+
"qwen2.5-coder": ModelProfile(
|
|
22
|
+
name="qwen2.5-coder",
|
|
23
|
+
model="qwen2.5-coder:latest",
|
|
24
|
+
think=False,
|
|
25
|
+
temperature=0,
|
|
26
|
+
num_ctx=8192,
|
|
27
|
+
num_predict=512,
|
|
28
|
+
keep_alive="10m",
|
|
29
|
+
max_context_length=32_768,
|
|
30
|
+
),
|
|
31
|
+
"ornith-9b": ModelProfile(
|
|
32
|
+
name="ornith-9b",
|
|
33
|
+
model="codex-ornith-9b:latest",
|
|
34
|
+
think=False,
|
|
35
|
+
temperature=0,
|
|
36
|
+
num_ctx=8192,
|
|
37
|
+
num_predict=512,
|
|
38
|
+
keep_alive="10m",
|
|
39
|
+
max_context_length=262_144,
|
|
40
|
+
),
|
|
41
|
+
"qwen3-coder-30b": ModelProfile(
|
|
42
|
+
name="qwen3-coder-30b",
|
|
43
|
+
model="codex-qwen3-coder-30b:latest",
|
|
44
|
+
think=False,
|
|
45
|
+
temperature=0,
|
|
46
|
+
num_ctx=8192,
|
|
47
|
+
num_predict=512,
|
|
48
|
+
keep_alive="10m",
|
|
49
|
+
max_context_length=262_144,
|
|
50
|
+
),
|
|
51
|
+
"devstral-small-2-24b": ModelProfile(
|
|
52
|
+
name="devstral-small-2-24b",
|
|
53
|
+
model="codex-devstral-small-2-24b:latest",
|
|
54
|
+
think=False,
|
|
55
|
+
temperature=0,
|
|
56
|
+
num_ctx=8192,
|
|
57
|
+
num_predict=512,
|
|
58
|
+
keep_alive="10m",
|
|
59
|
+
max_context_length=393_216,
|
|
60
|
+
),
|
|
61
|
+
"qwen3-8b-q6k": ModelProfile(
|
|
62
|
+
name="qwen3-8b-q6k",
|
|
63
|
+
model="codex-qwen3-8b-q6k:latest",
|
|
64
|
+
think=False,
|
|
65
|
+
temperature=0.7,
|
|
66
|
+
top_p=0.80,
|
|
67
|
+
presence_penalty=1.5,
|
|
68
|
+
num_ctx=8192,
|
|
69
|
+
num_predict=512,
|
|
70
|
+
keep_alive="10m",
|
|
71
|
+
max_context_length=32_768,
|
|
72
|
+
),
|
|
73
|
+
"qwen3-coder-30b-iq2": ModelProfile(
|
|
74
|
+
name="qwen3-coder-30b-iq2",
|
|
75
|
+
model="codex-qwen3-coder-30b-ud-iq2:latest",
|
|
76
|
+
think=False,
|
|
77
|
+
temperature=0,
|
|
78
|
+
num_ctx=8192,
|
|
79
|
+
num_predict=512,
|
|
80
|
+
keep_alive="10m",
|
|
81
|
+
max_context_length=262_144,
|
|
82
|
+
),
|
|
83
|
+
"qwen3-coder-30b-q4": ModelProfile(
|
|
84
|
+
name="qwen3-coder-30b-q4",
|
|
85
|
+
model="codex-qwen3-coder-30b-ud-q4:latest",
|
|
86
|
+
think=False,
|
|
87
|
+
temperature=0,
|
|
88
|
+
num_ctx=8192,
|
|
89
|
+
num_predict=512,
|
|
90
|
+
keep_alive="10m",
|
|
91
|
+
max_context_length=262_144,
|
|
92
|
+
),
|
|
93
|
+
"qwen2.5-coder-14b-q6k": ModelProfile(
|
|
94
|
+
name="qwen2.5-coder-14b-q6k",
|
|
95
|
+
model="codex-qwen2.5-coder-14b-q6k:latest",
|
|
96
|
+
think=False,
|
|
97
|
+
temperature=0,
|
|
98
|
+
num_ctx=8192,
|
|
99
|
+
num_predict=512,
|
|
100
|
+
keep_alive="10m",
|
|
101
|
+
max_context_length=32_768,
|
|
102
|
+
),
|
|
103
|
+
"nemotron-30b-mxfp4": ModelProfile(
|
|
104
|
+
name="nemotron-30b-mxfp4",
|
|
105
|
+
model="codex-nemotron-30b-mxfp4:latest",
|
|
106
|
+
think=False,
|
|
107
|
+
temperature=0,
|
|
108
|
+
num_ctx=8192,
|
|
109
|
+
num_predict=512,
|
|
110
|
+
keep_alive="10m",
|
|
111
|
+
max_context_length=131_072,
|
|
112
|
+
),
|
|
113
|
+
"qwen3.8-27b-q4": ModelProfile(
|
|
114
|
+
name="qwen3.8-27b-q4",
|
|
115
|
+
model="codex-qwen3.8-27b-q4:latest",
|
|
116
|
+
think=False,
|
|
117
|
+
temperature=0,
|
|
118
|
+
num_ctx=8192,
|
|
119
|
+
num_predict=512,
|
|
120
|
+
keep_alive="10m",
|
|
121
|
+
max_context_length=262_144,
|
|
122
|
+
),
|
|
123
|
+
"qwen3.8-27b-q5": ModelProfile(
|
|
124
|
+
name="qwen3.8-27b-q5",
|
|
125
|
+
model="codex-qwen3.8-27b-q5:latest",
|
|
126
|
+
think=False,
|
|
127
|
+
temperature=0,
|
|
128
|
+
num_ctx=8192,
|
|
129
|
+
num_predict=512,
|
|
130
|
+
keep_alive="10m",
|
|
131
|
+
max_context_length=262_144,
|
|
132
|
+
),
|
|
133
|
+
"qwen3.8-27b-think": ModelProfile(
|
|
134
|
+
name="qwen3.8-27b-think",
|
|
135
|
+
model="codex-qwen3.8-27b-q4:latest",
|
|
136
|
+
think=True,
|
|
137
|
+
temperature=1.0,
|
|
138
|
+
num_ctx=8192,
|
|
139
|
+
num_predict=1024,
|
|
140
|
+
keep_alive="10m",
|
|
141
|
+
max_context_length=262_144,
|
|
142
|
+
),
|
|
143
|
+
"qwen2.5-coder-7b-q4": ModelProfile(
|
|
144
|
+
name="qwen2.5-coder-7b-q4",
|
|
145
|
+
model="codex-qwen2.5-coder-7b-q4:latest",
|
|
146
|
+
think=False,
|
|
147
|
+
temperature=0,
|
|
148
|
+
num_ctx=8192,
|
|
149
|
+
num_predict=512,
|
|
150
|
+
keep_alive="10m",
|
|
151
|
+
max_context_length=32_768,
|
|
152
|
+
),
|
|
153
|
+
"qwen2.5-coder-7b-q5": ModelProfile(
|
|
154
|
+
name="qwen2.5-coder-7b-q5",
|
|
155
|
+
model="local-qwen2.5-coder-7b-q5:latest",
|
|
156
|
+
think=False,
|
|
157
|
+
temperature=0,
|
|
158
|
+
num_ctx=8192,
|
|
159
|
+
num_predict=512,
|
|
160
|
+
keep_alive="10m",
|
|
161
|
+
max_context_length=32_768,
|
|
162
|
+
),
|
|
163
|
+
"gemma4-e4b-q4": ModelProfile(
|
|
164
|
+
name="gemma4-e4b-q4",
|
|
165
|
+
model="codex-gemma4-e4b-q4:latest",
|
|
166
|
+
think=False,
|
|
167
|
+
temperature=0,
|
|
168
|
+
num_ctx=8192,
|
|
169
|
+
num_predict=512,
|
|
170
|
+
keep_alive="10m",
|
|
171
|
+
max_context_length=131_072,
|
|
172
|
+
),
|
|
173
|
+
"gemma4-e4b-q5": ModelProfile(
|
|
174
|
+
name="gemma4-e4b-q5",
|
|
175
|
+
model="local-gemma4-e4b-q5:latest",
|
|
176
|
+
think=False,
|
|
177
|
+
temperature=0,
|
|
178
|
+
num_ctx=8192,
|
|
179
|
+
num_predict=512,
|
|
180
|
+
keep_alive="10m",
|
|
181
|
+
max_context_length=131_072,
|
|
182
|
+
),
|
|
183
|
+
"gemma4-e2b-q4": ModelProfile(
|
|
184
|
+
name="gemma4-e2b-q4",
|
|
185
|
+
model="codex-gemma4-e2b-q4:latest",
|
|
186
|
+
think=False,
|
|
187
|
+
temperature=0,
|
|
188
|
+
num_ctx=8192,
|
|
189
|
+
num_predict=512,
|
|
190
|
+
keep_alive="10m",
|
|
191
|
+
max_context_length=131_072,
|
|
192
|
+
),
|
|
193
|
+
"gemma4-e2b-q8": ModelProfile(
|
|
194
|
+
name="gemma4-e2b-q8",
|
|
195
|
+
model="local-gemma4-e2b-q8:latest",
|
|
196
|
+
think=False,
|
|
197
|
+
temperature=0,
|
|
198
|
+
num_ctx=8192,
|
|
199
|
+
num_predict=512,
|
|
200
|
+
keep_alive="10m",
|
|
201
|
+
max_context_length=131_072,
|
|
202
|
+
),
|
|
203
|
+
"ling-3.0-tiny-q6k": ModelProfile(
|
|
204
|
+
name="ling-3.0-tiny-q6k",
|
|
205
|
+
model="ling-3.0-tiny-q6k",
|
|
206
|
+
endpoint="http://127.0.0.1:8080",
|
|
207
|
+
provider="openai",
|
|
208
|
+
think=False,
|
|
209
|
+
temperature=0,
|
|
210
|
+
num_ctx=8192,
|
|
211
|
+
num_predict=2048,
|
|
212
|
+
keep_alive="10m",
|
|
213
|
+
max_context_length=262_144,
|
|
214
|
+
stop=("<|role_end|>", "<role>"),
|
|
215
|
+
system_contract=(
|
|
216
|
+
"You are a local coding sub-agent assigned to a single atomic task.\n"
|
|
217
|
+
"Operate strictly within the provided task envelope.\n"
|
|
218
|
+
"Do not invent missing context or assumptions.\n"
|
|
219
|
+
"Do not claim to have run tests or modified files without tool evidence.\n"
|
|
220
|
+
"Use only the provided tools.\n"
|
|
221
|
+
"For propose_patch prefer SEARCH/REPLACE (edits with file+search+replace). In search copy old code byte-for-byte including leading indentation.\n"
|
|
222
|
+
"Return only structured JSON upon completion: {\"status\":\"candidate\",\"summary\":\"...\",\"patch\":\"\",\"checks\":[],\"risks\":[]} or with \"edits\":[{\"file\":\"...\",\"search\":\"...\",\"replace\":\"...\"}]."
|
|
223
|
+
),
|
|
224
|
+
),
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def list_profiles() -> tuple[str, ...]:
|
|
230
|
+
return tuple(sorted(_PROFILES))
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def get_profile(name: str, **overrides: object) -> ModelProfile:
|
|
234
|
+
try:
|
|
235
|
+
profile = _PROFILES[name]
|
|
236
|
+
except KeyError as error:
|
|
237
|
+
available = ", ".join(list_profiles())
|
|
238
|
+
raise ValueError(f"unknown model profile {name!r}; available: {available}") from error
|
|
239
|
+
return replace(profile, **overrides)
|