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,405 @@
|
|
|
1
|
+
"""Transport-neutral, proposal-only entry point for bounded delegations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import copy
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
from collections import OrderedDict
|
|
10
|
+
from dataclasses import asdict, dataclass
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from threading import Event, RLock
|
|
13
|
+
from typing import Any, Callable, Mapping
|
|
14
|
+
|
|
15
|
+
from .atomizer import TaskBudget, preflight
|
|
16
|
+
from .controller import Controller, ModelClient, mediated_apply
|
|
17
|
+
from .controller._post_apply import apply_failure
|
|
18
|
+
from .ollama_adapter import ModelProfile, build_client
|
|
19
|
+
from .profiles import get_profile
|
|
20
|
+
from .stats import append_stats, default_stats_path
|
|
21
|
+
from .task import TaskEnvelope
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class DelegationRequest:
|
|
28
|
+
"""A host-approved request that is independent of any transport schema."""
|
|
29
|
+
|
|
30
|
+
request_id: str
|
|
31
|
+
workspace_ref: str
|
|
32
|
+
model_profile: str
|
|
33
|
+
task: TaskEnvelope
|
|
34
|
+
|
|
35
|
+
def __post_init__(self) -> None:
|
|
36
|
+
for name, value in (
|
|
37
|
+
("request_id", self.request_id),
|
|
38
|
+
("workspace_ref", self.workspace_ref),
|
|
39
|
+
("model_profile", self.model_profile),
|
|
40
|
+
):
|
|
41
|
+
if not isinstance(value, str) or not value.strip():
|
|
42
|
+
raise ValueError(f"delegation field '{name}' must be a non-empty string")
|
|
43
|
+
if not isinstance(self.task, TaskEnvelope):
|
|
44
|
+
raise ValueError("delegation field 'task' must be a TaskEnvelope")
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def from_mapping(cls, value: Mapping[str, Any]) -> "DelegationRequest":
|
|
48
|
+
if not isinstance(value, Mapping):
|
|
49
|
+
raise ValueError("delegation request must be an object")
|
|
50
|
+
task = value.get("task")
|
|
51
|
+
if not isinstance(task, Mapping):
|
|
52
|
+
raise ValueError("delegation field 'task' must be an object")
|
|
53
|
+
return cls(
|
|
54
|
+
request_id=value.get("request_id"),
|
|
55
|
+
workspace_ref=value.get("workspace_ref"),
|
|
56
|
+
model_profile=value.get("model_profile"),
|
|
57
|
+
task=TaskEnvelope.from_mapping(task),
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class _CachedResult:
|
|
63
|
+
fingerprint: str
|
|
64
|
+
completed: Event
|
|
65
|
+
result: dict[str, Any] | None = None
|
|
66
|
+
request: DelegationRequest | None = None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class DelegationService:
|
|
70
|
+
"""Direct adapter that resolves only host-registered workspaces and profiles.
|
|
71
|
+
|
|
72
|
+
Direct ``delegate`` remains proposal-only and does not accept an apply flag
|
|
73
|
+
or arbitrary paths. The separate mediated ``apply`` method is host-facing,
|
|
74
|
+
confirmation-bound and still keeps policy/result ownership in the controller
|
|
75
|
+
and service boundary.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
def __init__(
|
|
79
|
+
self,
|
|
80
|
+
workspaces: Mapping[str, str | Path],
|
|
81
|
+
*,
|
|
82
|
+
model_factory: Callable[[ModelProfile], ModelClient] = build_client,
|
|
83
|
+
max_turns: int = 4,
|
|
84
|
+
max_cached_results: int = 256,
|
|
85
|
+
preflight_budget: TaskBudget = TaskBudget(),
|
|
86
|
+
) -> None:
|
|
87
|
+
if max_turns <= 0:
|
|
88
|
+
raise ValueError("max_turns must be positive")
|
|
89
|
+
if max_cached_results <= 0:
|
|
90
|
+
raise ValueError("max_cached_results must be positive")
|
|
91
|
+
if not isinstance(preflight_budget, TaskBudget):
|
|
92
|
+
raise ValueError("preflight_budget must be a TaskBudget")
|
|
93
|
+
registered: dict[str, Path] = {}
|
|
94
|
+
for reference, raw_path in workspaces.items():
|
|
95
|
+
if not isinstance(reference, str) or not reference.strip():
|
|
96
|
+
raise ValueError("workspace references must be non-empty strings")
|
|
97
|
+
path = Path(raw_path).resolve()
|
|
98
|
+
if not path.is_dir():
|
|
99
|
+
raise ValueError(f"registered workspace {reference!r} is not a directory: {path}")
|
|
100
|
+
registered[reference] = path
|
|
101
|
+
self._workspaces = registered
|
|
102
|
+
self._apply_locks = {reference: RLock() for reference in registered}
|
|
103
|
+
self._model_factory = model_factory
|
|
104
|
+
self._max_turns = max_turns
|
|
105
|
+
self._max_cached_results = max_cached_results
|
|
106
|
+
self._preflight_budget = preflight_budget
|
|
107
|
+
self._cache: OrderedDict[tuple[str, str, str], _CachedResult] = OrderedDict()
|
|
108
|
+
self._cache_lock = RLock()
|
|
109
|
+
|
|
110
|
+
def delegate(
|
|
111
|
+
self,
|
|
112
|
+
caller_id: str,
|
|
113
|
+
request: DelegationRequest,
|
|
114
|
+
*,
|
|
115
|
+
cancel_event: Event | None = None,
|
|
116
|
+
completion_event: Event | None = None,
|
|
117
|
+
) -> dict[str, Any]:
|
|
118
|
+
"""Run one proposal-only delegation with caller-scoped idempotency."""
|
|
119
|
+
|
|
120
|
+
controller_started = Event()
|
|
121
|
+
try:
|
|
122
|
+
if not isinstance(caller_id, str) or not caller_id.strip():
|
|
123
|
+
return self._policy_failure("invalid_caller", "caller_id must be a non-empty string")
|
|
124
|
+
if not isinstance(request, DelegationRequest):
|
|
125
|
+
return self._policy_failure("invalid_request", "request must be a DelegationRequest")
|
|
126
|
+
|
|
127
|
+
cache_key = (caller_id, request.workspace_ref, request.request_id)
|
|
128
|
+
fingerprint = self._fingerprint(request)
|
|
129
|
+
with self._cache_lock:
|
|
130
|
+
cached = self._cache.get(cache_key)
|
|
131
|
+
if cached is None:
|
|
132
|
+
if len(self._cache) >= self._max_cached_results:
|
|
133
|
+
for stale_key, stale_record in self._cache.items():
|
|
134
|
+
if stale_record.completed.is_set():
|
|
135
|
+
self._cache.pop(stale_key)
|
|
136
|
+
break
|
|
137
|
+
else:
|
|
138
|
+
return self._policy_failure(
|
|
139
|
+
"idempotency_capacity",
|
|
140
|
+
"in-memory idempotency capacity is exhausted by active requests",
|
|
141
|
+
)
|
|
142
|
+
cached = _CachedResult(fingerprint=fingerprint, completed=Event(), request=request)
|
|
143
|
+
self._cache[cache_key] = cached
|
|
144
|
+
owner = True
|
|
145
|
+
elif cached.fingerprint != fingerprint:
|
|
146
|
+
return self._policy_failure(
|
|
147
|
+
"idempotency_conflict",
|
|
148
|
+
"request_id was already used with a different request payload",
|
|
149
|
+
)
|
|
150
|
+
else:
|
|
151
|
+
self._cache.move_to_end(cache_key)
|
|
152
|
+
owner = False
|
|
153
|
+
|
|
154
|
+
if not owner:
|
|
155
|
+
cached.completed.wait()
|
|
156
|
+
assert cached.result is not None
|
|
157
|
+
return copy.deepcopy(cached.result)
|
|
158
|
+
|
|
159
|
+
result: dict[str, Any] | None = None
|
|
160
|
+
started_ns = time.monotonic_ns()
|
|
161
|
+
try:
|
|
162
|
+
try:
|
|
163
|
+
result = self._execute(
|
|
164
|
+
request,
|
|
165
|
+
cancel_event=cancel_event,
|
|
166
|
+
completion_event=completion_event,
|
|
167
|
+
controller_started=controller_started,
|
|
168
|
+
)
|
|
169
|
+
except Exception as error:
|
|
170
|
+
import traceback
|
|
171
|
+
traceback.print_exc(file=sys.stderr)
|
|
172
|
+
result = self._policy_failure("controller_error", f"controller execution failed: {error}")
|
|
173
|
+
finally:
|
|
174
|
+
if result is None:
|
|
175
|
+
result = self._policy_failure("interrupted", "controller execution interrupted")
|
|
176
|
+
normalized = self._normalize_result(result)
|
|
177
|
+
append_stats(
|
|
178
|
+
default_stats_path(),
|
|
179
|
+
normalized,
|
|
180
|
+
model=request.model_profile,
|
|
181
|
+
latency_ns=time.monotonic_ns() - started_ns,
|
|
182
|
+
)
|
|
183
|
+
with self._cache_lock:
|
|
184
|
+
cached.result = copy.deepcopy(normalized)
|
|
185
|
+
cached.completed.set()
|
|
186
|
+
self._evict_completed_results()
|
|
187
|
+
self._mirror_desktop_task(request, normalized)
|
|
188
|
+
return copy.deepcopy(normalized)
|
|
189
|
+
finally:
|
|
190
|
+
if completion_event is not None and not controller_started.is_set():
|
|
191
|
+
completion_event.set()
|
|
192
|
+
|
|
193
|
+
def _mirror_desktop_task(self, request: DelegationRequest, result: Mapping[str, Any]) -> None:
|
|
194
|
+
"""Surface a completed delegation in the desktop task panel.
|
|
195
|
+
|
|
196
|
+
The desktop harness polls ``<workspace>/.local_agent_tasks.json``; MCP /
|
|
197
|
+
stdio / CLI delegations run in a separate process and would otherwise be
|
|
198
|
+
invisible there. This upstreams the same shared-file journal pattern as
|
|
199
|
+
:func:`append_stats` so every delegation shows up in one place.
|
|
200
|
+
|
|
201
|
+
# ponytail: best-effort; never raises. Cross-process writers have no
|
|
202
|
+
lock, but a lost update on a dashboard record is not worth a lock file.
|
|
203
|
+
"""
|
|
204
|
+
workspace = self._workspaces.get(request.workspace_ref)
|
|
205
|
+
if workspace is None:
|
|
206
|
+
return
|
|
207
|
+
path = Path(workspace) / ".local_agent_tasks.json"
|
|
208
|
+
try:
|
|
209
|
+
tasks = []
|
|
210
|
+
if path.exists():
|
|
211
|
+
try:
|
|
212
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
213
|
+
if isinstance(data, list):
|
|
214
|
+
tasks = data
|
|
215
|
+
except Exception:
|
|
216
|
+
tasks = []
|
|
217
|
+
status = "accepted" if result.get("status") in ("accepted", "candidate") else "failed"
|
|
218
|
+
error = result.get("error") or ""
|
|
219
|
+
if isinstance(error, dict):
|
|
220
|
+
error = error.get("message", "")
|
|
221
|
+
record = {
|
|
222
|
+
"id": request.request_id,
|
|
223
|
+
"goal": request.task.goal,
|
|
224
|
+
"files": list(request.task.files),
|
|
225
|
+
"checks": list(request.task.checks),
|
|
226
|
+
"profile": request.model_profile,
|
|
227
|
+
"status": status,
|
|
228
|
+
"created_at": time.time(),
|
|
229
|
+
"started_at": time.time(),
|
|
230
|
+
"finished_at": time.time(),
|
|
231
|
+
"summary": str(result.get("summary") or ""),
|
|
232
|
+
"patch": str(result.get("patch") or ""),
|
|
233
|
+
"checks_results": result.get("checks") if isinstance(result.get("checks"), list) else [],
|
|
234
|
+
"error": str(error),
|
|
235
|
+
}
|
|
236
|
+
tasks = [t for t in tasks if t.get("id") != record["id"]]
|
|
237
|
+
tasks.insert(0, record)
|
|
238
|
+
path.write_text(json.dumps(tasks[:100], indent=2, ensure_ascii=False), encoding="utf-8")
|
|
239
|
+
except Exception:
|
|
240
|
+
pass
|
|
241
|
+
|
|
242
|
+
def _execute(
|
|
243
|
+
self,
|
|
244
|
+
request: DelegationRequest,
|
|
245
|
+
*,
|
|
246
|
+
cancel_event: Event | None = None,
|
|
247
|
+
completion_event: Event | None = None,
|
|
248
|
+
controller_started: Event | None = None,
|
|
249
|
+
) -> dict[str, Any]:
|
|
250
|
+
workspace = self._workspaces.get(request.workspace_ref)
|
|
251
|
+
if workspace is None:
|
|
252
|
+
return self._policy_failure(
|
|
253
|
+
"unknown_workspace",
|
|
254
|
+
f"workspace_ref is not registered: {request.workspace_ref!r}",
|
|
255
|
+
)
|
|
256
|
+
report = preflight(request.task, self._preflight_budget)
|
|
257
|
+
if not report.accepted:
|
|
258
|
+
return self._policy_failure("preflight_rejected", report.reason or "preflight_rejected")
|
|
259
|
+
try:
|
|
260
|
+
profile = get_profile(request.model_profile)
|
|
261
|
+
except ValueError:
|
|
262
|
+
return self._policy_failure(
|
|
263
|
+
"unknown_model_profile",
|
|
264
|
+
f"model_profile is not allowlisted: {request.model_profile!r}",
|
|
265
|
+
)
|
|
266
|
+
model = self._model_factory(profile)
|
|
267
|
+
# apply is intentionally absent: direct delegation always remains a proposal.
|
|
268
|
+
if controller_started is not None:
|
|
269
|
+
controller_started.set()
|
|
270
|
+
return Controller(
|
|
271
|
+
model,
|
|
272
|
+
workspace,
|
|
273
|
+
max_turns=self._max_turns,
|
|
274
|
+
max_files=self._preflight_budget.max_files,
|
|
275
|
+
preflight_budget=self._preflight_budget,
|
|
276
|
+
system_contract=profile.system_contract,
|
|
277
|
+
).run(
|
|
278
|
+
request.task,
|
|
279
|
+
cancel_event=cancel_event,
|
|
280
|
+
completion_event=completion_event,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
def apply(
|
|
284
|
+
self,
|
|
285
|
+
caller_id: str,
|
|
286
|
+
workspace_ref: str,
|
|
287
|
+
request_id: str,
|
|
288
|
+
*,
|
|
289
|
+
cancel_event: Event | None = None,
|
|
290
|
+
) -> dict[str, Any]:
|
|
291
|
+
"""Apply a previously stored terminal proposal to its workspace.
|
|
292
|
+
|
|
293
|
+
Mediated-apply: revalidates the stored patch against the current
|
|
294
|
+
workspace, applies it, runs the task's allowlisted checks, and rolls
|
|
295
|
+
back on any failure. Only a stored terminal proposal whose status is
|
|
296
|
+
``accepted`` and that carries a resolved ``patch`` is applied; the local
|
|
297
|
+
model is never invoked again.
|
|
298
|
+
"""
|
|
299
|
+
|
|
300
|
+
audit: list[dict[str, Any]] = [{"event": "apply_requested", "request_id": request_id}]
|
|
301
|
+
if not isinstance(caller_id, str) or not caller_id.strip():
|
|
302
|
+
return self._policy_failure("invalid_caller", "caller_id must be a non-empty string")
|
|
303
|
+
cache_key = (caller_id, workspace_ref, request_id)
|
|
304
|
+
with self._cache_lock:
|
|
305
|
+
record = self._cache.get(cache_key)
|
|
306
|
+
if record is None:
|
|
307
|
+
return apply_failure("unknown_proposal", "proposal is unknown for this caller/workspace", audit)
|
|
308
|
+
assert record.request is not None
|
|
309
|
+
request = record.request
|
|
310
|
+
record.completed.wait()
|
|
311
|
+
proposal = record.result
|
|
312
|
+
if proposal is None:
|
|
313
|
+
return apply_failure("unknown_proposal", "proposal has no stored result", audit)
|
|
314
|
+
if proposal.get("status") != "accepted":
|
|
315
|
+
return apply_failure("proposal_not_accepted", "only an accepted proposal may be applied", audit)
|
|
316
|
+
patch = proposal.get("patch")
|
|
317
|
+
if not isinstance(patch, str) or not patch.strip():
|
|
318
|
+
return apply_failure("no_patch", "proposal carries no resolved patch", audit)
|
|
319
|
+
if not request.task.checks:
|
|
320
|
+
return apply_failure(
|
|
321
|
+
"apply_requires_checks",
|
|
322
|
+
"applying a non-empty patch requires at least one targeted check",
|
|
323
|
+
audit,
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
workspace = self._workspaces.get(request.workspace_ref)
|
|
327
|
+
if workspace is None:
|
|
328
|
+
return apply_failure("unknown_workspace", "workspace_ref is not registered", audit)
|
|
329
|
+
|
|
330
|
+
workspace_lock = self._apply_locks.get(request.workspace_ref)
|
|
331
|
+
if workspace_lock is None:
|
|
332
|
+
return apply_failure("unknown_workspace", "workspace_ref is not registered", audit)
|
|
333
|
+
with workspace_lock:
|
|
334
|
+
return self._apply_to_workspace(request, proposal, workspace, patch, cancel_event, audit)
|
|
335
|
+
|
|
336
|
+
def _apply_to_workspace(
|
|
337
|
+
self,
|
|
338
|
+
request: DelegationRequest,
|
|
339
|
+
proposal: Mapping[str, Any],
|
|
340
|
+
workspace: Path,
|
|
341
|
+
patch: str,
|
|
342
|
+
cancel_event: Event | None,
|
|
343
|
+
audit: list[dict[str, Any]],
|
|
344
|
+
) -> dict[str, Any]:
|
|
345
|
+
"""Delegate the mutation pipeline to the shared mediated-apply seam."""
|
|
346
|
+
|
|
347
|
+
result = mediated_apply(workspace, patch, request.task, cancel_event=cancel_event, audit=audit)
|
|
348
|
+
if result.get("applied") is True:
|
|
349
|
+
result["summary"] = proposal.get("summary", "")
|
|
350
|
+
result["patch"] = patch
|
|
351
|
+
return result
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def proposal_preview(
|
|
355
|
+
self,
|
|
356
|
+
caller_id: str,
|
|
357
|
+
workspace_ref: str,
|
|
358
|
+
request_id: str,
|
|
359
|
+
) -> dict[str, Any]:
|
|
360
|
+
"""Return bounded, read-only details for an informed apply confirmation."""
|
|
361
|
+
|
|
362
|
+
cache_key = (caller_id, workspace_ref, request_id)
|
|
363
|
+
with self._cache_lock:
|
|
364
|
+
record = self._cache.get(cache_key)
|
|
365
|
+
if record is None or record.request is None:
|
|
366
|
+
return {"status": "unknown_proposal", "request_id": request_id}
|
|
367
|
+
if not record.completed.is_set() or record.result is None:
|
|
368
|
+
return {"status": "working", "request_id": request_id}
|
|
369
|
+
proposal = record.result
|
|
370
|
+
return {
|
|
371
|
+
"status": proposal.get("status"),
|
|
372
|
+
"request_id": request_id,
|
|
373
|
+
"workspace_ref": workspace_ref,
|
|
374
|
+
"summary": proposal.get("summary", ""),
|
|
375
|
+
"files": list(record.request.task.files),
|
|
376
|
+
"patch": proposal.get("patch", ""),
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
def _evict_completed_results(self) -> None:
|
|
380
|
+
while len(self._cache) > self._max_cached_results:
|
|
381
|
+
for key, record in self._cache.items():
|
|
382
|
+
if record.completed.is_set():
|
|
383
|
+
self._cache.pop(key)
|
|
384
|
+
break
|
|
385
|
+
else:
|
|
386
|
+
# Active reservations must keep their idempotency boundary.
|
|
387
|
+
return
|
|
388
|
+
|
|
389
|
+
@staticmethod
|
|
390
|
+
def _normalize_result(result: Mapping[str, Any]) -> dict[str, Any]:
|
|
391
|
+
normalized = dict(result)
|
|
392
|
+
normalized["applied"] = False
|
|
393
|
+
return normalized
|
|
394
|
+
|
|
395
|
+
@staticmethod
|
|
396
|
+
def _fingerprint(request: DelegationRequest) -> str:
|
|
397
|
+
return json.dumps(asdict(request), ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
|
398
|
+
|
|
399
|
+
@staticmethod
|
|
400
|
+
def _policy_failure(kind: str, message: str) -> dict[str, Any]:
|
|
401
|
+
return {
|
|
402
|
+
"status": "failed",
|
|
403
|
+
"error": {"kind": kind, "message": message},
|
|
404
|
+
"audit": [{"event": "policy_rejected", "kind": kind}],
|
|
405
|
+
}
|