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,155 @@
|
|
|
1
|
+
"""Harness State Machine and Context Manager (R14).
|
|
2
|
+
|
|
3
|
+
Provides deterministic state tracking and stateless context assembly for local models.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from .task import TaskEnvelope
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class HarnessState:
|
|
17
|
+
"""Explicit state representation of a delegation session."""
|
|
18
|
+
|
|
19
|
+
task: TaskEnvelope
|
|
20
|
+
turn: int = 1
|
|
21
|
+
observed_files: dict[str, str] = field(default_factory=dict)
|
|
22
|
+
latest_tool_name: str | None = None
|
|
23
|
+
latest_tool_arguments: dict[str, Any] | None = None
|
|
24
|
+
latest_tool_result: dict[str, Any] | None = None
|
|
25
|
+
latest_tool_call_id: str | None = None
|
|
26
|
+
active_prescription: str | None = None
|
|
27
|
+
last_patch: str | None = None
|
|
28
|
+
observed_checks: dict[str, dict[str, Any]] = field(default_factory=dict)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ContextAssembler:
|
|
32
|
+
"""Assembles a clean, stateless prompt context envelope from current HarnessState."""
|
|
33
|
+
|
|
34
|
+
def assemble(
|
|
35
|
+
self,
|
|
36
|
+
state: HarnessState,
|
|
37
|
+
system_contract: str,
|
|
38
|
+
*,
|
|
39
|
+
limits: dict[str, Any] | None = None,
|
|
40
|
+
) -> list[dict[str, Any]]:
|
|
41
|
+
task = state.task
|
|
42
|
+
envelope_payload: dict[str, Any] = {
|
|
43
|
+
"id": task.id,
|
|
44
|
+
"goal": task.goal,
|
|
45
|
+
"files": list(task.files),
|
|
46
|
+
"context": task.context,
|
|
47
|
+
"constraints": list(task.constraints),
|
|
48
|
+
"checks": list(task.checks),
|
|
49
|
+
"acceptance": list(task.acceptance),
|
|
50
|
+
}
|
|
51
|
+
if limits:
|
|
52
|
+
envelope_payload["limits"] = limits
|
|
53
|
+
|
|
54
|
+
messages: list[dict[str, Any]] = [
|
|
55
|
+
{"role": "system", "content": system_contract},
|
|
56
|
+
{
|
|
57
|
+
"role": "user",
|
|
58
|
+
"content": json.dumps(envelope_payload, ensure_ascii=False, separators=(",", ":")),
|
|
59
|
+
},
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
if state.latest_tool_name and state.latest_tool_result is not None:
|
|
63
|
+
# Synthetic pairing compliant with OpenAI / Ollama tool calling
|
|
64
|
+
tool_args = state.latest_tool_arguments or {}
|
|
65
|
+
assistant_tool_call = {
|
|
66
|
+
"id": state.latest_tool_call_id or "call_0",
|
|
67
|
+
"type": "function",
|
|
68
|
+
"function": {
|
|
69
|
+
"name": state.latest_tool_name,
|
|
70
|
+
"arguments": json.dumps(tool_args, ensure_ascii=False),
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
tool_res_message: dict[str, Any] = {
|
|
74
|
+
"role": "tool",
|
|
75
|
+
"tool_name": state.latest_tool_name,
|
|
76
|
+
"content": json.dumps(state.latest_tool_result, ensure_ascii=False),
|
|
77
|
+
}
|
|
78
|
+
if state.latest_tool_call_id:
|
|
79
|
+
tool_res_message["tool_call_id"] = state.latest_tool_call_id
|
|
80
|
+
|
|
81
|
+
messages.append({"role": "assistant", "tool_calls": [assistant_tool_call]})
|
|
82
|
+
messages.append(tool_res_message)
|
|
83
|
+
|
|
84
|
+
if state.active_prescription:
|
|
85
|
+
messages.append({
|
|
86
|
+
"role": "user",
|
|
87
|
+
"content": json.dumps(
|
|
88
|
+
{
|
|
89
|
+
"error": "CANDIDATE_VALIDATION_FAILED",
|
|
90
|
+
"instruction": f"ОШИБКА ВАЛИДАЦИИ: {state.active_prescription} Исправь эти поля и верни скорректированный JSON-объект.",
|
|
91
|
+
},
|
|
92
|
+
ensure_ascii=False,
|
|
93
|
+
),
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
return messages
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _message_bytes(messages: list[dict[str, Any]]) -> int:
|
|
100
|
+
return len(json.dumps(messages, ensure_ascii=False).encode("utf-8"))
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def compact_tool_exchanges(
|
|
104
|
+
messages: list[dict[str, Any]], max_bytes: int
|
|
105
|
+
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
|
106
|
+
"""Compacts message list by evicting older tool-call pairs while respecting max_bytes."""
|
|
107
|
+
current = list(messages)
|
|
108
|
+
dropped_all: list[dict[str, Any]] = []
|
|
109
|
+
|
|
110
|
+
while _message_bytes(current) > max_bytes:
|
|
111
|
+
# Find tool exchange blocks [start, end)
|
|
112
|
+
blocks: list[tuple[int, int]] = []
|
|
113
|
+
i = 2
|
|
114
|
+
n = len(current)
|
|
115
|
+
while i < n:
|
|
116
|
+
msg = current[i]
|
|
117
|
+
if msg.get("role") == "assistant" and msg.get("tool_calls"):
|
|
118
|
+
start = i
|
|
119
|
+
j = i + 1
|
|
120
|
+
while j < n and current[j].get("role") == "tool":
|
|
121
|
+
j += 1
|
|
122
|
+
blocks.append((start, j))
|
|
123
|
+
i = j
|
|
124
|
+
else:
|
|
125
|
+
i += 1
|
|
126
|
+
|
|
127
|
+
if len(blocks) <= 1:
|
|
128
|
+
break
|
|
129
|
+
|
|
130
|
+
# Evict oldest block
|
|
131
|
+
start, end = blocks[0]
|
|
132
|
+
dropped = current[start:end]
|
|
133
|
+
dropped_all.extend(dropped)
|
|
134
|
+
del current[start:end]
|
|
135
|
+
|
|
136
|
+
return current, dropped_all
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def purge_diff_residues(messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
140
|
+
"""Purge raw invalid patch strings from previous assistant messages in the history."""
|
|
141
|
+
purged: list[dict[str, Any]] = []
|
|
142
|
+
for msg in messages:
|
|
143
|
+
if msg.get("role") == "assistant" and isinstance(msg.get("content"), str):
|
|
144
|
+
try:
|
|
145
|
+
data = json.loads(msg["content"])
|
|
146
|
+
if isinstance(data, dict) and data.get("patch"):
|
|
147
|
+
data["patch"] = "<invalid_patch_omitted>"
|
|
148
|
+
msg_copy = dict(msg)
|
|
149
|
+
msg_copy["content"] = json.dumps(data, ensure_ascii=False)
|
|
150
|
+
purged.append(msg_copy)
|
|
151
|
+
continue
|
|
152
|
+
except (json.JSONDecodeError, TypeError):
|
|
153
|
+
pass
|
|
154
|
+
purged.append(msg)
|
|
155
|
+
return purged
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Bounded tool loop between a task envelope and an Ollama-compatible model."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ..validators import apply_patch
|
|
6
|
+
from ._constants import SYSTEM_CONTRACT, TOOL_DEFINITIONS, ModelClient
|
|
7
|
+
from ._controller import Controller
|
|
8
|
+
from ._post_apply import mediated_apply, run_post_apply_checks
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"Controller",
|
|
12
|
+
"ModelClient",
|
|
13
|
+
"SYSTEM_CONTRACT",
|
|
14
|
+
"TOOL_DEFINITIONS",
|
|
15
|
+
"mediated_apply",
|
|
16
|
+
"run_post_apply_checks",
|
|
17
|
+
"apply_patch",
|
|
18
|
+
]
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Protocol
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
SYSTEM_CONTRACT = """Ты локальный coding-subagent для одной атомарной задачи.
|
|
7
|
+
Работай только в пределах task envelope.
|
|
8
|
+
Не выдумывай отсутствующий контекст.
|
|
9
|
+
Не утверждай, что запускал тесты или менял файлы без результата инструмента.
|
|
10
|
+
Используй только предоставленные инструменты.
|
|
11
|
+
Для файлов используй только относительные пути из task allowlist; абсолютные пути и '..' запрещены.
|
|
12
|
+
Если данных не хватает, задай один точный вопрос.
|
|
13
|
+
Патч должен быть минимальным и затрагивать только разрешённые файлы.
|
|
14
|
+
Для propose_patch предпочтителен SEARCH/REPLACE (edits: file+search+replace, номера строк не нужны) либо полный unified diff с корректными hunk headers. Применимость проверяют validator и git. В search копируй старый код точно, включая ведущие пробелы каждой строки.
|
|
15
|
+
После завершения верни один JSON без markdown: {"status":"candidate","summary":"...","patch":"<diff>","checks":[],"risks":[]}. Вместо "patch" можно "edits":[{"file","search","replace"}]. Патч из propose_patch можно не дублировать."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
TOOL_DEFINITIONS: list[dict[str, Any]] = [
|
|
19
|
+
{
|
|
20
|
+
"type": "function",
|
|
21
|
+
"function": {
|
|
22
|
+
"name": "list_files",
|
|
23
|
+
"description": "List bounded files below a workspace-relative directory.",
|
|
24
|
+
"parameters": {
|
|
25
|
+
"type": "object",
|
|
26
|
+
"properties": {"path": {"type": "string"}},
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"type": "function",
|
|
32
|
+
"function": {
|
|
33
|
+
"name": "read_file",
|
|
34
|
+
"description": "Read one UTF-8 file from the task allowlist.",
|
|
35
|
+
"parameters": {
|
|
36
|
+
"type": "object",
|
|
37
|
+
"properties": {"path": {"type": "string"}},
|
|
38
|
+
"required": ["path"],
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"type": "function",
|
|
44
|
+
"function": {
|
|
45
|
+
"name": "search_text",
|
|
46
|
+
"description": "Search text in bounded allowlisted files.",
|
|
47
|
+
"parameters": {
|
|
48
|
+
"type": "object",
|
|
49
|
+
"properties": {
|
|
50
|
+
"query": {"type": "string"},
|
|
51
|
+
"paths": {"type": "array", "items": {"type": "string"}},
|
|
52
|
+
},
|
|
53
|
+
"required": ["query"],
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"type": "function",
|
|
59
|
+
"function": {
|
|
60
|
+
"name": "propose_patch",
|
|
61
|
+
"description": (
|
|
62
|
+
"Return a complete change proposal without writing files. "
|
|
63
|
+
"Prefer SEARCH/REPLACE: a list of edits, each with file+search+replace "
|
|
64
|
+
"(no line numbers needed). Copy search BYTE-FOR-BYTE from the file "
|
|
65
|
+
"including every leading space/indent of each line. Example: "
|
|
66
|
+
"{\"edits\":[{\"file\":\"src/a.py\",\"search\":\"def f(x):\\n return x+1\","
|
|
67
|
+
"\"replace\":\"def f(x):\\n return x+2\"}]}. "
|
|
68
|
+
"Alternatively provide one unified diff with diff --git, ---, +++ and "
|
|
69
|
+
"valid hunk headers. Use real newlines and relative allowlisted paths. "
|
|
70
|
+
"Applicability is checked by the controller-owned validator and git."
|
|
71
|
+
),
|
|
72
|
+
"parameters": {
|
|
73
|
+
"type": "object",
|
|
74
|
+
"properties": {
|
|
75
|
+
"patch": {"type": "string"},
|
|
76
|
+
"edits": {
|
|
77
|
+
"type": "array",
|
|
78
|
+
"items": {
|
|
79
|
+
"type": "object",
|
|
80
|
+
"properties": {
|
|
81
|
+
"file": {"type": "string"},
|
|
82
|
+
"search": {"type": "string"},
|
|
83
|
+
"replace": {"type": "string"},
|
|
84
|
+
},
|
|
85
|
+
"required": ["file", "search", "replace"],
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"type": "function",
|
|
94
|
+
"function": {
|
|
95
|
+
"name": "run_tests",
|
|
96
|
+
"description": "Run exactly one command from the task checks allowlist.",
|
|
97
|
+
"parameters": {
|
|
98
|
+
"type": "object",
|
|
99
|
+
"properties": {"command": {"type": "string"}},
|
|
100
|
+
"required": ["command"],
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class ModelClient(Protocol):
|
|
108
|
+
def chat(self, messages: list[dict[str, Any]], *, tools: list[dict[str, Any]]) -> dict[str, Any]: ...
|