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,346 @@
|
|
|
1
|
+
"""Lifecycle hook bridge: registry and dispatcher (R30)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import threading
|
|
7
|
+
import uuid
|
|
8
|
+
from dataclasses import asdict, dataclass
|
|
9
|
+
from typing import Any, Callable
|
|
10
|
+
|
|
11
|
+
from ._decision import HookDecision
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class _HookRegistration:
|
|
16
|
+
hook_id: str
|
|
17
|
+
point: str
|
|
18
|
+
handler: Callable[..., Any]
|
|
19
|
+
matcher: str | None
|
|
20
|
+
priority: int = 0
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class HookBridge:
|
|
24
|
+
"""Central registry and dispatcher for lifecycle hooks."""
|
|
25
|
+
|
|
26
|
+
POINT_PRE_TOOL_CALL = "on_pre_tool_call"
|
|
27
|
+
POINT_POST_TOOL_CALL = "on_post_tool_call"
|
|
28
|
+
POINT_TURN_START = "on_turn_start"
|
|
29
|
+
POINT_TURN_END = "on_turn_end"
|
|
30
|
+
POINT_SESSION_FINISH = "on_session_finish"
|
|
31
|
+
|
|
32
|
+
_POINT_ALIASES = {
|
|
33
|
+
"PreToolUse": POINT_PRE_TOOL_CALL,
|
|
34
|
+
"pre_tool_call": POINT_PRE_TOOL_CALL,
|
|
35
|
+
"on_pre_tool_call": POINT_PRE_TOOL_CALL,
|
|
36
|
+
"PostToolUse": POINT_POST_TOOL_CALL,
|
|
37
|
+
"post_tool_call": POINT_POST_TOOL_CALL,
|
|
38
|
+
"on_post_tool_call": POINT_POST_TOOL_CALL,
|
|
39
|
+
"UserPromptSubmit": POINT_TURN_START,
|
|
40
|
+
"turn_start": POINT_TURN_START,
|
|
41
|
+
"on_turn_start": POINT_TURN_START,
|
|
42
|
+
"TurnEnd": POINT_TURN_END,
|
|
43
|
+
"turn_end": POINT_TURN_END,
|
|
44
|
+
"on_turn_end": POINT_TURN_END,
|
|
45
|
+
"SessionStart": "on_session_start",
|
|
46
|
+
"session_start": "on_session_start",
|
|
47
|
+
"on_session_start": "on_session_start",
|
|
48
|
+
"Stop": POINT_SESSION_FINISH,
|
|
49
|
+
"session_finish": POINT_SESSION_FINISH,
|
|
50
|
+
"on_session_finish": POINT_SESSION_FINISH,
|
|
51
|
+
"SubagentStart": "on_subagent_start",
|
|
52
|
+
"SubagentStop": "on_subagent_stop",
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
def __init__(self) -> None:
|
|
56
|
+
self._lock = threading.RLock()
|
|
57
|
+
self._hooks: dict[str, list[_HookRegistration]] = {}
|
|
58
|
+
|
|
59
|
+
def _normalize_point(self, point: str) -> str:
|
|
60
|
+
return self._POINT_ALIASES.get(point, point)
|
|
61
|
+
|
|
62
|
+
def register(
|
|
63
|
+
self,
|
|
64
|
+
point: str,
|
|
65
|
+
handler: Callable[..., Any],
|
|
66
|
+
*,
|
|
67
|
+
matcher: str | None = None,
|
|
68
|
+
priority: int = 0,
|
|
69
|
+
) -> str:
|
|
70
|
+
"""Register a hook handler for a lifecycle point."""
|
|
71
|
+
norm_point = self._normalize_point(point)
|
|
72
|
+
hook_id = f"hook-{uuid.uuid4().hex[:8]}"
|
|
73
|
+
reg = _HookRegistration(
|
|
74
|
+
hook_id=hook_id,
|
|
75
|
+
point=norm_point,
|
|
76
|
+
handler=handler,
|
|
77
|
+
matcher=matcher,
|
|
78
|
+
priority=priority,
|
|
79
|
+
)
|
|
80
|
+
with self._lock:
|
|
81
|
+
if norm_point not in self._hooks:
|
|
82
|
+
self._hooks[norm_point] = []
|
|
83
|
+
self._hooks[norm_point].append(reg)
|
|
84
|
+
self._hooks[norm_point].sort(key=lambda h: h.priority, reverse=True)
|
|
85
|
+
return hook_id
|
|
86
|
+
|
|
87
|
+
def unregister(self, hook_id: str) -> bool:
|
|
88
|
+
"""Unregister a hook handler by its ID."""
|
|
89
|
+
with self._lock:
|
|
90
|
+
for point, reg_list in self._hooks.items():
|
|
91
|
+
for idx, reg in enumerate(reg_list):
|
|
92
|
+
if reg.hook_id == hook_id:
|
|
93
|
+
reg_list.pop(idx)
|
|
94
|
+
return True
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
def on_pre_tool_call(self, matcher: str | None = None, priority: int = 0) -> Callable:
|
|
98
|
+
"""Decorator for registering pre-tool-call hooks."""
|
|
99
|
+
def decorator(fn: Callable) -> Callable:
|
|
100
|
+
self.register(self.POINT_PRE_TOOL_CALL, fn, matcher=matcher, priority=priority)
|
|
101
|
+
return fn
|
|
102
|
+
return decorator
|
|
103
|
+
|
|
104
|
+
def on_post_tool_call(self, matcher: str | None = None, priority: int = 0) -> Callable:
|
|
105
|
+
"""Decorator for registering post-tool-call hooks."""
|
|
106
|
+
def decorator(fn: Callable) -> Callable:
|
|
107
|
+
self.register(self.POINT_POST_TOOL_CALL, fn, matcher=matcher, priority=priority)
|
|
108
|
+
return fn
|
|
109
|
+
return decorator
|
|
110
|
+
|
|
111
|
+
def on_turn_start(self, matcher: str | None = None, priority: int = 0) -> Callable:
|
|
112
|
+
"""Decorator for registering turn-start hooks."""
|
|
113
|
+
def decorator(fn: Callable) -> Callable:
|
|
114
|
+
self.register(self.POINT_TURN_START, fn, matcher=matcher, priority=priority)
|
|
115
|
+
return fn
|
|
116
|
+
return decorator
|
|
117
|
+
|
|
118
|
+
def on_turn_end(self, matcher: str | None = None, priority: int = 0) -> Callable:
|
|
119
|
+
"""Decorator for registering turn-end hooks."""
|
|
120
|
+
def decorator(fn: Callable) -> Callable:
|
|
121
|
+
self.register(self.POINT_TURN_END, fn, matcher=matcher, priority=priority)
|
|
122
|
+
return fn
|
|
123
|
+
return decorator
|
|
124
|
+
|
|
125
|
+
def on_session_finish(self, priority: int = 0) -> Callable:
|
|
126
|
+
"""Decorator for registering session-finish hooks."""
|
|
127
|
+
def decorator(fn: Callable) -> Callable:
|
|
128
|
+
self.register(self.POINT_SESSION_FINISH, fn, priority=priority)
|
|
129
|
+
return fn
|
|
130
|
+
return decorator
|
|
131
|
+
|
|
132
|
+
def _matches(self, pattern: str | None, query: str) -> bool:
|
|
133
|
+
if pattern is None or pattern == "" or pattern == "*":
|
|
134
|
+
return True
|
|
135
|
+
try:
|
|
136
|
+
return bool(re.search(pattern, query))
|
|
137
|
+
except re.error:
|
|
138
|
+
return pattern == query
|
|
139
|
+
|
|
140
|
+
def trigger(
|
|
141
|
+
self,
|
|
142
|
+
point: str,
|
|
143
|
+
match_target: str = "",
|
|
144
|
+
payload: dict[str, Any] | None = None,
|
|
145
|
+
**kwargs: Any,
|
|
146
|
+
) -> HookDecision:
|
|
147
|
+
"""Execute all matching hooks for a given point and combine decisions."""
|
|
148
|
+
norm_point = self._normalize_point(point)
|
|
149
|
+
with self._lock:
|
|
150
|
+
candidates = list(self._hooks.get(norm_point, []))
|
|
151
|
+
|
|
152
|
+
decision = HookDecision()
|
|
153
|
+
merged_payload = dict(payload or {})
|
|
154
|
+
merged_payload.update(kwargs)
|
|
155
|
+
|
|
156
|
+
for reg in candidates:
|
|
157
|
+
if not self._matches(reg.matcher, match_target):
|
|
158
|
+
continue
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
import inspect
|
|
162
|
+
sig = inspect.signature(reg.handler)
|
|
163
|
+
param_count = len([
|
|
164
|
+
p for p in sig.parameters.values()
|
|
165
|
+
if p.kind in (inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD)
|
|
166
|
+
])
|
|
167
|
+
var_pos = any(p.kind == inspect.Parameter.VAR_POSITIONAL for p in sig.parameters.values())
|
|
168
|
+
if var_pos or param_count >= 2:
|
|
169
|
+
out = reg.handler(match_target, merged_payload)
|
|
170
|
+
elif param_count == 1:
|
|
171
|
+
out = reg.handler(merged_payload)
|
|
172
|
+
else:
|
|
173
|
+
out = reg.handler()
|
|
174
|
+
except Exception as exc:
|
|
175
|
+
out = {"decision": "error", "reason": f"hook handler error: {exc}"}
|
|
176
|
+
|
|
177
|
+
self._fold_outcome(decision, out)
|
|
178
|
+
if decision.is_blocking():
|
|
179
|
+
# Short-circuit on blocking decision
|
|
180
|
+
break
|
|
181
|
+
|
|
182
|
+
return decision
|
|
183
|
+
|
|
184
|
+
def trigger_pre_tool_call(
|
|
185
|
+
self,
|
|
186
|
+
tool_name: str,
|
|
187
|
+
arguments: dict[str, Any],
|
|
188
|
+
context: dict[str, Any] | None = None,
|
|
189
|
+
) -> HookDecision:
|
|
190
|
+
"""Convenience method for on_pre_tool_call point."""
|
|
191
|
+
payload = {
|
|
192
|
+
"tool_name": tool_name,
|
|
193
|
+
"tool_input": arguments,
|
|
194
|
+
"context": context or {},
|
|
195
|
+
}
|
|
196
|
+
return self.trigger(self.POINT_PRE_TOOL_CALL, match_target=tool_name, payload=payload)
|
|
197
|
+
|
|
198
|
+
def trigger_post_tool_call(
|
|
199
|
+
self,
|
|
200
|
+
tool_name: str,
|
|
201
|
+
arguments: dict[str, Any],
|
|
202
|
+
result: dict[str, Any],
|
|
203
|
+
context: dict[str, Any] | None = None,
|
|
204
|
+
) -> HookDecision:
|
|
205
|
+
"""Convenience method for on_post_tool_call point."""
|
|
206
|
+
payload = {
|
|
207
|
+
"tool_name": tool_name,
|
|
208
|
+
"tool_input": arguments,
|
|
209
|
+
"tool_output": result,
|
|
210
|
+
"context": context or {},
|
|
211
|
+
}
|
|
212
|
+
return self.trigger(self.POINT_POST_TOOL_CALL, match_target=tool_name, payload=payload)
|
|
213
|
+
|
|
214
|
+
def trigger_turn_start(
|
|
215
|
+
self,
|
|
216
|
+
turn: int,
|
|
217
|
+
prompt: str,
|
|
218
|
+
context: dict[str, Any] | None = None,
|
|
219
|
+
) -> HookDecision:
|
|
220
|
+
"""Convenience method for on_turn_start point."""
|
|
221
|
+
payload = {
|
|
222
|
+
"turn": turn,
|
|
223
|
+
"turn_id": str(turn),
|
|
224
|
+
"prompt": prompt,
|
|
225
|
+
"context": context or {},
|
|
226
|
+
}
|
|
227
|
+
return self.trigger(self.POINT_TURN_START, match_target="", payload=payload)
|
|
228
|
+
|
|
229
|
+
def trigger_turn_end(
|
|
230
|
+
self,
|
|
231
|
+
turn: int,
|
|
232
|
+
response: Any,
|
|
233
|
+
context: dict[str, Any] | None = None,
|
|
234
|
+
) -> HookDecision:
|
|
235
|
+
"""Convenience method for on_turn_end point."""
|
|
236
|
+
payload = {
|
|
237
|
+
"turn": turn,
|
|
238
|
+
"turn_id": str(turn),
|
|
239
|
+
"response": response,
|
|
240
|
+
"context": context or {},
|
|
241
|
+
}
|
|
242
|
+
return self.trigger(self.POINT_TURN_END, match_target="", payload=payload)
|
|
243
|
+
|
|
244
|
+
def trigger_session_finish(
|
|
245
|
+
self,
|
|
246
|
+
session_id: str,
|
|
247
|
+
summary: str = "",
|
|
248
|
+
status: str = "completed",
|
|
249
|
+
context: dict[str, Any] | None = None,
|
|
250
|
+
) -> HookDecision:
|
|
251
|
+
"""Convenience method for on_session_finish point."""
|
|
252
|
+
payload = {
|
|
253
|
+
"session_id": session_id,
|
|
254
|
+
"summary": summary,
|
|
255
|
+
"status": status,
|
|
256
|
+
"context": context or {},
|
|
257
|
+
}
|
|
258
|
+
return self.trigger(self.POINT_SESSION_FINISH, match_target="", payload=payload)
|
|
259
|
+
|
|
260
|
+
def _fold_outcome(self, decision: HookDecision, outcome: Any) -> None:
|
|
261
|
+
"""Fold an individual hook outcome into the aggregate HookDecision."""
|
|
262
|
+
if outcome is None:
|
|
263
|
+
return
|
|
264
|
+
|
|
265
|
+
if isinstance(outcome, HookDecision):
|
|
266
|
+
raw = asdict(outcome) if hasattr(outcome, "__dataclass_fields__") else {}
|
|
267
|
+
if outcome.is_blocking():
|
|
268
|
+
decision.decision = outcome.decision
|
|
269
|
+
decision.allowed = False
|
|
270
|
+
decision.reason = outcome.reason or decision.reason
|
|
271
|
+
if outcome.additional_context:
|
|
272
|
+
decision.additional_context.extend(outcome.additional_context)
|
|
273
|
+
if outcome.feedback:
|
|
274
|
+
decision.feedback.extend(outcome.feedback)
|
|
275
|
+
if outcome.system_messages:
|
|
276
|
+
decision.system_messages.extend(outcome.system_messages)
|
|
277
|
+
if outcome.updated_input:
|
|
278
|
+
decision.updated_input = outcome.updated_input
|
|
279
|
+
if outcome.stop:
|
|
280
|
+
decision.stop = True
|
|
281
|
+
decision.stop_reason = outcome.stop_reason or decision.stop_reason
|
|
282
|
+
decision.raw_outputs.append(raw)
|
|
283
|
+
return
|
|
284
|
+
|
|
285
|
+
if isinstance(outcome, dict):
|
|
286
|
+
decision.raw_outputs.append(outcome)
|
|
287
|
+
# Permission / blocking decision check
|
|
288
|
+
dec = outcome.get("decision")
|
|
289
|
+
perm_dec = (
|
|
290
|
+
outcome.get("hookSpecificOutput", {}).get("permissionDecision")
|
|
291
|
+
if isinstance(outcome.get("hookSpecificOutput"), dict)
|
|
292
|
+
else None
|
|
293
|
+
)
|
|
294
|
+
selected_dec = perm_dec or dec
|
|
295
|
+
|
|
296
|
+
if selected_dec in ("deny", "block", "error"):
|
|
297
|
+
decision.decision = selected_dec
|
|
298
|
+
decision.allowed = False
|
|
299
|
+
reason = outcome.get("reason") or (
|
|
300
|
+
outcome.get("hookSpecificOutput", {}).get("reason")
|
|
301
|
+
if isinstance(outcome.get("hookSpecificOutput"), dict)
|
|
302
|
+
else None
|
|
303
|
+
)
|
|
304
|
+
if reason:
|
|
305
|
+
decision.reason = str(reason)
|
|
306
|
+
elif selected_dec in ("allow", "approve", "ask"):
|
|
307
|
+
if not decision.is_blocking():
|
|
308
|
+
decision.decision = selected_dec
|
|
309
|
+
|
|
310
|
+
# Additional context
|
|
311
|
+
ctx = outcome.get("additionalContext") or outcome.get("additional_context")
|
|
312
|
+
if not ctx and isinstance(outcome.get("hookSpecificOutput"), dict):
|
|
313
|
+
ctx = outcome.get("hookSpecificOutput", {}).get("additionalContext")
|
|
314
|
+
if isinstance(ctx, str) and ctx.strip():
|
|
315
|
+
decision.additional_context.append(ctx.strip())
|
|
316
|
+
elif isinstance(ctx, list):
|
|
317
|
+
decision.additional_context.extend([str(c) for c in ctx if c])
|
|
318
|
+
|
|
319
|
+
# Feedback
|
|
320
|
+
fb = outcome.get("feedback")
|
|
321
|
+
if not fb and isinstance(outcome.get("hookSpecificOutput"), dict):
|
|
322
|
+
fb = outcome.get("hookSpecificOutput", {}).get("feedback")
|
|
323
|
+
if isinstance(fb, str) and fb.strip():
|
|
324
|
+
decision.feedback.append(fb.strip())
|
|
325
|
+
elif isinstance(fb, list):
|
|
326
|
+
decision.feedback.extend([str(f) for f in fb if f])
|
|
327
|
+
|
|
328
|
+
# System message
|
|
329
|
+
sys_msg = outcome.get("systemMessage") or outcome.get("system_message")
|
|
330
|
+
if not sys_msg and isinstance(outcome.get("hookSpecificOutput"), dict):
|
|
331
|
+
sys_msg = outcome.get("hookSpecificOutput", {}).get("systemMessage")
|
|
332
|
+
if isinstance(sys_msg, str) and sys_msg.strip():
|
|
333
|
+
decision.system_messages.append(sys_msg.strip())
|
|
334
|
+
|
|
335
|
+
# Updated input
|
|
336
|
+
updated = outcome.get("updatedInput") or outcome.get("updated_input")
|
|
337
|
+
if not updated and isinstance(outcome.get("hookSpecificOutput"), dict):
|
|
338
|
+
updated = outcome.get("hookSpecificOutput", {}).get("updatedInput")
|
|
339
|
+
if isinstance(updated, dict):
|
|
340
|
+
decision.updated_input = updated
|
|
341
|
+
|
|
342
|
+
# Stop
|
|
343
|
+
cont = outcome.get("continue")
|
|
344
|
+
if cont is False:
|
|
345
|
+
decision.stop = True
|
|
346
|
+
decision.stop_reason = outcome.get("stopReason") or outcome.get("stop_reason")
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Hook decision model (R30)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class HookDecision:
|
|
11
|
+
"""Consolidated outcome of hook execution at an interception point."""
|
|
12
|
+
|
|
13
|
+
decision: str = "allow" # "allow", "deny", "ask", "approve", "block"
|
|
14
|
+
allowed: bool = True
|
|
15
|
+
reason: str | None = None
|
|
16
|
+
additional_context: list[str] = field(default_factory=list)
|
|
17
|
+
feedback: list[str] = field(default_factory=list)
|
|
18
|
+
system_messages: list[str] = field(default_factory=list)
|
|
19
|
+
updated_input: dict[str, Any] | None = None
|
|
20
|
+
stop: bool = False
|
|
21
|
+
stop_reason: str | None = None
|
|
22
|
+
raw_outputs: list[dict[str, Any]] = field(default_factory=list)
|
|
23
|
+
|
|
24
|
+
def is_blocking(self) -> bool:
|
|
25
|
+
"""True if the hook explicitly denied or blocked the operation."""
|
|
26
|
+
return not self.allowed or self.decision in ("deny", "block")
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Generic LSP Stdio Code Intelligence Seam & Language Server Navigation (R25).
|
|
2
|
+
|
|
3
|
+
Adapted from DeepSeek Harness @deepseek-ai/dsh-lsp and @deepseek-ai/dsh-tool-lsp.
|
|
4
|
+
Provides standardized JSON-RPC stdio language server communication, process lifecycle,
|
|
5
|
+
protocol translation, mock servers, and AST/regex fallback intelligence.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from ._types import (
|
|
11
|
+
DEFAULT_SERVER_CANDIDATES,
|
|
12
|
+
EXTENSION_TO_LANGUAGE,
|
|
13
|
+
HEADER_SEPARATOR,
|
|
14
|
+
LspConnectionError,
|
|
15
|
+
LspError,
|
|
16
|
+
LspHoverResult,
|
|
17
|
+
LspLocation,
|
|
18
|
+
LspPosition,
|
|
19
|
+
LspRange,
|
|
20
|
+
LspResponseError,
|
|
21
|
+
LspSymbol,
|
|
22
|
+
LspTimeoutError,
|
|
23
|
+
MAX_HEADER_BYTES,
|
|
24
|
+
MAX_MESSAGE_BYTES,
|
|
25
|
+
MessageDecoder,
|
|
26
|
+
SYMBOL_KIND_NAMES,
|
|
27
|
+
encode_message,
|
|
28
|
+
path_to_uri,
|
|
29
|
+
uri_to_path,
|
|
30
|
+
)
|
|
31
|
+
from ._client import LspClient
|
|
32
|
+
from ._fallback import FallbackLspEngine
|
|
33
|
+
from ._manager import LspManager
|
|
34
|
+
from ._mock import MockLspServer
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"DEFAULT_SERVER_CANDIDATES",
|
|
38
|
+
"EXTENSION_TO_LANGUAGE",
|
|
39
|
+
"FallbackLspEngine",
|
|
40
|
+
"HEADER_SEPARATOR",
|
|
41
|
+
"LspClient",
|
|
42
|
+
"LspConnectionError",
|
|
43
|
+
"LspError",
|
|
44
|
+
"LspHoverResult",
|
|
45
|
+
"LspLocation",
|
|
46
|
+
"LspManager",
|
|
47
|
+
"LspPosition",
|
|
48
|
+
"LspRange",
|
|
49
|
+
"LspResponseError",
|
|
50
|
+
"LspSymbol",
|
|
51
|
+
"LspTimeoutError",
|
|
52
|
+
"MAX_HEADER_BYTES",
|
|
53
|
+
"MAX_MESSAGE_BYTES",
|
|
54
|
+
"MessageDecoder",
|
|
55
|
+
"MockLspServer",
|
|
56
|
+
"SYMBOL_KIND_NAMES",
|
|
57
|
+
"encode_message",
|
|
58
|
+
"path_to_uri",
|
|
59
|
+
"uri_to_path",
|
|
60
|
+
]
|