crabcode 0.1.6__py3-none-win_amd64.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.
Files changed (205) hide show
  1. crabcode-0.1.6.dist-info/METADATA +63 -0
  2. crabcode-0.1.6.dist-info/RECORD +205 -0
  3. crabcode-0.1.6.dist-info/WHEEL +4 -0
  4. crabcode-0.1.6.dist-info/entry_points.txt +2 -0
  5. crabcode_cli/__init__.py +7 -0
  6. crabcode_cli/__main__.py +4 -0
  7. crabcode_cli/_native/crabcode-computer-use-host.exe +0 -0
  8. crabcode_cli/_native/runtime.json +6 -0
  9. crabcode_cli/app.py +776 -0
  10. crabcode_cli/banner.py +39 -0
  11. crabcode_cli/computer_use.py +286 -0
  12. crabcode_cli/model_manager.py +649 -0
  13. crabcode_cli/model_picker.py +168 -0
  14. crabcode_cli/pipe.py +148 -0
  15. crabcode_cli/render/__init__.py +0 -0
  16. crabcode_cli/render/messages.py +19 -0
  17. crabcode_cli/repl.py +4841 -0
  18. crabcode_cli/session_picker.py +239 -0
  19. crabcode_core/__init__.py +7 -0
  20. crabcode_core/_version.py +3 -0
  21. crabcode_core/_windows_process.py +68 -0
  22. crabcode_core/agent_manager.py +1731 -0
  23. crabcode_core/api/__init__.py +20 -0
  24. crabcode_core/api/anthropic_adapter.py +648 -0
  25. crabcode_core/api/azure_adapter.py +69 -0
  26. crabcode_core/api/base.py +233 -0
  27. crabcode_core/api/codex_adapter.py +1327 -0
  28. crabcode_core/api/error_logging.py +132 -0
  29. crabcode_core/api/gemini_adapter.py +26 -0
  30. crabcode_core/api/model_info.py +257 -0
  31. crabcode_core/api/network.py +62 -0
  32. crabcode_core/api/ollama_adapter.py +27 -0
  33. crabcode_core/api/openai_adapter.py +323 -0
  34. crabcode_core/api/registry.py +58 -0
  35. crabcode_core/command_line.py +18 -0
  36. crabcode_core/compact/__init__.py +17 -0
  37. crabcode_core/compact/compact.py +514 -0
  38. crabcode_core/compact/context_tokens.py +165 -0
  39. crabcode_core/computer_use_mode.py +38 -0
  40. crabcode_core/computer_use_observation.py +144 -0
  41. crabcode_core/config/__init__.py +3 -0
  42. crabcode_core/config/claudemd.py +61 -0
  43. crabcode_core/config/manager.py +237 -0
  44. crabcode_core/config/model_catalog.py +256 -0
  45. crabcode_core/events.py +4896 -0
  46. crabcode_core/file_lock.py +81 -0
  47. crabcode_core/filesystem.py +22 -0
  48. crabcode_core/goal.py +149 -0
  49. crabcode_core/hooks/__init__.py +5 -0
  50. crabcode_core/hooks/manager.py +290 -0
  51. crabcode_core/image_io.py +111 -0
  52. crabcode_core/io_worker.py +211 -0
  53. crabcode_core/logging_utils.py +168 -0
  54. crabcode_core/lsp/__init__.py +6 -0
  55. crabcode_core/lsp/client.py +786 -0
  56. crabcode_core/lsp/diagnostics.py +150 -0
  57. crabcode_core/lsp/manager.py +583 -0
  58. crabcode_core/mcp/__init__.py +4 -0
  59. crabcode_core/mcp/client.py +169 -0
  60. crabcode_core/mcp/config.py +57 -0
  61. crabcode_core/path_validation.py +34 -0
  62. crabcode_core/paths.py +24 -0
  63. crabcode_core/peer/__init__.py +10 -0
  64. crabcode_core/peer/runtime.py +518 -0
  65. crabcode_core/permissions/__init__.py +7 -0
  66. crabcode_core/permissions/ai_reviewer.py +184 -0
  67. crabcode_core/permissions/manager.py +187 -0
  68. crabcode_core/plan/__init__.py +5 -0
  69. crabcode_core/plan/executor.py +270 -0
  70. crabcode_core/plan/types.py +169 -0
  71. crabcode_core/prompts/__init__.py +0 -0
  72. crabcode_core/prompts/blocks.py +10 -0
  73. crabcode_core/prompts/context.py +229 -0
  74. crabcode_core/prompts/library.py +92 -0
  75. crabcode_core/prompts/profile.py +79 -0
  76. crabcode_core/prompts/system.py +356 -0
  77. crabcode_core/prompts/templates.py +104 -0
  78. crabcode_core/query/__init__.py +0 -0
  79. crabcode_core/query/computer_use_history.py +83 -0
  80. crabcode_core/query/loop.py +2446 -0
  81. crabcode_core/query/retry.py +116 -0
  82. crabcode_core/schedule/__init__.py +13 -0
  83. crabcode_core/schedule/manager.py +631 -0
  84. crabcode_core/schedule/models.py +204 -0
  85. crabcode_core/schedule/store.py +558 -0
  86. crabcode_core/schedule/timing.py +287 -0
  87. crabcode_core/session/__init__.py +12 -0
  88. crabcode_core/session/export.py +141 -0
  89. crabcode_core/session/meta_db.py +684 -0
  90. crabcode_core/session/storage.py +2032 -0
  91. crabcode_core/session/title_gen.py +52 -0
  92. crabcode_core/skills/__init__.py +6 -0
  93. crabcode_core/skills/context.py +23 -0
  94. crabcode_core/skills/loader.py +214 -0
  95. crabcode_core/skills/matcher.py +203 -0
  96. crabcode_core/snapshot/__init__.py +21 -0
  97. crabcode_core/snapshot/snapshot.py +436 -0
  98. crabcode_core/snapshot/tracker.py +359 -0
  99. crabcode_core/subprocess_utils.py +260 -0
  100. crabcode_core/team/__init__.py +27 -0
  101. crabcode_core/team/bridge.py +66 -0
  102. crabcode_core/team/inbox.py +237 -0
  103. crabcode_core/team/manager.py +1660 -0
  104. crabcode_core/team/message_bus.py +821 -0
  105. crabcode_core/team/models.py +191 -0
  106. crabcode_core/team/recovery.py +192 -0
  107. crabcode_core/text_io.py +52 -0
  108. crabcode_core/tools/__init__.py +112 -0
  109. crabcode_core/tools/_input_helpers.py +39 -0
  110. crabcode_core/tools/_input_schema.py +130 -0
  111. crabcode_core/tools/agent.py +413 -0
  112. crabcode_core/tools/apply_patch.py +587 -0
  113. crabcode_core/tools/ask_user.py +122 -0
  114. crabcode_core/tools/bash.py +154 -0
  115. crabcode_core/tools/browser.py +678 -0
  116. crabcode_core/tools/checklist.py +241 -0
  117. crabcode_core/tools/checkpoint.py +78 -0
  118. crabcode_core/tools/computer_use.py +599 -0
  119. crabcode_core/tools/diff_utils.py +123 -0
  120. crabcode_core/tools/file_edit.py +240 -0
  121. crabcode_core/tools/file_read.py +187 -0
  122. crabcode_core/tools/file_write.py +154 -0
  123. crabcode_core/tools/glob.py +123 -0
  124. crabcode_core/tools/goal.py +134 -0
  125. crabcode_core/tools/grep.py +247 -0
  126. crabcode_core/tools/image.py +173 -0
  127. crabcode_core/tools/image_generate.py +115 -0
  128. crabcode_core/tools/lint.py +388 -0
  129. crabcode_core/tools/loading.py +187 -0
  130. crabcode_core/tools/memory.py +456 -0
  131. crabcode_core/tools/monitor.py +677 -0
  132. crabcode_core/tools/peer.py +103 -0
  133. crabcode_core/tools/revert.py +90 -0
  134. crabcode_core/tools/schedule.py +683 -0
  135. crabcode_core/tools/skill.py +108 -0
  136. crabcode_core/tools/switch_mode.py +213 -0
  137. crabcode_core/tools/team.py +329 -0
  138. crabcode_core/tools/tool_search.py +64 -0
  139. crabcode_core/tools/web_search.py +444 -0
  140. crabcode_core/turn_details.py +64 -0
  141. crabcode_core/types/__init__.py +95 -0
  142. crabcode_core/types/config.py +434 -0
  143. crabcode_core/types/event.py +334 -0
  144. crabcode_core/types/message.py +263 -0
  145. crabcode_core/types/tool.py +208 -0
  146. crabcode_core/usage.py +289 -0
  147. crabcode_core/utf8_sanitize.py +29 -0
  148. crabcode_debugger/__init__.py +5 -0
  149. crabcode_debugger/adapters.py +332 -0
  150. crabcode_debugger/dap.py +304 -0
  151. crabcode_debugger/memory.py +1806 -0
  152. crabcode_debugger/process.py +707 -0
  153. crabcode_debugger/runtime.py +58 -0
  154. crabcode_debugger/sessions.py +387 -0
  155. crabcode_debugger/tool.py +731 -0
  156. crabcode_gateway/__init__.py +18 -0
  157. crabcode_gateway/acp/__init__.py +13 -0
  158. crabcode_gateway/acp/agent.py +1581 -0
  159. crabcode_gateway/acp/session.py +114 -0
  160. crabcode_gateway/acp/transport.py +39 -0
  161. crabcode_gateway/acp/types.py +90 -0
  162. crabcode_gateway/adapter.py +30 -0
  163. crabcode_gateway/auth.py +139 -0
  164. crabcode_gateway/computer_use.py +364 -0
  165. crabcode_gateway/document_engine.py +637 -0
  166. crabcode_gateway/document_worker.py +301 -0
  167. crabcode_gateway/event_bus.py +655 -0
  168. crabcode_gateway/grpc/__init__.py +0 -0
  169. crabcode_gateway/grpc/crabcode_pb2.py +70 -0
  170. crabcode_gateway/grpc/crabcode_pb2_grpc.py +474 -0
  171. crabcode_gateway/grpc/proto/crabcode.proto +160 -0
  172. crabcode_gateway/grpc/server.py +765 -0
  173. crabcode_gateway/middleware.py +344 -0
  174. crabcode_gateway/protocol.py +17 -0
  175. crabcode_gateway/routes/__init__.py +0 -0
  176. crabcode_gateway/routes/agent.py +392 -0
  177. crabcode_gateway/routes/auth.py +150 -0
  178. crabcode_gateway/routes/computer_use.py +101 -0
  179. crabcode_gateway/routes/config.py +1698 -0
  180. crabcode_gateway/routes/document.py +1810 -0
  181. crabcode_gateway/routes/event.py +3801 -0
  182. crabcode_gateway/routes/git_review.py +232 -0
  183. crabcode_gateway/routes/health.py +14 -0
  184. crabcode_gateway/routes/peer.py +78 -0
  185. crabcode_gateway/routes/permission.py +121 -0
  186. crabcode_gateway/routes/schedule.py +300 -0
  187. crabcode_gateway/routes/session.py +1496 -0
  188. crabcode_gateway/routes/snapshot.py +211 -0
  189. crabcode_gateway/routes/tasks.py +318 -0
  190. crabcode_gateway/routes/team.py +392 -0
  191. crabcode_gateway/routes/usage.py +49 -0
  192. crabcode_gateway/routes/workspace.py +299 -0
  193. crabcode_gateway/safe_file.py +63 -0
  194. crabcode_gateway/schemas.py +1820 -0
  195. crabcode_gateway/server.py +514 -0
  196. crabcode_gateway/session_registry.py +36 -0
  197. crabcode_gateway/task_registry.py +708 -0
  198. crabcode_gateway/turn_details.py +60 -0
  199. crabcode_search/__init__.py +11 -0
  200. crabcode_search/background.py +368 -0
  201. crabcode_search/chunker.py +517 -0
  202. crabcode_search/embedder.py +497 -0
  203. crabcode_search/indexer.py +215 -0
  204. crabcode_search/store.py +222 -0
  205. crabcode_search/tool.py +268 -0
@@ -0,0 +1,58 @@
1
+ """Shared debugger runtime state for multiple tool instances."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+
8
+ from crabcode_debugger.sessions import DebugSessionManager
9
+
10
+
11
+ @dataclass
12
+ class _ManagerEntry:
13
+ manager: DebugSessionManager
14
+ users: int = 1
15
+
16
+
17
+ _MANAGERS: dict[tuple[str, str], _ManagerEntry] = {}
18
+
19
+
20
+ def _manager_key(cwd: str, owner_id: str) -> tuple[str, str]:
21
+ return (str(Path(cwd).resolve()), owner_id or "legacy")
22
+
23
+
24
+ def get_debug_session_manager(
25
+ *,
26
+ cwd: str,
27
+ owner_id: str = "",
28
+ env: dict[str, str] | None = None,
29
+ config: dict | None = None,
30
+ ) -> DebugSessionManager:
31
+ key = _manager_key(cwd, owner_id)
32
+ entry = _MANAGERS.get(key)
33
+ if entry is None:
34
+ manager = DebugSessionManager(cwd=cwd, env=env, config=config)
35
+ _MANAGERS[key] = _ManagerEntry(manager=manager)
36
+ else:
37
+ manager = entry.manager
38
+ entry.users += 1
39
+ manager.update_config(config)
40
+ return manager
41
+
42
+
43
+ async def release_debug_session_manager(
44
+ *,
45
+ cwd: str,
46
+ owner_id: str = "",
47
+ manager: DebugSessionManager,
48
+ ) -> None:
49
+ """Release one tool's reference and close the manager after the last user."""
50
+ key = _manager_key(cwd, owner_id)
51
+ entry = _MANAGERS.get(key)
52
+ if entry is None or entry.manager is not manager:
53
+ return
54
+ entry.users -= 1
55
+ if entry.users > 0:
56
+ return
57
+ _MANAGERS.pop(key, None)
58
+ await manager.close()
@@ -0,0 +1,387 @@
1
+ """Debug session manager built on DAP."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import uuid
7
+ from dataclasses import dataclass, field
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from crabcode_debugger.adapters import AdapterRegistry, AdapterStatus, infer_language
12
+ from crabcode_debugger.dap import DAPClient, DAPEvent
13
+
14
+
15
+ @dataclass
16
+ class DebugSession:
17
+ session_id: str
18
+ language: str
19
+ adapter: AdapterStatus
20
+ client: DAPClient
21
+ target: dict[str, Any]
22
+ capabilities: dict[str, Any] = field(default_factory=dict)
23
+ last_stopped_thread_id: int | None = None
24
+ pending_configuration_seq: int | None = None
25
+
26
+
27
+ class DebugSessionManager:
28
+ """Tracks live DAP sessions."""
29
+
30
+ def __init__(
31
+ self,
32
+ *,
33
+ cwd: str,
34
+ env: dict[str, str] | None = None,
35
+ config: dict[str, Any] | None = None,
36
+ ) -> None:
37
+ self.cwd = str(Path(cwd).resolve())
38
+ self.env = dict(env or {})
39
+ self.config = config or {}
40
+ self.registry = AdapterRegistry(self.config)
41
+ self.sessions: dict[str, DebugSession] = {}
42
+ self.timeout = float(self.config.get("default_timeout_seconds", 30))
43
+ self.disconnect_timeout = float(self.config.get("disconnect_timeout_seconds", 10))
44
+
45
+ def update_config(self, config: dict[str, Any] | None) -> None:
46
+ """Merge later tool configuration into the shared manager."""
47
+ if not config:
48
+ return
49
+ self.config = _merge_dicts(self.config, config)
50
+ self.registry = AdapterRegistry(self.config)
51
+ self.timeout = float(self.config.get("default_timeout_seconds", self.timeout))
52
+ self.disconnect_timeout = float(
53
+ self.config.get("disconnect_timeout_seconds", self.disconnect_timeout)
54
+ )
55
+
56
+ async def close(self) -> None:
57
+ for session_id in list(self.sessions):
58
+ try:
59
+ await self.stop(session_id, terminate_debuggee=False)
60
+ except Exception:
61
+ continue
62
+
63
+ async def start(
64
+ self,
65
+ *,
66
+ language: str,
67
+ program: str | None = None,
68
+ args: list[str] | None = None,
69
+ cwd: str | None = None,
70
+ adapter_id: str | None = None,
71
+ launch_config: dict[str, Any] | None = None,
72
+ ) -> dict[str, Any]:
73
+ normalized = infer_language(language or program)
74
+ if not normalized:
75
+ return {"error": "language is required or could not be inferred"}
76
+ adapter = self.registry.resolve(normalized, adapter_id)
77
+ if not adapter:
78
+ return {
79
+ "error": f"no available debug adapter for {normalized}",
80
+ "install_hints": self.registry.install_hints(normalized),
81
+ "adapters": [s.to_dict() for s in self.registry.status(normalized)],
82
+ }
83
+
84
+ client = DAPClient(
85
+ adapter.command,
86
+ cwd=cwd or self.cwd,
87
+ env=self.env,
88
+ timeout=self.timeout,
89
+ max_events=int(self.config.get("max_dap_events", 10_000)),
90
+ )
91
+ try:
92
+ await client.start()
93
+ capabilities = await client.request(
94
+ "initialize",
95
+ {
96
+ "clientID": "crabcode",
97
+ "clientName": "CrabCode",
98
+ "adapterID": adapter.adapter_id,
99
+ "pathFormat": "path",
100
+ "linesStartAt1": True,
101
+ "columnsStartAt1": True,
102
+ "supportsVariableType": True,
103
+ "supportsVariablePaging": True,
104
+ "supportsRunInTerminalRequest": False,
105
+ },
106
+ )
107
+
108
+ launch_args: dict[str, Any] = dict(launch_config or {})
109
+ launch_args.setdefault("cwd", cwd or self.cwd)
110
+ if program:
111
+ program_path = Path(program)
112
+ if not program_path.is_absolute():
113
+ program_path = Path(cwd or self.cwd) / program_path
114
+ launch_args.setdefault("program", str(program_path.resolve()))
115
+ if args is not None:
116
+ launch_args.setdefault("args", args)
117
+
118
+ pending_seq = await client.start_request("launch", launch_args)
119
+ session = DebugSession(
120
+ session_id=f"dbg-{uuid.uuid4().hex[:10]}",
121
+ language=normalized,
122
+ adapter=adapter,
123
+ client=client,
124
+ target={"mode": "launch", "program": program, "cwd": cwd or self.cwd},
125
+ capabilities=capabilities,
126
+ pending_configuration_seq=pending_seq,
127
+ )
128
+ self.sessions[session.session_id] = session
129
+ except BaseException:
130
+ await self._close_failed_client(client)
131
+ raise
132
+ return {
133
+ "session_id": session.session_id,
134
+ "language": normalized,
135
+ "adapter": adapter.to_dict(),
136
+ "capabilities": capabilities,
137
+ "events": [event.to_dict() for event in await self._drain_and_track(session)],
138
+ }
139
+
140
+ async def attach(
141
+ self,
142
+ *,
143
+ language: str,
144
+ pid: int,
145
+ adapter_id: str | None = None,
146
+ attach_config: dict[str, Any] | None = None,
147
+ ) -> dict[str, Any]:
148
+ if isinstance(pid, bool) or pid <= 0:
149
+ return {"error": "pid must be a positive integer for attach"}
150
+ normalized = infer_language(language)
151
+ if not normalized:
152
+ return {"error": "language is required for attach"}
153
+ adapter = self.registry.resolve(normalized, adapter_id)
154
+ if not adapter:
155
+ return {
156
+ "error": f"no available debug adapter for {normalized}",
157
+ "install_hints": self.registry.install_hints(normalized),
158
+ "adapters": [s.to_dict() for s in self.registry.status(normalized)],
159
+ }
160
+ client = DAPClient(
161
+ adapter.command,
162
+ cwd=self.cwd,
163
+ env=self.env,
164
+ timeout=self.timeout,
165
+ max_events=int(self.config.get("max_dap_events", 10_000)),
166
+ )
167
+ try:
168
+ await client.start()
169
+ capabilities = await client.request(
170
+ "initialize",
171
+ {
172
+ "clientID": "crabcode",
173
+ "clientName": "CrabCode",
174
+ "adapterID": adapter.adapter_id,
175
+ "pathFormat": "path",
176
+ "linesStartAt1": True,
177
+ "columnsStartAt1": True,
178
+ "supportsVariableType": True,
179
+ },
180
+ )
181
+ args = dict(attach_config or {})
182
+ args.setdefault("processId", pid)
183
+ pending_seq = await client.start_request("attach", args)
184
+ session = DebugSession(
185
+ session_id=f"dbg-{uuid.uuid4().hex[:10]}",
186
+ language=normalized,
187
+ adapter=adapter,
188
+ client=client,
189
+ target={"mode": "attach", "pid": pid},
190
+ capabilities=capabilities,
191
+ pending_configuration_seq=pending_seq,
192
+ )
193
+ self.sessions[session.session_id] = session
194
+ except BaseException:
195
+ await self._close_failed_client(client)
196
+ raise
197
+ return {
198
+ "session_id": session.session_id,
199
+ "language": normalized,
200
+ "adapter": adapter.to_dict(),
201
+ "capabilities": capabilities,
202
+ "events": [event.to_dict() for event in await self._drain_and_track(session)],
203
+ }
204
+
205
+ async def set_breakpoints(
206
+ self,
207
+ session_id: str,
208
+ *,
209
+ path: str,
210
+ lines: list[int],
211
+ ) -> dict[str, Any]:
212
+ session = self._require(session_id)
213
+ source_path = Path(path)
214
+ if not source_path.is_absolute():
215
+ source_path = Path(str(session.target.get("cwd") or self.cwd)) / source_path
216
+ body = await session.client.request(
217
+ "setBreakpoints",
218
+ {
219
+ "source": {"path": str(source_path)},
220
+ "breakpoints": [{"line": int(line)} for line in lines],
221
+ },
222
+ )
223
+ return {**body, "events": [event.to_dict() for event in await self._drain_and_track(session)]}
224
+
225
+ async def configuration_done(self, session_id: str) -> dict[str, Any]:
226
+ session = self._require(session_id)
227
+ body = await session.client.request("configurationDone", {})
228
+ pending_response: dict[str, Any] | None = None
229
+ if session.pending_configuration_seq is not None:
230
+ pending_response = await session.client.wait_response(
231
+ session.pending_configuration_seq,
232
+ timeout=self.timeout,
233
+ )
234
+ session.pending_configuration_seq = None
235
+ if pending_response is not None:
236
+ body["pending_configuration_response"] = pending_response
237
+ return {**body, "events": [event.to_dict() for event in await self._drain_and_track(session)]}
238
+
239
+ async def continue_thread(self, session_id: str, thread_id: int | None = None) -> dict[str, Any]:
240
+ session = self._require(session_id)
241
+ body = await session.client.request("continue", {"threadId": self._thread_id(session, thread_id)})
242
+ return {**body, "events": [event.to_dict() for event in await self._drain_and_track(session)]}
243
+
244
+ async def pause(self, session_id: str, thread_id: int | None = None) -> dict[str, Any]:
245
+ session = self._require(session_id)
246
+ body = await session.client.request("pause", {"threadId": self._thread_id(session, thread_id)})
247
+ return {**body, "events": [event.to_dict() for event in await self._drain_and_track(session)]}
248
+
249
+ async def step(self, session_id: str, command: str, thread_id: int | None = None) -> dict[str, Any]:
250
+ session = self._require(session_id)
251
+ dap_command = {
252
+ "step_over": "next",
253
+ "step_in": "stepIn",
254
+ "step_out": "stepOut",
255
+ }[command]
256
+ body = await session.client.request(dap_command, {"threadId": self._thread_id(session, thread_id)})
257
+ return {**body, "events": [event.to_dict() for event in await self._drain_and_track(session)]}
258
+
259
+ async def threads(self, session_id: str) -> dict[str, Any]:
260
+ session = self._require(session_id)
261
+ body = await session.client.request("threads", {})
262
+ return {**body, "events": [event.to_dict() for event in await self._drain_and_track(session)]}
263
+
264
+ async def stack(self, session_id: str, thread_id: int | None = None, levels: int = 20) -> dict[str, Any]:
265
+ session = self._require(session_id)
266
+ body = await session.client.request(
267
+ "stackTrace",
268
+ {"threadId": self._thread_id(session, thread_id), "levels": levels},
269
+ )
270
+ return {**body, "events": [event.to_dict() for event in await self._drain_and_track(session)]}
271
+
272
+ async def scopes(self, session_id: str, frame_id: int) -> dict[str, Any]:
273
+ session = self._require(session_id)
274
+ body = await session.client.request("scopes", {"frameId": frame_id})
275
+ return {**body, "events": [event.to_dict() for event in await self._drain_and_track(session)]}
276
+
277
+ async def variables(self, session_id: str, variables_reference: int, start: int = 0, count: int = 100) -> dict[str, Any]:
278
+ session = self._require(session_id)
279
+ body = await session.client.request(
280
+ "variables",
281
+ {
282
+ "variablesReference": variables_reference,
283
+ "start": start,
284
+ "count": count,
285
+ },
286
+ )
287
+ return {**body, "events": [event.to_dict() for event in await self._drain_and_track(session)]}
288
+
289
+ async def evaluate(
290
+ self,
291
+ session_id: str,
292
+ expression: str,
293
+ *,
294
+ frame_id: int | None = None,
295
+ context: str = "repl",
296
+ ) -> dict[str, Any]:
297
+ session = self._require(session_id)
298
+ args: dict[str, Any] = {"expression": expression, "context": context}
299
+ if frame_id is not None:
300
+ args["frameId"] = frame_id
301
+ body = await session.client.request("evaluate", args)
302
+ return {**body, "events": [event.to_dict() for event in await self._drain_and_track(session)]}
303
+
304
+ async def events(self, session_id: str) -> dict[str, Any]:
305
+ session = self._require(session_id)
306
+ return {"events": [event.to_dict() for event in await self._drain_and_track(session)]}
307
+
308
+ async def stop(self, session_id: str, *, terminate_debuggee: bool = False) -> dict[str, Any]:
309
+ session = self._require(session_id)
310
+ result: dict[str, Any] = {}
311
+ disconnect_error: str | None = None
312
+ try:
313
+ result = await session.client.request(
314
+ "disconnect",
315
+ {"terminateDebuggee": terminate_debuggee},
316
+ timeout=self.disconnect_timeout,
317
+ )
318
+ except Exception as exc:
319
+ disconnect_error = f"{type(exc).__name__}: {exc}"
320
+ finally:
321
+ try:
322
+ await session.client.close()
323
+ finally:
324
+ self.sessions.pop(session_id, None)
325
+ response = {
326
+ "stopped": session_id,
327
+ "terminate_debuggee": terminate_debuggee,
328
+ "result": result,
329
+ }
330
+ if disconnect_error is not None:
331
+ response["disconnect_warning"] = disconnect_error
332
+ return response
333
+
334
+ def list_sessions(self) -> dict[str, Any]:
335
+ return {
336
+ "sessions": [
337
+ {
338
+ "session_id": s.session_id,
339
+ "language": s.language,
340
+ "adapter": s.adapter.to_dict(),
341
+ "target": s.target,
342
+ "last_stopped_thread_id": s.last_stopped_thread_id,
343
+ }
344
+ for s in self.sessions.values()
345
+ ]
346
+ }
347
+
348
+ def _require(self, session_id: str) -> DebugSession:
349
+ if session_id not in self.sessions:
350
+ raise KeyError(f"unknown debug session: {session_id}")
351
+ return self.sessions[session_id]
352
+
353
+ @staticmethod
354
+ def _thread_id(session: DebugSession, thread_id: int | None) -> int:
355
+ if thread_id is not None:
356
+ return int(thread_id)
357
+ if session.last_stopped_thread_id is not None:
358
+ return session.last_stopped_thread_id
359
+ return 1
360
+
361
+ async def _drain_and_track(self, session: DebugSession) -> list[DAPEvent]:
362
+ events = await session.client.drain_events()
363
+ for event in events:
364
+ if event.event == "stopped":
365
+ thread_id = event.body.get("threadId")
366
+ if isinstance(thread_id, int):
367
+ session.last_stopped_thread_id = thread_id
368
+ if event.event in {"terminated", "exited"}:
369
+ session.last_stopped_thread_id = None
370
+ return events
371
+
372
+ @staticmethod
373
+ async def _close_failed_client(client: DAPClient) -> None:
374
+ try:
375
+ await asyncio.shield(client.close())
376
+ except Exception:
377
+ pass
378
+
379
+
380
+ def _merge_dicts(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]:
381
+ merged = dict(base)
382
+ for key, value in override.items():
383
+ if isinstance(merged.get(key), dict) and isinstance(value, dict):
384
+ merged[key] = _merge_dicts(merged[key], value)
385
+ else:
386
+ merged[key] = value
387
+ return merged