agentpool 2.1.9__py3-none-any.whl → 2.5.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.
Files changed (311) hide show
  1. acp/__init__.py +13 -4
  2. acp/acp_requests.py +20 -77
  3. acp/agent/connection.py +8 -0
  4. acp/agent/implementations/debug_server/debug_server.py +6 -2
  5. acp/agent/protocol.py +6 -0
  6. acp/bridge/README.md +15 -2
  7. acp/bridge/__init__.py +3 -2
  8. acp/bridge/__main__.py +60 -19
  9. acp/bridge/ws_server.py +173 -0
  10. acp/bridge/ws_server_cli.py +89 -0
  11. acp/client/connection.py +38 -29
  12. acp/client/implementations/default_client.py +3 -2
  13. acp/client/implementations/headless_client.py +2 -2
  14. acp/connection.py +2 -2
  15. acp/notifications.py +20 -50
  16. acp/schema/__init__.py +2 -0
  17. acp/schema/agent_responses.py +21 -0
  18. acp/schema/client_requests.py +3 -3
  19. acp/schema/session_state.py +63 -29
  20. acp/stdio.py +39 -9
  21. acp/task/supervisor.py +2 -2
  22. acp/transports.py +362 -2
  23. acp/utils.py +17 -4
  24. agentpool/__init__.py +6 -1
  25. agentpool/agents/__init__.py +2 -0
  26. agentpool/agents/acp_agent/acp_agent.py +407 -277
  27. agentpool/agents/acp_agent/acp_converters.py +196 -38
  28. agentpool/agents/acp_agent/client_handler.py +191 -26
  29. agentpool/agents/acp_agent/session_state.py +17 -6
  30. agentpool/agents/agent.py +607 -572
  31. agentpool/agents/agui_agent/__init__.py +0 -2
  32. agentpool/agents/agui_agent/agui_agent.py +176 -110
  33. agentpool/agents/agui_agent/agui_converters.py +0 -131
  34. agentpool/agents/agui_agent/helpers.py +3 -4
  35. agentpool/agents/base_agent.py +632 -17
  36. agentpool/agents/claude_code_agent/FORKING.md +191 -0
  37. agentpool/agents/claude_code_agent/__init__.py +13 -1
  38. agentpool/agents/claude_code_agent/claude_code_agent.py +1058 -291
  39. agentpool/agents/claude_code_agent/converters.py +74 -143
  40. agentpool/agents/claude_code_agent/history.py +474 -0
  41. agentpool/agents/claude_code_agent/models.py +77 -0
  42. agentpool/agents/claude_code_agent/static_info.py +100 -0
  43. agentpool/agents/claude_code_agent/usage.py +242 -0
  44. agentpool/agents/context.py +40 -0
  45. agentpool/agents/events/__init__.py +24 -0
  46. agentpool/agents/events/builtin_handlers.py +67 -1
  47. agentpool/agents/events/event_emitter.py +32 -2
  48. agentpool/agents/events/events.py +104 -3
  49. agentpool/agents/events/infer_info.py +145 -0
  50. agentpool/agents/events/processors.py +254 -0
  51. agentpool/agents/interactions.py +41 -6
  52. agentpool/agents/modes.py +67 -0
  53. agentpool/agents/slashed_agent.py +5 -4
  54. agentpool/agents/tool_call_accumulator.py +213 -0
  55. agentpool/agents/tool_wrapping.py +18 -6
  56. agentpool/common_types.py +56 -21
  57. agentpool/config_resources/__init__.py +38 -1
  58. agentpool/config_resources/acp_assistant.yml +2 -2
  59. agentpool/config_resources/agents.yml +3 -0
  60. agentpool/config_resources/agents_template.yml +1 -0
  61. agentpool/config_resources/claude_code_agent.yml +10 -6
  62. agentpool/config_resources/external_acp_agents.yml +2 -1
  63. agentpool/delegation/base_team.py +4 -30
  64. agentpool/delegation/pool.py +136 -289
  65. agentpool/delegation/team.py +58 -57
  66. agentpool/delegation/teamrun.py +51 -55
  67. agentpool/diagnostics/__init__.py +53 -0
  68. agentpool/diagnostics/lsp_manager.py +1593 -0
  69. agentpool/diagnostics/lsp_proxy.py +41 -0
  70. agentpool/diagnostics/lsp_proxy_script.py +229 -0
  71. agentpool/diagnostics/models.py +398 -0
  72. agentpool/functional/run.py +10 -4
  73. agentpool/mcp_server/__init__.py +0 -2
  74. agentpool/mcp_server/client.py +76 -32
  75. agentpool/mcp_server/conversions.py +54 -13
  76. agentpool/mcp_server/manager.py +34 -54
  77. agentpool/mcp_server/registries/official_registry_client.py +35 -1
  78. agentpool/mcp_server/tool_bridge.py +186 -139
  79. agentpool/messaging/__init__.py +0 -2
  80. agentpool/messaging/compaction.py +72 -197
  81. agentpool/messaging/connection_manager.py +11 -10
  82. agentpool/messaging/event_manager.py +5 -5
  83. agentpool/messaging/message_container.py +6 -30
  84. agentpool/messaging/message_history.py +99 -8
  85. agentpool/messaging/messagenode.py +52 -14
  86. agentpool/messaging/messages.py +54 -35
  87. agentpool/messaging/processing.py +12 -22
  88. agentpool/models/__init__.py +1 -1
  89. agentpool/models/acp_agents/base.py +6 -24
  90. agentpool/models/acp_agents/mcp_capable.py +126 -157
  91. agentpool/models/acp_agents/non_mcp.py +129 -95
  92. agentpool/models/agents.py +98 -76
  93. agentpool/models/agui_agents.py +1 -1
  94. agentpool/models/claude_code_agents.py +144 -19
  95. agentpool/models/file_parsing.py +0 -1
  96. agentpool/models/manifest.py +113 -50
  97. agentpool/prompts/conversion_manager.py +1 -1
  98. agentpool/prompts/prompts.py +5 -2
  99. agentpool/repomap.py +1 -1
  100. agentpool/resource_providers/__init__.py +11 -1
  101. agentpool/resource_providers/aggregating.py +56 -5
  102. agentpool/resource_providers/base.py +70 -4
  103. agentpool/resource_providers/codemode/code_executor.py +72 -5
  104. agentpool/resource_providers/codemode/helpers.py +2 -2
  105. agentpool/resource_providers/codemode/provider.py +64 -12
  106. agentpool/resource_providers/codemode/remote_mcp_execution.py +2 -2
  107. agentpool/resource_providers/codemode/remote_provider.py +9 -12
  108. agentpool/resource_providers/filtering.py +3 -1
  109. agentpool/resource_providers/mcp_provider.py +89 -12
  110. agentpool/resource_providers/plan_provider.py +228 -46
  111. agentpool/resource_providers/pool.py +7 -3
  112. agentpool/resource_providers/resource_info.py +111 -0
  113. agentpool/resource_providers/static.py +4 -2
  114. agentpool/sessions/__init__.py +4 -1
  115. agentpool/sessions/manager.py +33 -5
  116. agentpool/sessions/models.py +59 -6
  117. agentpool/sessions/protocol.py +28 -0
  118. agentpool/sessions/session.py +11 -55
  119. agentpool/skills/registry.py +13 -8
  120. agentpool/storage/manager.py +572 -49
  121. agentpool/talk/registry.py +4 -4
  122. agentpool/talk/talk.py +9 -10
  123. agentpool/testing.py +538 -20
  124. agentpool/tool_impls/__init__.py +6 -0
  125. agentpool/tool_impls/agent_cli/__init__.py +42 -0
  126. agentpool/tool_impls/agent_cli/tool.py +95 -0
  127. agentpool/tool_impls/bash/__init__.py +64 -0
  128. agentpool/tool_impls/bash/helpers.py +35 -0
  129. agentpool/tool_impls/bash/tool.py +171 -0
  130. agentpool/tool_impls/delete_path/__init__.py +70 -0
  131. agentpool/tool_impls/delete_path/tool.py +142 -0
  132. agentpool/tool_impls/download_file/__init__.py +80 -0
  133. agentpool/tool_impls/download_file/tool.py +183 -0
  134. agentpool/tool_impls/execute_code/__init__.py +55 -0
  135. agentpool/tool_impls/execute_code/tool.py +163 -0
  136. agentpool/tool_impls/grep/__init__.py +80 -0
  137. agentpool/tool_impls/grep/tool.py +200 -0
  138. agentpool/tool_impls/list_directory/__init__.py +73 -0
  139. agentpool/tool_impls/list_directory/tool.py +197 -0
  140. agentpool/tool_impls/question/__init__.py +42 -0
  141. agentpool/tool_impls/question/tool.py +127 -0
  142. agentpool/tool_impls/read/__init__.py +104 -0
  143. agentpool/tool_impls/read/tool.py +305 -0
  144. agentpool/tools/__init__.py +2 -1
  145. agentpool/tools/base.py +114 -34
  146. agentpool/tools/manager.py +57 -1
  147. agentpool/ui/base.py +2 -2
  148. agentpool/ui/mock_provider.py +2 -2
  149. agentpool/ui/stdlib_provider.py +2 -2
  150. agentpool/utils/file_watcher.py +269 -0
  151. agentpool/utils/identifiers.py +121 -0
  152. agentpool/utils/pydantic_ai_helpers.py +46 -0
  153. agentpool/utils/streams.py +616 -2
  154. agentpool/utils/subprocess_utils.py +155 -0
  155. agentpool/utils/token_breakdown.py +461 -0
  156. agentpool/vfs_registry.py +7 -2
  157. {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/METADATA +41 -27
  158. agentpool-2.5.0.dist-info/RECORD +579 -0
  159. {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/WHEEL +1 -1
  160. agentpool_cli/__main__.py +24 -0
  161. agentpool_cli/create.py +1 -1
  162. agentpool_cli/serve_acp.py +100 -21
  163. agentpool_cli/serve_agui.py +87 -0
  164. agentpool_cli/serve_opencode.py +119 -0
  165. agentpool_cli/ui.py +557 -0
  166. agentpool_commands/__init__.py +42 -5
  167. agentpool_commands/agents.py +75 -2
  168. agentpool_commands/history.py +62 -0
  169. agentpool_commands/mcp.py +176 -0
  170. agentpool_commands/models.py +56 -3
  171. agentpool_commands/pool.py +260 -0
  172. agentpool_commands/session.py +1 -1
  173. agentpool_commands/text_sharing/__init__.py +119 -0
  174. agentpool_commands/text_sharing/base.py +123 -0
  175. agentpool_commands/text_sharing/github_gist.py +80 -0
  176. agentpool_commands/text_sharing/opencode.py +462 -0
  177. agentpool_commands/text_sharing/paste_rs.py +59 -0
  178. agentpool_commands/text_sharing/pastebin.py +116 -0
  179. agentpool_commands/text_sharing/shittycodingagent.py +112 -0
  180. agentpool_commands/tools.py +57 -0
  181. agentpool_commands/utils.py +80 -30
  182. agentpool_config/__init__.py +30 -2
  183. agentpool_config/agentpool_tools.py +498 -0
  184. agentpool_config/builtin_tools.py +77 -22
  185. agentpool_config/commands.py +24 -1
  186. agentpool_config/compaction.py +258 -0
  187. agentpool_config/converters.py +1 -1
  188. agentpool_config/event_handlers.py +42 -0
  189. agentpool_config/events.py +1 -1
  190. agentpool_config/forward_targets.py +1 -4
  191. agentpool_config/jinja.py +3 -3
  192. agentpool_config/mcp_server.py +132 -6
  193. agentpool_config/nodes.py +1 -1
  194. agentpool_config/observability.py +44 -0
  195. agentpool_config/session.py +0 -3
  196. agentpool_config/storage.py +82 -38
  197. agentpool_config/task.py +3 -3
  198. agentpool_config/tools.py +11 -22
  199. agentpool_config/toolsets.py +109 -233
  200. agentpool_server/a2a_server/agent_worker.py +307 -0
  201. agentpool_server/a2a_server/server.py +23 -18
  202. agentpool_server/acp_server/acp_agent.py +234 -181
  203. agentpool_server/acp_server/commands/acp_commands.py +151 -156
  204. agentpool_server/acp_server/commands/docs_commands/fetch_repo.py +18 -17
  205. agentpool_server/acp_server/event_converter.py +651 -0
  206. agentpool_server/acp_server/input_provider.py +53 -10
  207. agentpool_server/acp_server/server.py +24 -90
  208. agentpool_server/acp_server/session.py +173 -331
  209. agentpool_server/acp_server/session_manager.py +8 -34
  210. agentpool_server/agui_server/server.py +3 -1
  211. agentpool_server/mcp_server/server.py +5 -2
  212. agentpool_server/opencode_server/.rules +95 -0
  213. agentpool_server/opencode_server/ENDPOINTS.md +401 -0
  214. agentpool_server/opencode_server/OPENCODE_UI_TOOLS_COMPLETE.md +202 -0
  215. agentpool_server/opencode_server/__init__.py +19 -0
  216. agentpool_server/opencode_server/command_validation.py +172 -0
  217. agentpool_server/opencode_server/converters.py +975 -0
  218. agentpool_server/opencode_server/dependencies.py +24 -0
  219. agentpool_server/opencode_server/input_provider.py +421 -0
  220. agentpool_server/opencode_server/models/__init__.py +250 -0
  221. agentpool_server/opencode_server/models/agent.py +53 -0
  222. agentpool_server/opencode_server/models/app.py +72 -0
  223. agentpool_server/opencode_server/models/base.py +26 -0
  224. agentpool_server/opencode_server/models/common.py +23 -0
  225. agentpool_server/opencode_server/models/config.py +37 -0
  226. agentpool_server/opencode_server/models/events.py +821 -0
  227. agentpool_server/opencode_server/models/file.py +88 -0
  228. agentpool_server/opencode_server/models/mcp.py +44 -0
  229. agentpool_server/opencode_server/models/message.py +179 -0
  230. agentpool_server/opencode_server/models/parts.py +323 -0
  231. agentpool_server/opencode_server/models/provider.py +81 -0
  232. agentpool_server/opencode_server/models/pty.py +43 -0
  233. agentpool_server/opencode_server/models/question.py +56 -0
  234. agentpool_server/opencode_server/models/session.py +111 -0
  235. agentpool_server/opencode_server/routes/__init__.py +29 -0
  236. agentpool_server/opencode_server/routes/agent_routes.py +473 -0
  237. agentpool_server/opencode_server/routes/app_routes.py +202 -0
  238. agentpool_server/opencode_server/routes/config_routes.py +302 -0
  239. agentpool_server/opencode_server/routes/file_routes.py +571 -0
  240. agentpool_server/opencode_server/routes/global_routes.py +94 -0
  241. agentpool_server/opencode_server/routes/lsp_routes.py +319 -0
  242. agentpool_server/opencode_server/routes/message_routes.py +761 -0
  243. agentpool_server/opencode_server/routes/permission_routes.py +63 -0
  244. agentpool_server/opencode_server/routes/pty_routes.py +300 -0
  245. agentpool_server/opencode_server/routes/question_routes.py +128 -0
  246. agentpool_server/opencode_server/routes/session_routes.py +1276 -0
  247. agentpool_server/opencode_server/routes/tui_routes.py +139 -0
  248. agentpool_server/opencode_server/server.py +475 -0
  249. agentpool_server/opencode_server/state.py +151 -0
  250. agentpool_server/opencode_server/time_utils.py +8 -0
  251. agentpool_storage/__init__.py +12 -0
  252. agentpool_storage/base.py +184 -2
  253. agentpool_storage/claude_provider/ARCHITECTURE.md +433 -0
  254. agentpool_storage/claude_provider/__init__.py +42 -0
  255. agentpool_storage/claude_provider/provider.py +1089 -0
  256. agentpool_storage/file_provider.py +278 -15
  257. agentpool_storage/memory_provider.py +193 -12
  258. agentpool_storage/models.py +3 -0
  259. agentpool_storage/opencode_provider/ARCHITECTURE.md +386 -0
  260. agentpool_storage/opencode_provider/__init__.py +16 -0
  261. agentpool_storage/opencode_provider/helpers.py +414 -0
  262. agentpool_storage/opencode_provider/provider.py +895 -0
  263. agentpool_storage/project_store.py +325 -0
  264. agentpool_storage/session_store.py +26 -6
  265. agentpool_storage/sql_provider/__init__.py +4 -2
  266. agentpool_storage/sql_provider/models.py +48 -0
  267. agentpool_storage/sql_provider/sql_provider.py +269 -3
  268. agentpool_storage/sql_provider/utils.py +12 -13
  269. agentpool_storage/zed_provider/__init__.py +16 -0
  270. agentpool_storage/zed_provider/helpers.py +281 -0
  271. agentpool_storage/zed_provider/models.py +130 -0
  272. agentpool_storage/zed_provider/provider.py +442 -0
  273. agentpool_storage/zed_provider.py +803 -0
  274. agentpool_toolsets/__init__.py +0 -2
  275. agentpool_toolsets/builtin/__init__.py +2 -12
  276. agentpool_toolsets/builtin/code.py +96 -57
  277. agentpool_toolsets/builtin/debug.py +118 -48
  278. agentpool_toolsets/builtin/execution_environment.py +115 -230
  279. agentpool_toolsets/builtin/file_edit/file_edit.py +115 -7
  280. agentpool_toolsets/builtin/skills.py +9 -4
  281. agentpool_toolsets/builtin/subagent_tools.py +64 -51
  282. agentpool_toolsets/builtin/workers.py +4 -2
  283. agentpool_toolsets/composio_toolset.py +2 -2
  284. agentpool_toolsets/entry_points.py +3 -1
  285. agentpool_toolsets/fsspec_toolset/__init__.py +13 -1
  286. agentpool_toolsets/fsspec_toolset/diagnostics.py +860 -73
  287. agentpool_toolsets/fsspec_toolset/grep.py +99 -7
  288. agentpool_toolsets/fsspec_toolset/helpers.py +3 -2
  289. agentpool_toolsets/fsspec_toolset/image_utils.py +161 -0
  290. agentpool_toolsets/fsspec_toolset/toolset.py +500 -95
  291. agentpool_toolsets/mcp_discovery/__init__.py +5 -0
  292. agentpool_toolsets/mcp_discovery/data/mcp_servers.parquet +0 -0
  293. agentpool_toolsets/mcp_discovery/toolset.py +511 -0
  294. agentpool_toolsets/mcp_run_toolset.py +87 -12
  295. agentpool_toolsets/notifications.py +33 -33
  296. agentpool_toolsets/openapi.py +3 -1
  297. agentpool_toolsets/search_toolset.py +3 -1
  298. agentpool-2.1.9.dist-info/RECORD +0 -474
  299. agentpool_config/resources.py +0 -33
  300. agentpool_server/acp_server/acp_tools.py +0 -43
  301. agentpool_server/acp_server/commands/spawn.py +0 -210
  302. agentpool_storage/text_log_provider.py +0 -275
  303. agentpool_toolsets/builtin/agent_management.py +0 -239
  304. agentpool_toolsets/builtin/chain.py +0 -288
  305. agentpool_toolsets/builtin/history.py +0 -36
  306. agentpool_toolsets/builtin/integration.py +0 -85
  307. agentpool_toolsets/builtin/tool_management.py +0 -90
  308. agentpool_toolsets/builtin/user_interaction.py +0 -52
  309. agentpool_toolsets/semantic_memory_toolset.py +0 -536
  310. {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/entry_points.txt +0 -0
  311. {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,41 @@
1
+ """LSP Proxy - Helper for deploying and managing LSP proxy instances.
2
+
3
+ The actual proxy script is in lsp_proxy_script.py and is executed as a subprocess.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from pathlib import Path
9
+
10
+
11
+ # Path to the proxy script
12
+ _PROXY_SCRIPT_PATH = Path(__file__).parent / "lsp_proxy_script.py"
13
+
14
+
15
+ class LSPProxy:
16
+ """Helper class for deploying and managing LSP proxy instances."""
17
+
18
+ @staticmethod
19
+ def get_script_path() -> Path:
20
+ """Get the path to the proxy script."""
21
+ return _PROXY_SCRIPT_PATH
22
+
23
+ @staticmethod
24
+ def get_start_command(lsp_command: str, port_file: str) -> list[str]:
25
+ """Get command to start proxy as a background process.
26
+
27
+ Args:
28
+ lsp_command: The LSP server command (e.g., "pyright-langserver --stdio")
29
+ port_file: Path for the port file (proxy writes its port here)
30
+
31
+ Returns:
32
+ Command and args for process_manager.start_process()
33
+ """
34
+ return [
35
+ "python",
36
+ str(_PROXY_SCRIPT_PATH),
37
+ "--command",
38
+ lsp_command,
39
+ "--port-file",
40
+ port_file,
41
+ ]
@@ -0,0 +1,229 @@
1
+ """LSP Proxy - Wraps stdio LSP server, exposes via TCP.
2
+
3
+ This script is executed as a subprocess to proxy between TCP clients
4
+ and a stdio-based LSP server.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import argparse
10
+ import asyncio
11
+ import contextlib
12
+ import json
13
+ from pathlib import Path
14
+ import sys
15
+ from typing import Any
16
+
17
+
18
+ class LSPProxy:
19
+ """Proxies between TCP clients and a stdio-based LSP server."""
20
+
21
+ def __init__(self, command: list[str], port_file: str):
22
+ self.command = command
23
+ self.port_file = port_file
24
+ self._port_file_path = Path(port_file)
25
+ self.process: asyncio.subprocess.Process | None = None
26
+ self.lock = asyncio.Lock()
27
+ self._request_id = 0
28
+ self.port: int | None = None
29
+
30
+ async def start(self) -> None:
31
+ """Start the LSP server subprocess."""
32
+ # Remove existing port file if present
33
+ if self._port_file_path.exists():
34
+ self._port_file_path.unlink()
35
+
36
+ # Use asyncio subprocess for native async I/O
37
+ self.process = await asyncio.create_subprocess_exec(
38
+ *self.command,
39
+ stdin=asyncio.subprocess.PIPE,
40
+ stdout=asyncio.subprocess.PIPE,
41
+ stderr=asyncio.subprocess.PIPE,
42
+ )
43
+
44
+ async def _read_lsp_message(self) -> dict[str, Any] | None:
45
+ """Read a single JSON-RPC message from LSP server stdout."""
46
+ if not self.process or not self.process.stdout:
47
+ return None
48
+
49
+ headers = {}
50
+ while True:
51
+ line = await self.process.stdout.readline()
52
+ if not line:
53
+ return None # EOF
54
+ line_str = line.decode().strip()
55
+ if not line_str:
56
+ break # Empty line = end of headers
57
+ if ": " in line_str:
58
+ key, value = line_str.split(": ", 1)
59
+ headers[key] = value
60
+
61
+ if "Content-Length" not in headers:
62
+ return None
63
+
64
+ length = int(headers["Content-Length"])
65
+ body = await self.process.stdout.readexactly(length)
66
+ result: dict[str, Any] = json.loads(body)
67
+ return result
68
+
69
+ async def _send_to_lsp(self, message: dict[str, Any]) -> None:
70
+ """Send JSON-RPC message to LSP server."""
71
+ if not self.process or not self.process.stdin:
72
+ raise RuntimeError("LSP server not running")
73
+
74
+ payload = json.dumps(message)
75
+ header = f"Content-Length: {len(payload)}\r\n\r\n"
76
+ self.process.stdin.write((header + payload).encode())
77
+ await self.process.stdin.drain()
78
+
79
+ async def send_request(self, method: str, params: dict[str, Any]) -> dict[str, Any]:
80
+ """Send request to LSP and wait for response."""
81
+ if not self.process:
82
+ return {"error": "LSP server not running"}
83
+
84
+ async with self.lock:
85
+ self._request_id += 1
86
+ msg_id = self._request_id
87
+ request = {"jsonrpc": "2.0", "id": msg_id, "method": method, "params": params}
88
+ await self._send_to_lsp(request)
89
+
90
+ try:
91
+ response = await asyncio.wait_for(self._read_lsp_message(), timeout=30.0)
92
+ except TimeoutError:
93
+ # Check stderr for error info
94
+ if self.process.stderr:
95
+ try:
96
+ err = await asyncio.wait_for(self.process.stderr.read(4096), timeout=0.1)
97
+ if err:
98
+ return {"error": f"Timeout, stderr: {err.decode()}"}
99
+ except TimeoutError:
100
+ pass
101
+ return {"error": "Timeout waiting for LSP response"}
102
+
103
+ if response is None:
104
+ # Check stderr for error info
105
+ if self.process.stderr:
106
+ try:
107
+ err = await asyncio.wait_for(self.process.stderr.read(4096), timeout=0.1)
108
+ if err:
109
+ return {"error": f"LSP error: {err.decode()}"}
110
+ except TimeoutError:
111
+ pass
112
+ return {"error": "No response from LSP server"}
113
+
114
+ return response
115
+
116
+ async def send_notification(self, method: str, params: dict[str, Any]) -> None:
117
+ """Send notification to LSP (no response expected)."""
118
+ notification = {"jsonrpc": "2.0", "method": method, "params": params}
119
+ async with self.lock:
120
+ await self._send_to_lsp(notification)
121
+
122
+ async def handle_client(
123
+ self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter
124
+ ) -> None:
125
+ """Handle incoming client connection."""
126
+ try:
127
+ while True:
128
+ # Read Content-Length header
129
+ headers = b""
130
+ while b"\r\n\r\n" not in headers:
131
+ chunk = await reader.read(1)
132
+ if not chunk:
133
+ return # Client disconnected
134
+ headers += chunk
135
+
136
+ # Parse Content-Length
137
+ header_str = headers.decode()
138
+ length = None
139
+ for line in header_str.split("\r\n"):
140
+ if line.startswith("Content-Length:"):
141
+ length = int(line.split(":")[1].strip())
142
+ break
143
+
144
+ if length is None:
145
+ return
146
+
147
+ # Read body
148
+ body = await reader.read(length)
149
+ request = json.loads(body)
150
+
151
+ # Forward to LSP
152
+ if "id" in request:
153
+ # It's a request, wait for response
154
+ response = await self.send_request(request["method"], request.get("params", {}))
155
+ else:
156
+ # It's a notification
157
+ await self.send_notification(request["method"], request.get("params", {}))
158
+ continue # No response to send
159
+
160
+ # Send response back to client
161
+ payload = json.dumps(response)
162
+ header = f"Content-Length: {len(payload)}\r\n\r\n"
163
+ writer.write(header.encode() + payload.encode())
164
+ await writer.drain()
165
+
166
+ except (ConnectionError, json.JSONDecodeError, UnicodeDecodeError) as e:
167
+ print(f"Client error: {e}", file=sys.stderr, flush=True)
168
+ finally:
169
+ writer.close()
170
+ await writer.wait_closed()
171
+
172
+ async def run(self) -> None:
173
+ """Start proxy server."""
174
+ await self.start()
175
+
176
+ # Ensure parent directory exists
177
+ self._port_file_path.parent.mkdir(parents=True, exist_ok=True)
178
+
179
+ # Start TCP server on localhost with dynamic port
180
+ server = await asyncio.start_server(self.handle_client, host="127.0.0.1", port=0)
181
+
182
+ # Get the assigned port
183
+ addr = server.sockets[0].getsockname()
184
+ self.port = addr[1]
185
+
186
+ # Write port to file so clients know where to connect
187
+ self._port_file_path.write_text(str(self.port))
188
+
189
+ # Signal ready by creating a marker file
190
+ ready_path = Path(str(self.port_file) + ".ready")
191
+ ready_path.touch()
192
+
193
+ print(f"LSP Proxy listening on 127.0.0.1:{self.port}", file=sys.stderr, flush=True)
194
+
195
+ async with server:
196
+ await server.serve_forever()
197
+
198
+ async def shutdown(self) -> None:
199
+ """Shutdown the proxy and LSP server."""
200
+ if self.process:
201
+ self.process.terminate()
202
+ try:
203
+ await asyncio.wait_for(self.process.wait(), timeout=5)
204
+ except TimeoutError:
205
+ self.process.kill()
206
+
207
+ # Cleanup files
208
+ ready_path = Path(str(self.port_file) + ".ready")
209
+ if self._port_file_path.exists():
210
+ self._port_file_path.unlink()
211
+ if ready_path.exists():
212
+ ready_path.unlink()
213
+
214
+
215
+ def main() -> None:
216
+ """Entry point for the LSP proxy script."""
217
+ parser = argparse.ArgumentParser(description="LSP Proxy Server")
218
+ parser.add_argument("--command", required=True, help="LSP server command")
219
+ parser.add_argument("--port-file", required=True, help="File to write port number to")
220
+ args = parser.parse_args()
221
+
222
+ proxy = LSPProxy(args.command.split(), args.port_file)
223
+
224
+ with contextlib.suppress(KeyboardInterrupt):
225
+ asyncio.run(proxy.run())
226
+
227
+
228
+ if __name__ == "__main__":
229
+ main()
@@ -0,0 +1,398 @@
1
+ """Data models for diagnostics and LSP operations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Any, Literal, TypedDict
7
+
8
+
9
+ @dataclass
10
+ class Diagnostic:
11
+ """A single diagnostic message from an LSP server or CLI tool."""
12
+
13
+ file: str
14
+ """File path the diagnostic applies to."""
15
+
16
+ line: int
17
+ """1-based line number."""
18
+
19
+ column: int
20
+ """1-based column number."""
21
+
22
+ severity: Literal["error", "warning", "info", "hint"]
23
+ """Severity level."""
24
+
25
+ message: str
26
+ """Human-readable diagnostic message."""
27
+
28
+ source: str
29
+ """Tool/server that produced this diagnostic (e.g., 'pyright', 'mypy')."""
30
+
31
+ code: str | None = None
32
+ """Optional error code (e.g., 'reportGeneralTypeIssues')."""
33
+
34
+ end_line: int | None = None
35
+ """End line for range diagnostics."""
36
+
37
+ end_column: int | None = None
38
+ """End column for range diagnostics."""
39
+
40
+
41
+ @dataclass
42
+ class DiagnosticsResult:
43
+ """Result of running diagnostics on files."""
44
+
45
+ diagnostics: list[Diagnostic] = field(default_factory=list)
46
+ """List of diagnostics found."""
47
+
48
+ success: bool = True
49
+ """Whether the diagnostic run completed without errors."""
50
+
51
+ duration: float = 0.0
52
+ """Time taken in seconds."""
53
+
54
+ error: str | None = None
55
+ """Error message if success is False."""
56
+
57
+ server_id: str | None = None
58
+ """ID of the server that produced these results."""
59
+
60
+ @property
61
+ def error_count(self) -> int:
62
+ """Count of error-level diagnostics."""
63
+ return sum(1 for d in self.diagnostics if d.severity == "error")
64
+
65
+ @property
66
+ def warning_count(self) -> int:
67
+ """Count of warning-level diagnostics."""
68
+ return sum(1 for d in self.diagnostics if d.severity == "warning")
69
+
70
+
71
+ @dataclass
72
+ class LSPServerState:
73
+ """State of a running LSP server."""
74
+
75
+ server_id: str
76
+ """Identifier for this server (e.g., 'pyright', 'pylsp')."""
77
+
78
+ process_id: str
79
+ """Process ID from ProcessManager."""
80
+
81
+ port: int
82
+ """TCP port for communication."""
83
+
84
+ language: str
85
+ """Primary language this server handles."""
86
+
87
+ root_uri: str | None = None
88
+ """Workspace root URI."""
89
+
90
+ initialized: bool = False
91
+ """Whether LSP initialize handshake completed."""
92
+
93
+ capabilities: dict[str, Any] = field(default_factory=dict)
94
+ """Server capabilities from initialize response."""
95
+
96
+
97
+ @dataclass
98
+ class Position:
99
+ """A position in a text document (0-based line and character)."""
100
+
101
+ line: int
102
+ """0-based line number."""
103
+
104
+ character: int
105
+ """0-based character offset."""
106
+
107
+
108
+ @dataclass
109
+ class Range:
110
+ """A range in a text document."""
111
+
112
+ start: Position
113
+ """Start position (inclusive)."""
114
+
115
+ end: Position
116
+ """End position (exclusive)."""
117
+
118
+
119
+ @dataclass
120
+ class Location:
121
+ """A location in a resource (file + range)."""
122
+
123
+ uri: str
124
+ """File URI."""
125
+
126
+ range: Range
127
+ """Range within the file."""
128
+
129
+
130
+ @dataclass
131
+ class SymbolInfo:
132
+ """Information about a symbol (function, class, variable, etc.)."""
133
+
134
+ name: str
135
+ """Symbol name."""
136
+
137
+ kind: str
138
+ """Symbol kind (function, class, variable, etc.)."""
139
+
140
+ location: Location
141
+ """Where the symbol is defined."""
142
+
143
+ container_name: str | None = None
144
+ """Name of the containing symbol (e.g., class name for a method)."""
145
+
146
+ detail: str | None = None
147
+ """Additional detail (e.g., type signature)."""
148
+
149
+ children: list[SymbolInfo] = field(default_factory=list)
150
+ """Child symbols (for document symbols with hierarchy)."""
151
+
152
+
153
+ @dataclass
154
+ class HoverInfo:
155
+ """Hover information for a position."""
156
+
157
+ contents: str
158
+ """Hover content (may be markdown)."""
159
+
160
+ range: Range | None = None
161
+ """Range the hover applies to."""
162
+
163
+
164
+ @dataclass
165
+ class CompletionItem:
166
+ """A completion suggestion."""
167
+
168
+ label: str
169
+ """The label shown in completion list."""
170
+
171
+ kind: str | None = None
172
+ """Kind of completion (function, variable, class, etc.)."""
173
+
174
+ detail: str | None = None
175
+ """Detail shown next to label."""
176
+
177
+ documentation: str | None = None
178
+ """Documentation string."""
179
+
180
+ insert_text: str | None = None
181
+ """Text to insert (if different from label)."""
182
+
183
+ sort_text: str | None = None
184
+ """Sort order text."""
185
+
186
+
187
+ @dataclass
188
+ class CodeAction:
189
+ """A code action (quick fix, refactoring, etc.)."""
190
+
191
+ title: str
192
+ """Title of the action."""
193
+
194
+ kind: str | None = None
195
+ """Kind of action (quickfix, refactor, etc.)."""
196
+
197
+ diagnostics: list[Diagnostic] = field(default_factory=list)
198
+ """Diagnostics this action resolves."""
199
+
200
+ is_preferred: bool = False
201
+ """Whether this is the preferred action."""
202
+
203
+ edit: dict[str, Any] | None = None
204
+ """Workspace edit to apply."""
205
+
206
+ command: dict[str, Any] | None = None
207
+ """Command to execute."""
208
+
209
+
210
+ @dataclass
211
+ class CallHierarchyItem:
212
+ """An item in a call hierarchy."""
213
+
214
+ name: str
215
+ """Name of the callable."""
216
+
217
+ kind: str
218
+ """Symbol kind."""
219
+
220
+ uri: str
221
+ """File URI."""
222
+
223
+ range: Range
224
+ """Range of the callable."""
225
+
226
+ selection_range: Range
227
+ """Range to select (usually the name)."""
228
+
229
+ detail: str | None = None
230
+ """Additional detail."""
231
+
232
+ data: Any = None
233
+ """Server-specific data for resolving."""
234
+
235
+
236
+ @dataclass
237
+ class CallHierarchyCall:
238
+ """A call in a call hierarchy."""
239
+
240
+ item: CallHierarchyItem
241
+ """The calling/called item."""
242
+
243
+ from_ranges: list[Range] = field(default_factory=list)
244
+ """Ranges where the call occurs."""
245
+
246
+
247
+ @dataclass
248
+ class TypeHierarchyItem:
249
+ """An item in a type hierarchy."""
250
+
251
+ name: str
252
+ """Name of the type."""
253
+
254
+ kind: str
255
+ """Symbol kind."""
256
+
257
+ uri: str
258
+ """File URI."""
259
+
260
+ range: Range
261
+ """Range of the type."""
262
+
263
+ selection_range: Range
264
+ """Range to select (usually the name)."""
265
+
266
+ detail: str | None = None
267
+ """Additional detail."""
268
+
269
+ data: Any = None
270
+ """Server-specific data for resolving."""
271
+
272
+
273
+ @dataclass
274
+ class SignatureInfo:
275
+ """Signature help information."""
276
+
277
+ label: str
278
+ """The signature label."""
279
+
280
+ documentation: str | None = None
281
+ """Documentation string."""
282
+
283
+ parameters: list[dict[str, Any]] = field(default_factory=list)
284
+ """Parameter information."""
285
+
286
+ active_parameter: int | None = None
287
+ """Index of the active parameter."""
288
+
289
+
290
+ @dataclass
291
+ class RenameResult:
292
+ """Result of a rename operation."""
293
+
294
+ changes: dict[str, list[dict[str, Any]]]
295
+ """Map of file URI to list of text edits."""
296
+
297
+ success: bool = True
298
+ """Whether rename preparation succeeded."""
299
+
300
+ error: str | None = None
301
+ """Error message if failed."""
302
+
303
+
304
+ # LSP Hover content types (from LSP spec)
305
+
306
+
307
+ class MarkedStringDict(TypedDict):
308
+ """MarkedString with language identifier (deprecated in LSP 3.0+)."""
309
+
310
+ language: str
311
+ value: str
312
+
313
+
314
+ class MarkupContent(TypedDict):
315
+ """MarkupContent from LSP spec."""
316
+
317
+ kind: str # "plaintext" or "markdown"
318
+ value: str
319
+
320
+
321
+ # Union type for hover contents
322
+ type MarkedString = str | MarkedStringDict
323
+ type HoverContents = str | MarkupContent | MarkedStringDict | list[MarkedString]
324
+
325
+
326
+ # Symbol kind mapping from LSP numeric values
327
+ SYMBOL_KIND_MAP: dict[int, str] = {
328
+ 1: "file",
329
+ 2: "module",
330
+ 3: "namespace",
331
+ 4: "package",
332
+ 5: "class",
333
+ 6: "method",
334
+ 7: "property",
335
+ 8: "field",
336
+ 9: "constructor",
337
+ 10: "enum",
338
+ 11: "interface",
339
+ 12: "function",
340
+ 13: "variable",
341
+ 14: "constant",
342
+ 15: "string",
343
+ 16: "number",
344
+ 17: "boolean",
345
+ 18: "array",
346
+ 19: "object",
347
+ 20: "key",
348
+ 21: "null",
349
+ 22: "enum_member",
350
+ 23: "struct",
351
+ 24: "event",
352
+ 25: "operator",
353
+ 26: "type_parameter",
354
+ }
355
+
356
+
357
+ # Completion item kind mapping
358
+ COMPLETION_KIND_MAP: dict[int, str] = {
359
+ 1: "text",
360
+ 2: "method",
361
+ 3: "function",
362
+ 4: "constructor",
363
+ 5: "field",
364
+ 6: "variable",
365
+ 7: "class",
366
+ 8: "interface",
367
+ 9: "module",
368
+ 10: "property",
369
+ 11: "unit",
370
+ 12: "value",
371
+ 13: "enum",
372
+ 14: "keyword",
373
+ 15: "snippet",
374
+ 16: "color",
375
+ 17: "file",
376
+ 18: "reference",
377
+ 19: "folder",
378
+ 20: "enum_member",
379
+ 21: "constant",
380
+ 22: "struct",
381
+ 23: "event",
382
+ 24: "operator",
383
+ 25: "type_parameter",
384
+ }
385
+
386
+
387
+ # Code action kind mapping
388
+ CODE_ACTION_KIND_MAP: dict[str, str] = {
389
+ "": "quickfix",
390
+ "quickfix": "quickfix",
391
+ "refactor": "refactor",
392
+ "refactor.extract": "refactor.extract",
393
+ "refactor.inline": "refactor.inline",
394
+ "refactor.rewrite": "refactor.rewrite",
395
+ "source": "source",
396
+ "source.organizeImports": "source.organize_imports",
397
+ "source.fixAll": "source.fix_all",
398
+ }
@@ -42,11 +42,14 @@ async def run_agent(
42
42
  ) -> Any:
43
43
  """Run prompt through agent and return result."""
44
44
  async with Agent[Any, str](**kwargs) as agent:
45
+ # Convert to structured output agent if output_type specified
46
+ final = agent.to_structured(output_type) if output_type is not None else agent
47
+
45
48
  if image_url:
46
49
  image = ImageUrl(url=image_url)
47
- result = await agent.run(prompt, image, output_type=output_type)
50
+ result = await final.run(prompt, image)
48
51
  else:
49
- result = await agent.run(prompt, output_type=output_type)
52
+ result = await final.run(prompt)
50
53
  return result.content
51
54
 
52
55
 
@@ -76,5 +79,8 @@ def run_agent_sync(
76
79
  **kwargs: Unpack[AgentKwargs],
77
80
  ) -> Any:
78
81
  """Sync wrapper for run_agent."""
79
- coro = run_agent(prompt, image_url, output_type=output_type, **kwargs) # type: ignore
80
- return run_sync(coro)
82
+
83
+ async def _run() -> Any:
84
+ return await run_agent(prompt, image_url, output_type=output_type, **kwargs) # type: ignore
85
+
86
+ return run_sync(_run())