agentpool 2.1.9__py3-none-any.whl → 2.2.3__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.
Files changed (174) hide show
  1. acp/__init__.py +13 -0
  2. acp/bridge/README.md +15 -2
  3. acp/bridge/__init__.py +3 -2
  4. acp/bridge/__main__.py +60 -19
  5. acp/bridge/ws_server.py +173 -0
  6. acp/bridge/ws_server_cli.py +89 -0
  7. acp/notifications.py +2 -1
  8. acp/stdio.py +39 -9
  9. acp/transports.py +362 -2
  10. acp/utils.py +15 -2
  11. agentpool/__init__.py +4 -1
  12. agentpool/agents/__init__.py +2 -0
  13. agentpool/agents/acp_agent/acp_agent.py +203 -88
  14. agentpool/agents/acp_agent/acp_converters.py +46 -21
  15. agentpool/agents/acp_agent/client_handler.py +157 -3
  16. agentpool/agents/acp_agent/session_state.py +4 -1
  17. agentpool/agents/agent.py +314 -107
  18. agentpool/agents/agui_agent/__init__.py +0 -2
  19. agentpool/agents/agui_agent/agui_agent.py +90 -21
  20. agentpool/agents/agui_agent/agui_converters.py +0 -131
  21. agentpool/agents/base_agent.py +163 -1
  22. agentpool/agents/claude_code_agent/claude_code_agent.py +626 -179
  23. agentpool/agents/claude_code_agent/converters.py +71 -3
  24. agentpool/agents/claude_code_agent/history.py +474 -0
  25. agentpool/agents/context.py +40 -0
  26. agentpool/agents/events/__init__.py +2 -0
  27. agentpool/agents/events/builtin_handlers.py +2 -1
  28. agentpool/agents/events/event_emitter.py +29 -2
  29. agentpool/agents/events/events.py +20 -0
  30. agentpool/agents/modes.py +54 -0
  31. agentpool/agents/tool_call_accumulator.py +213 -0
  32. agentpool/common_types.py +21 -0
  33. agentpool/config_resources/__init__.py +38 -1
  34. agentpool/config_resources/claude_code_agent.yml +3 -0
  35. agentpool/delegation/pool.py +37 -29
  36. agentpool/delegation/team.py +1 -0
  37. agentpool/delegation/teamrun.py +1 -0
  38. agentpool/diagnostics/__init__.py +53 -0
  39. agentpool/diagnostics/lsp_manager.py +1593 -0
  40. agentpool/diagnostics/lsp_proxy.py +41 -0
  41. agentpool/diagnostics/lsp_proxy_script.py +229 -0
  42. agentpool/diagnostics/models.py +398 -0
  43. agentpool/mcp_server/__init__.py +0 -2
  44. agentpool/mcp_server/client.py +12 -3
  45. agentpool/mcp_server/manager.py +25 -31
  46. agentpool/mcp_server/registries/official_registry_client.py +25 -0
  47. agentpool/mcp_server/tool_bridge.py +78 -66
  48. agentpool/messaging/__init__.py +0 -2
  49. agentpool/messaging/compaction.py +72 -197
  50. agentpool/messaging/message_history.py +12 -0
  51. agentpool/messaging/messages.py +52 -9
  52. agentpool/messaging/processing.py +3 -1
  53. agentpool/models/acp_agents/base.py +0 -22
  54. agentpool/models/acp_agents/mcp_capable.py +8 -148
  55. agentpool/models/acp_agents/non_mcp.py +129 -72
  56. agentpool/models/agents.py +35 -13
  57. agentpool/models/claude_code_agents.py +33 -2
  58. agentpool/models/manifest.py +43 -0
  59. agentpool/repomap.py +1 -1
  60. agentpool/resource_providers/__init__.py +9 -1
  61. agentpool/resource_providers/aggregating.py +52 -3
  62. agentpool/resource_providers/base.py +57 -1
  63. agentpool/resource_providers/mcp_provider.py +23 -0
  64. agentpool/resource_providers/plan_provider.py +130 -41
  65. agentpool/resource_providers/pool.py +2 -0
  66. agentpool/resource_providers/static.py +2 -0
  67. agentpool/sessions/__init__.py +2 -1
  68. agentpool/sessions/manager.py +31 -2
  69. agentpool/sessions/models.py +50 -0
  70. agentpool/skills/registry.py +13 -8
  71. agentpool/storage/manager.py +217 -1
  72. agentpool/testing.py +537 -19
  73. agentpool/utils/file_watcher.py +269 -0
  74. agentpool/utils/identifiers.py +121 -0
  75. agentpool/utils/pydantic_ai_helpers.py +46 -0
  76. agentpool/utils/streams.py +690 -1
  77. agentpool/utils/subprocess_utils.py +155 -0
  78. agentpool/utils/token_breakdown.py +461 -0
  79. {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/METADATA +27 -7
  80. {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/RECORD +170 -112
  81. {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/WHEEL +1 -1
  82. agentpool_cli/__main__.py +4 -0
  83. agentpool_cli/serve_acp.py +41 -20
  84. agentpool_cli/serve_agui.py +87 -0
  85. agentpool_cli/serve_opencode.py +119 -0
  86. agentpool_commands/__init__.py +30 -0
  87. agentpool_commands/agents.py +74 -1
  88. agentpool_commands/history.py +62 -0
  89. agentpool_commands/mcp.py +176 -0
  90. agentpool_commands/models.py +56 -3
  91. agentpool_commands/tools.py +57 -0
  92. agentpool_commands/utils.py +51 -0
  93. agentpool_config/builtin_tools.py +77 -22
  94. agentpool_config/commands.py +24 -1
  95. agentpool_config/compaction.py +258 -0
  96. agentpool_config/mcp_server.py +131 -1
  97. agentpool_config/storage.py +46 -1
  98. agentpool_config/tools.py +7 -1
  99. agentpool_config/toolsets.py +92 -148
  100. agentpool_server/acp_server/acp_agent.py +134 -150
  101. agentpool_server/acp_server/commands/acp_commands.py +216 -51
  102. agentpool_server/acp_server/commands/docs_commands/fetch_repo.py +10 -10
  103. agentpool_server/acp_server/server.py +23 -79
  104. agentpool_server/acp_server/session.py +181 -19
  105. agentpool_server/opencode_server/.rules +95 -0
  106. agentpool_server/opencode_server/ENDPOINTS.md +362 -0
  107. agentpool_server/opencode_server/__init__.py +27 -0
  108. agentpool_server/opencode_server/command_validation.py +172 -0
  109. agentpool_server/opencode_server/converters.py +869 -0
  110. agentpool_server/opencode_server/dependencies.py +24 -0
  111. agentpool_server/opencode_server/input_provider.py +269 -0
  112. agentpool_server/opencode_server/models/__init__.py +228 -0
  113. agentpool_server/opencode_server/models/agent.py +53 -0
  114. agentpool_server/opencode_server/models/app.py +60 -0
  115. agentpool_server/opencode_server/models/base.py +26 -0
  116. agentpool_server/opencode_server/models/common.py +23 -0
  117. agentpool_server/opencode_server/models/config.py +37 -0
  118. agentpool_server/opencode_server/models/events.py +647 -0
  119. agentpool_server/opencode_server/models/file.py +88 -0
  120. agentpool_server/opencode_server/models/mcp.py +25 -0
  121. agentpool_server/opencode_server/models/message.py +162 -0
  122. agentpool_server/opencode_server/models/parts.py +190 -0
  123. agentpool_server/opencode_server/models/provider.py +81 -0
  124. agentpool_server/opencode_server/models/pty.py +43 -0
  125. agentpool_server/opencode_server/models/session.py +99 -0
  126. agentpool_server/opencode_server/routes/__init__.py +25 -0
  127. agentpool_server/opencode_server/routes/agent_routes.py +442 -0
  128. agentpool_server/opencode_server/routes/app_routes.py +139 -0
  129. agentpool_server/opencode_server/routes/config_routes.py +241 -0
  130. agentpool_server/opencode_server/routes/file_routes.py +392 -0
  131. agentpool_server/opencode_server/routes/global_routes.py +94 -0
  132. agentpool_server/opencode_server/routes/lsp_routes.py +319 -0
  133. agentpool_server/opencode_server/routes/message_routes.py +705 -0
  134. agentpool_server/opencode_server/routes/pty_routes.py +299 -0
  135. agentpool_server/opencode_server/routes/session_routes.py +1205 -0
  136. agentpool_server/opencode_server/routes/tui_routes.py +139 -0
  137. agentpool_server/opencode_server/server.py +430 -0
  138. agentpool_server/opencode_server/state.py +121 -0
  139. agentpool_server/opencode_server/time_utils.py +8 -0
  140. agentpool_storage/__init__.py +16 -0
  141. agentpool_storage/base.py +103 -0
  142. agentpool_storage/claude_provider.py +907 -0
  143. agentpool_storage/file_provider.py +129 -0
  144. agentpool_storage/memory_provider.py +61 -0
  145. agentpool_storage/models.py +3 -0
  146. agentpool_storage/opencode_provider.py +730 -0
  147. agentpool_storage/project_store.py +325 -0
  148. agentpool_storage/session_store.py +6 -0
  149. agentpool_storage/sql_provider/__init__.py +4 -2
  150. agentpool_storage/sql_provider/models.py +48 -0
  151. agentpool_storage/sql_provider/sql_provider.py +134 -1
  152. agentpool_storage/sql_provider/utils.py +10 -1
  153. agentpool_storage/text_log_provider.py +1 -0
  154. agentpool_toolsets/builtin/__init__.py +0 -8
  155. agentpool_toolsets/builtin/code.py +95 -56
  156. agentpool_toolsets/builtin/debug.py +16 -21
  157. agentpool_toolsets/builtin/execution_environment.py +99 -103
  158. agentpool_toolsets/builtin/file_edit/file_edit.py +115 -7
  159. agentpool_toolsets/builtin/skills.py +86 -4
  160. agentpool_toolsets/fsspec_toolset/__init__.py +13 -1
  161. agentpool_toolsets/fsspec_toolset/diagnostics.py +860 -73
  162. agentpool_toolsets/fsspec_toolset/grep.py +74 -2
  163. agentpool_toolsets/fsspec_toolset/image_utils.py +161 -0
  164. agentpool_toolsets/fsspec_toolset/toolset.py +159 -38
  165. agentpool_toolsets/mcp_discovery/__init__.py +5 -0
  166. agentpool_toolsets/mcp_discovery/data/mcp_servers.parquet +0 -0
  167. agentpool_toolsets/mcp_discovery/toolset.py +454 -0
  168. agentpool_toolsets/mcp_run_toolset.py +84 -6
  169. agentpool_toolsets/builtin/agent_management.py +0 -239
  170. agentpool_toolsets/builtin/history.py +0 -36
  171. agentpool_toolsets/builtin/integration.py +0 -85
  172. agentpool_toolsets/builtin/tool_management.py +0 -90
  173. {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/entry_points.txt +0 -0
  174. {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,94 @@
1
+ """Global routes (health, events)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import logging
7
+ from typing import TYPE_CHECKING, Any
8
+
9
+ from fastapi import APIRouter
10
+ from sse_starlette.sse import EventSourceResponse
11
+
12
+ from agentpool_server.opencode_server.dependencies import StateDep # noqa: TC001
13
+ from agentpool_server.opencode_server.models import ( # noqa: TC001
14
+ Event,
15
+ HealthResponse,
16
+ ServerConnectedEvent,
17
+ )
18
+
19
+
20
+ if TYPE_CHECKING:
21
+ from collections.abc import AsyncGenerator
22
+
23
+ from agentpool_server.opencode_server.state import ServerState
24
+
25
+
26
+ logger = logging.getLogger(__name__)
27
+ router = APIRouter(tags=["global"])
28
+
29
+ VERSION = "0.1.0"
30
+
31
+
32
+ @router.get("/global/health")
33
+ async def get_health() -> HealthResponse:
34
+ """Get server health status."""
35
+ return HealthResponse(healthy=True, version=VERSION)
36
+
37
+
38
+ def _serialize_event(event: Event, wrap_payload: bool = False) -> str:
39
+ """Serialize event, optionally wrapping in payload structure."""
40
+ import json
41
+
42
+ event_data = event.model_dump(by_alias=True, exclude_none=True)
43
+ if wrap_payload:
44
+ return json.dumps({"payload": event_data})
45
+ return json.dumps(event_data)
46
+
47
+
48
+ async def _event_generator(
49
+ state: ServerState, *, wrap_payload: bool = False
50
+ ) -> AsyncGenerator[dict[str, Any]]:
51
+ """Generate SSE events."""
52
+ queue: asyncio.Queue[Event] = asyncio.Queue()
53
+ state.event_subscribers.append(queue)
54
+ subscriber_count = len(state.event_subscribers)
55
+ logger.info("SSE: New client connected (total subscribers: %s)", subscriber_count)
56
+
57
+ # Trigger first subscriber callback if this is the first connection
58
+ if (
59
+ subscriber_count == 1
60
+ and not state._first_subscriber_triggered
61
+ and state.on_first_subscriber is not None
62
+ ):
63
+ state._first_subscriber_triggered = True
64
+ state.create_background_task(state.on_first_subscriber(), name="on_first_subscriber")
65
+
66
+ try:
67
+ # Send initial connected event
68
+ connected = ServerConnectedEvent()
69
+ data = _serialize_event(connected, wrap_payload=wrap_payload)
70
+ logger.info("SSE: Sending connected event: %s", data)
71
+ yield {"data": data}
72
+ # Stream events
73
+ while True:
74
+ event = await queue.get()
75
+ data = _serialize_event(event, wrap_payload=wrap_payload)
76
+ logger.info("SSE: Sending event: %s", event.type)
77
+ yield {"data": data}
78
+ finally:
79
+ state.event_subscribers.remove(queue)
80
+ logger.info(
81
+ "SSE: Client disconnected (remaining subscribers: %s)", len(state.event_subscribers)
82
+ )
83
+
84
+
85
+ @router.get("/global/event")
86
+ async def get_global_events(state: StateDep) -> EventSourceResponse:
87
+ """Get global events as SSE stream (uses payload wrapper)."""
88
+ return EventSourceResponse(_event_generator(state, wrap_payload=True), sep="\n")
89
+
90
+
91
+ @router.get("/event")
92
+ async def get_events(state: StateDep) -> EventSourceResponse:
93
+ """Get events as SSE stream (no payload wrapper)."""
94
+ return EventSourceResponse(_event_generator(state, wrap_payload=False), sep="\n")
@@ -0,0 +1,319 @@
1
+ """LSP (Language Server Protocol) routes.
2
+
3
+ Provides endpoints for LSP server status and diagnostics,
4
+ compatible with OpenCode's LSP API.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from contextlib import suppress
10
+ import os
11
+ from typing import Literal
12
+
13
+ from fastapi import APIRouter, HTTPException, Query
14
+ from pydantic import BaseModel
15
+
16
+ from agentpool_server.opencode_server.dependencies import StateDep # noqa: TC001
17
+ from agentpool_server.opencode_server.models.events import LspStatus, LspUpdatedEvent
18
+
19
+
20
+ # =============================================================================
21
+ # Diagnostic Models (matching OpenCode's LSP diagnostic format)
22
+ # =============================================================================
23
+
24
+
25
+ class DiagnosticPosition(BaseModel):
26
+ """Position in a text document."""
27
+
28
+ line: int
29
+ character: int
30
+
31
+
32
+ class DiagnosticRange(BaseModel):
33
+ """Range in a text document."""
34
+
35
+ start: DiagnosticPosition
36
+ end: DiagnosticPosition
37
+
38
+
39
+ class Diagnostic(BaseModel):
40
+ """LSP Diagnostic matching vscode-languageserver-types format."""
41
+
42
+ range: DiagnosticRange
43
+ message: str
44
+ severity: int | None = None # 1=Error, 2=Warning, 3=Info, 4=Hint
45
+ code: str | int | None = None
46
+ source: str | None = None
47
+
48
+
49
+ class FormatterStatus(BaseModel):
50
+ """Formatter status information."""
51
+
52
+ id: str
53
+ """Formatter identifier."""
54
+
55
+ name: str
56
+ """Formatter name."""
57
+
58
+ root: str
59
+ """Workspace root path."""
60
+
61
+ status: Literal["connected", "error"]
62
+ """Connection status."""
63
+
64
+
65
+ router = APIRouter(tags=["lsp"])
66
+
67
+
68
+ @router.get("/lsp")
69
+ async def list_lsp_servers(state: StateDep) -> list[LspStatus]:
70
+ """List all active LSP servers.
71
+
72
+ Returns the status of all running LSP servers, including their
73
+ connection state and workspace root.
74
+
75
+ Returns:
76
+ List of LSP server status objects.
77
+ """
78
+ try:
79
+ lsp_manager = state.get_or_create_lsp_manager()
80
+ except RuntimeError:
81
+ # Agent doesn't have an execution environment - return empty list
82
+ return []
83
+
84
+ servers: list[LspStatus] = []
85
+ for server_id, server_state in lsp_manager._servers.items():
86
+ # Get relative root path
87
+ root_uri = server_state.root_uri or ""
88
+ if root_uri.startswith("file://"):
89
+ root_path = root_uri[7:] # Remove file:// prefix
90
+ # Make path relative to working directory
91
+ with suppress(ValueError):
92
+ root_path = os.path.relpath(root_path, state.working_dir)
93
+ else:
94
+ root_path = root_uri
95
+
96
+ servers.append(
97
+ LspStatus(
98
+ id=server_id,
99
+ name=server_id,
100
+ root=root_path,
101
+ status="connected" if server_state.initialized else "error",
102
+ )
103
+ )
104
+
105
+ return servers
106
+
107
+
108
+ @router.post("/lsp/start")
109
+ async def start_lsp_server(
110
+ state: StateDep,
111
+ server_id: str = Query(..., description="LSP server ID (e.g., 'pyright', 'rust-analyzer')"),
112
+ root_uri: str | None = Query(None, description="Workspace root URI"),
113
+ ) -> LspStatus:
114
+ """Start an LSP server.
115
+
116
+ Starts the specified LSP server for the given workspace root.
117
+ If no root_uri is provided, uses the server's working directory.
118
+
119
+ Args:
120
+ state: Server state dependency (injected).
121
+ server_id: The LSP server identifier (e.g., 'pyright', 'typescript').
122
+ root_uri: Optional workspace root URI (file:// format).
123
+
124
+ Returns:
125
+ The started server's status.
126
+
127
+ Raises:
128
+ HTTPException: If the server fails to start or is not registered.
129
+ """
130
+ try:
131
+ lsp_manager = state.get_or_create_lsp_manager()
132
+ except RuntimeError as e:
133
+ raise HTTPException(status_code=503, detail=str(e)) from e
134
+
135
+ # Default to working directory if no root provided
136
+ if root_uri is None:
137
+ root_uri = f"file://{state.working_dir}"
138
+
139
+ try:
140
+ server_state = await lsp_manager.start_server(server_id, root_uri)
141
+ except ValueError as e:
142
+ raise HTTPException(status_code=404, detail=str(e)) from e
143
+ except RuntimeError as e:
144
+ raise HTTPException(status_code=500, detail=str(e)) from e
145
+
146
+ # Emit lsp.updated event to notify clients of server status change
147
+ await state.broadcast_event(LspUpdatedEvent.create())
148
+
149
+ # Get relative root path for response
150
+ root_path = root_uri
151
+ if root_uri.startswith("file://"):
152
+ root_path = root_uri[7:]
153
+ with suppress(ValueError):
154
+ root_path = os.path.relpath(root_path, state.working_dir)
155
+
156
+ return LspStatus(
157
+ id=server_id,
158
+ name=server_id,
159
+ root=root_path,
160
+ status="connected" if server_state.initialized else "error",
161
+ )
162
+
163
+
164
+ @router.post("/lsp/stop")
165
+ async def stop_lsp_server(
166
+ state: StateDep,
167
+ server_id: str = Query(..., description="LSP server ID to stop"),
168
+ ) -> dict[str, str]:
169
+ """Stop an LSP server.
170
+
171
+ Args:
172
+ state: Server state dependency (injected).
173
+ server_id: The LSP server identifier to stop.
174
+
175
+ Returns:
176
+ Success message.
177
+ """
178
+ try:
179
+ lsp_manager = state.get_or_create_lsp_manager()
180
+ except RuntimeError:
181
+ return {"status": "ok", "message": "No LSP manager active"}
182
+
183
+ await lsp_manager.stop_server(server_id)
184
+
185
+ # Emit lsp.updated event to notify clients of server status change
186
+ await state.broadcast_event(LspUpdatedEvent.create())
187
+
188
+ return {"status": "ok", "message": f"Server {server_id} stopped"}
189
+
190
+
191
+ @router.get("/lsp/diagnostics")
192
+ async def get_diagnostics(
193
+ state: StateDep,
194
+ path: str | None = Query(None, description="File path to get diagnostics for"),
195
+ ) -> dict[str, list[Diagnostic]]:
196
+ """Get diagnostics from all active LSP servers.
197
+
198
+ Returns diagnostics organized by file path. If a specific path is provided,
199
+ returns diagnostics only for that file using CLI diagnostics.
200
+
201
+ This uses CLI-based diagnostic tools (pyright, mypy, etc.) which are more
202
+ reliable for on-demand checks than the LSP push model.
203
+
204
+ Args:
205
+ state: Server state dependency (injected).
206
+ path: Optional file path to get diagnostics for.
207
+
208
+ Returns:
209
+ Dictionary mapping file paths to lists of diagnostic objects.
210
+ """
211
+ try:
212
+ lsp_manager = state.get_or_create_lsp_manager()
213
+ except RuntimeError:
214
+ return {}
215
+
216
+ results: dict[str, list[Diagnostic]] = {}
217
+
218
+ # If a specific path is provided, run CLI diagnostics for it
219
+ if path:
220
+ # Make path absolute if needed
221
+ if not os.path.isabs(path): # noqa: PTH117
222
+ path = os.path.join(state.working_dir, path) # noqa: PTH118
223
+
224
+ # Find the appropriate server for this file
225
+ server_info = lsp_manager.get_server_for_file(path)
226
+ if server_info and server_info.has_cli_diagnostics:
227
+ try:
228
+ result = await lsp_manager.run_cli_diagnostics(server_info.id, [path])
229
+ if result.success and result.diagnostics:
230
+ for diag in result.diagnostics:
231
+ file_path = diag.file or path
232
+ if file_path not in results:
233
+ results[file_path] = []
234
+ # Convert from 1-based (CLI tools) to 0-based (LSP)
235
+ results[file_path].append(
236
+ Diagnostic(
237
+ range=DiagnosticRange(
238
+ start=DiagnosticPosition(
239
+ line=max(0, diag.line - 1),
240
+ character=max(0, diag.column - 1),
241
+ ),
242
+ end=DiagnosticPosition(
243
+ line=max(0, (diag.end_line or diag.line) - 1),
244
+ character=max(0, (diag.end_column or diag.column) - 1),
245
+ ),
246
+ ),
247
+ message=diag.message,
248
+ severity=_severity_to_lsp(diag.severity),
249
+ code=diag.code,
250
+ source=diag.source or server_info.id,
251
+ )
252
+ )
253
+ except Exception: # noqa: BLE001
254
+ # CLI diagnostics failed, return empty
255
+ pass
256
+
257
+ return results
258
+
259
+
260
+ def _severity_to_lsp(severity: str) -> int:
261
+ """Convert severity string to LSP severity number."""
262
+ mapping = {
263
+ "error": 1,
264
+ "warning": 2,
265
+ "info": 3,
266
+ "hint": 4,
267
+ }
268
+ return mapping.get(severity.lower(), 1)
269
+
270
+
271
+ @router.get("/lsp/servers")
272
+ async def list_available_servers(state: StateDep) -> list[dict[str, object]]:
273
+ """List all registered (available) LSP servers.
274
+
275
+ Returns information about all LSP servers that can be started,
276
+ regardless of whether they are currently running.
277
+
278
+ Returns:
279
+ List of server configurations.
280
+ """
281
+ try:
282
+ lsp_manager = state.get_or_create_lsp_manager()
283
+ except RuntimeError:
284
+ return []
285
+
286
+ servers = []
287
+ for server_id, config in lsp_manager._server_configs.items():
288
+ servers.append({
289
+ "id": server_id,
290
+ "extensions": config.extensions,
291
+ "running": server_id in lsp_manager._servers,
292
+ })
293
+
294
+ return servers
295
+
296
+
297
+ # =============================================================================
298
+ # Formatter Routes
299
+ # =============================================================================
300
+
301
+
302
+ @router.get("/formatter")
303
+ async def list_formatters(state: StateDep) -> list[FormatterStatus]:
304
+ """List all active formatters.
305
+
306
+ Returns the status of all running formatters, including their
307
+ connection state and workspace root.
308
+
309
+ Note: This is currently a stub that returns an empty list.
310
+ Formatter support can be added in the future.
311
+
312
+ Returns:
313
+ List of formatter status objects.
314
+ """
315
+ # Stub implementation - formatters not yet implemented
316
+ # OpenCode has formatters like prettier, biome, etc.
317
+ # For now, return empty list
318
+ _ = state # Reserved for future use
319
+ return []