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,139 @@
1
+ """TUI routes for external control of the TUI.
2
+
3
+ These endpoints allow external integrations (e.g., VSCode extension) to control
4
+ the TUI by broadcasting events that the TUI listens for via SSE.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Literal
10
+
11
+ from fastapi import APIRouter
12
+ from pydantic import BaseModel, Field
13
+
14
+ from agentpool_server.opencode_server.dependencies import StateDep
15
+ from agentpool_server.opencode_server.models.events import (
16
+ TuiCommandExecuteEvent,
17
+ TuiPromptAppendEvent,
18
+ TuiToastShowEvent,
19
+ )
20
+
21
+
22
+ router = APIRouter(prefix="/tui", tags=["tui"])
23
+
24
+
25
+ class AppendPromptRequest(BaseModel):
26
+ """Request body for appending text to the prompt."""
27
+
28
+ text: str = Field(..., description="Text to append to the prompt")
29
+
30
+
31
+ class ExecuteCommandRequest(BaseModel):
32
+ """Request body for executing a TUI command."""
33
+
34
+ command: str = Field(
35
+ ...,
36
+ description="Command to execute (e.g., 'prompt.submit', 'prompt.clear', 'session.new')",
37
+ )
38
+
39
+
40
+ class ShowToastRequest(BaseModel):
41
+ """Request body for showing a toast notification."""
42
+
43
+ title: str | None = Field(None, description="Optional toast title")
44
+ message: str = Field(..., description="Toast message")
45
+ variant: Literal["info", "success", "warning", "error"] = Field(
46
+ "info", description="Toast variant"
47
+ )
48
+ duration: int = Field(5000, description="Duration in milliseconds")
49
+
50
+
51
+ @router.post("/append-prompt")
52
+ async def append_prompt(request: AppendPromptRequest, state: StateDep) -> bool:
53
+ """Append text to the TUI prompt.
54
+
55
+ Used by external integrations (e.g., VSCode) to insert text like file
56
+ references into the prompt input.
57
+ """
58
+ await state.broadcast_event(TuiPromptAppendEvent.create(request.text))
59
+ return True
60
+
61
+
62
+ @router.post("/submit-prompt")
63
+ async def submit_prompt(state: StateDep) -> bool:
64
+ """Submit the current prompt.
65
+
66
+ Triggers the TUI to submit whatever is currently in the prompt input.
67
+ """
68
+ await state.broadcast_event(TuiCommandExecuteEvent.create("prompt.submit"))
69
+ return True
70
+
71
+
72
+ @router.post("/clear-prompt")
73
+ async def clear_prompt(state: StateDep) -> bool:
74
+ """Clear the TUI prompt.
75
+
76
+ Clears any text currently in the prompt input.
77
+ """
78
+ await state.broadcast_event(TuiCommandExecuteEvent.create("prompt.clear"))
79
+ return True
80
+
81
+
82
+ @router.post("/execute-command")
83
+ async def execute_command(request: ExecuteCommandRequest, state: StateDep) -> bool:
84
+ """Execute a TUI command.
85
+
86
+ Available commands:
87
+ - session.list, session.new, session.share, session.interrupt, session.compact
88
+ - session.page.up, session.page.down, session.half.page.up, session.half.page.down
89
+ - session.first, session.last
90
+ - prompt.clear, prompt.submit
91
+ - agent.cycle
92
+ """
93
+ await state.broadcast_event(TuiCommandExecuteEvent.create(request.command))
94
+ return True
95
+
96
+
97
+ @router.post("/show-toast")
98
+ async def show_toast(request: ShowToastRequest, state: StateDep) -> bool:
99
+ """Show a toast notification in the TUI."""
100
+ await state.broadcast_event(
101
+ TuiToastShowEvent.create(
102
+ message=request.message,
103
+ variant=request.variant,
104
+ title=request.title,
105
+ duration=request.duration,
106
+ )
107
+ )
108
+ return True
109
+
110
+
111
+ # Additional convenience endpoints matching OpenCode's API
112
+
113
+
114
+ @router.post("/open-help")
115
+ async def open_help(state: StateDep) -> bool:
116
+ """Open the help dialog."""
117
+ await state.broadcast_event(TuiCommandExecuteEvent.create("help.open"))
118
+ return True
119
+
120
+
121
+ @router.post("/open-sessions")
122
+ async def open_sessions(state: StateDep) -> bool:
123
+ """Open the session selector."""
124
+ await state.broadcast_event(TuiCommandExecuteEvent.create("session.list"))
125
+ return True
126
+
127
+
128
+ @router.post("/open-themes")
129
+ async def open_themes(state: StateDep) -> bool:
130
+ """Open the theme selector."""
131
+ await state.broadcast_event(TuiCommandExecuteEvent.create("theme.list"))
132
+ return True
133
+
134
+
135
+ @router.post("/open-models")
136
+ async def open_models(state: StateDep) -> bool:
137
+ """Open the model selector."""
138
+ await state.broadcast_event(TuiCommandExecuteEvent.create("model.list"))
139
+ return True
@@ -0,0 +1,475 @@
1
+ """OpenCode-compatible FastAPI server.
2
+
3
+ This server implements the OpenCode API endpoints to allow OpenCode SDK clients
4
+ to interact with AgentPool agents.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from contextlib import asynccontextmanager
10
+ from pathlib import Path
11
+ from typing import TYPE_CHECKING, Any
12
+
13
+ from fastapi import FastAPI, Request # noqa: TC002
14
+ from fastapi.exceptions import RequestValidationError
15
+ from fastapi.middleware.cors import CORSMiddleware
16
+ from fastapi.responses import JSONResponse, RedirectResponse, Response
17
+
18
+ from agentpool import AgentPool
19
+ from agentpool_server.opencode_server.routes import (
20
+ agent_router,
21
+ app_router,
22
+ config_router,
23
+ file_router,
24
+ global_router,
25
+ lsp_router,
26
+ message_router,
27
+ permission_router,
28
+ pty_router,
29
+ question_router,
30
+ session_router,
31
+ tui_router,
32
+ )
33
+ from agentpool_server.opencode_server.state import ServerState
34
+
35
+
36
+ class OpenCodeJSONResponse(JSONResponse):
37
+ """Custom JSON response that excludes None values (like OpenCode does)."""
38
+
39
+ def render(self, content: Any) -> bytes:
40
+ from fastapi.encoders import jsonable_encoder
41
+
42
+ return super().render(jsonable_encoder(content, exclude_none=True))
43
+
44
+
45
+ if TYPE_CHECKING:
46
+ from collections.abc import AsyncIterator, Set as AbstractSet
47
+
48
+ from agentpool.storage.manager import TitleGeneratedEvent
49
+
50
+
51
+ VERSION = "0.1.0"
52
+
53
+
54
+ async def check_pypi_version(package: str = "agentpool") -> str | None:
55
+ """Check PyPI for the latest version of a package.
56
+
57
+ Args:
58
+ package: Package name to check
59
+
60
+ Returns:
61
+ Latest version string, or None if check fails
62
+ """
63
+ import httpx
64
+
65
+ try:
66
+ async with httpx.AsyncClient(timeout=5.0) as client:
67
+ response = await client.get(f"https://pypi.org/pypi/{package}/json")
68
+ if response.status_code == 200: # noqa: PLR2004
69
+ data: dict[str, Any] = response.json()
70
+ info: dict[str, Any] = data.get("info", {})
71
+ version: str | None = info.get("version")
72
+ return version
73
+ except Exception: # noqa: BLE001
74
+ pass
75
+ return None
76
+
77
+
78
+ def compare_versions(current: str, latest: str) -> bool:
79
+ """Check if latest version is newer than current.
80
+
81
+ Args:
82
+ current: Current version string
83
+ latest: Latest version string
84
+
85
+ Returns:
86
+ True if latest is newer than current
87
+ """
88
+ from packaging.version import Version
89
+
90
+ try:
91
+ return Version(latest) > Version(current)
92
+ except Exception: # noqa: BLE001
93
+ return False
94
+
95
+
96
+ def create_app( # noqa: PLR0915
97
+ *,
98
+ pool: AgentPool[Any],
99
+ agent_name: str | None = None,
100
+ working_dir: str | None = None,
101
+ ) -> FastAPI:
102
+ """Create the FastAPI application.
103
+
104
+ Args:
105
+ pool: AgentPool for session persistence and agent access.
106
+ agent_name: Name of the agent to use for handling messages.
107
+ If None, uses the first agent in the pool.
108
+ working_dir: Working directory for file operations. Defaults to cwd.
109
+
110
+ Returns:
111
+ Configured FastAPI application.
112
+
113
+ Raises:
114
+ ValueError: If specified agent_name not found or pool has no agents.
115
+ """
116
+ # Resolve the agent from the pool
117
+ import logfire
118
+
119
+ if agent_name:
120
+ agent = pool.all_agents.get(agent_name)
121
+ if agent is None:
122
+ msg = f"Agent '{agent_name}' not found in pool"
123
+ raise ValueError(msg)
124
+ else:
125
+ # Use first agent as default
126
+ agent = next(iter(pool.all_agents.values()), None)
127
+ if agent is None:
128
+ msg = "Pool has no agents"
129
+ raise ValueError(msg)
130
+
131
+ state = ServerState(
132
+ working_dir=working_dir or str(Path.cwd()),
133
+ pool=pool,
134
+ agent=agent,
135
+ )
136
+
137
+ # Set up todo change callback to broadcast events
138
+ async def on_todo_change(tracker: Any) -> None:
139
+ """Broadcast todo updates to all active sessions."""
140
+ from agentpool_server.opencode_server.models.events import Todo, TodoUpdatedEvent
141
+
142
+ # Convert tracker entries to OpenCode Todo models
143
+ todos = [
144
+ Todo(id=e.id, content=e.content, status=e.status, priority=e.priority)
145
+ for e in tracker.entries
146
+ ]
147
+ # Broadcast to all active sessions
148
+ for session_id in state.sessions:
149
+ event = TodoUpdatedEvent.create(session_id=session_id, todos=todos)
150
+ await state.broadcast_event(event)
151
+
152
+ pool.todos.on_change = on_todo_change
153
+
154
+ # Set up title generation callback to update OpenCode sessions
155
+
156
+ async def on_title_generated(event: TitleGeneratedEvent) -> None:
157
+ """Update session when title is generated by StorageManager."""
158
+ import logging
159
+
160
+ from agentpool_server.opencode_server.models.events import SessionUpdatedEvent
161
+ from agentpool_server.opencode_server.routes.session_routes import opencode_to_session_data
162
+
163
+ log = logging.getLogger(__name__)
164
+ log.info("on_title_generated called: %s, title=%s", event.conversation_id, event.title)
165
+
166
+ session_id = event.conversation_id
167
+ if session_id in state.sessions:
168
+ # Update in-memory session
169
+ session = state.sessions[session_id]
170
+ updated_session = session.model_copy(update={"title": event.title})
171
+ state.sessions[session_id] = updated_session
172
+
173
+ # Persist to storage
174
+ session_data = opencode_to_session_data(
175
+ updated_session,
176
+ agent_name=state.agent.name,
177
+ pool_id=state.pool.manifest.config_file_path,
178
+ )
179
+ await state.pool.sessions.store.save(session_data)
180
+
181
+ # Broadcast session update to UI
182
+ await state.broadcast_event(SessionUpdatedEvent.create(updated_session))
183
+ else:
184
+ log.warning("Session %s not found in state.sessions", session_id)
185
+
186
+ # Connect to storage manager's title_generated signal
187
+ if pool.storage:
188
+ pool.storage.title_generated.connect(on_title_generated)
189
+
190
+ # Watchers for VCS and file events
191
+ git_branch_watcher: Any = None
192
+ project_file_watcher: Any = None
193
+
194
+ @asynccontextmanager
195
+ async def lifespan(app: FastAPI) -> AsyncIterator[None]:
196
+ nonlocal git_branch_watcher, project_file_watcher
197
+ import logging
198
+
199
+ from watchfiles import Change
200
+
201
+ from agentpool.utils.file_watcher import FileWatcher, GitBranchWatcher
202
+ from agentpool_server.opencode_server.models.events import (
203
+ FileWatcherUpdatedEvent,
204
+ VcsBranchUpdatedEvent,
205
+ )
206
+
207
+ log = logging.getLogger(__name__)
208
+
209
+ # --- Git branch watcher ---
210
+ async def on_branch_change(branch: str | None) -> None:
211
+ """Broadcast branch change to all subscribers."""
212
+ log.info("Broadcasting vcs.branch.updated event: %s", branch)
213
+ event = VcsBranchUpdatedEvent.create(branch=branch)
214
+ await state.broadcast_event(event)
215
+
216
+ log.info("Setting up GitBranchWatcher for: %s", state.working_dir)
217
+ git_branch_watcher = GitBranchWatcher(
218
+ repo_path=state.working_dir,
219
+ callback=on_branch_change,
220
+ )
221
+ await git_branch_watcher.start()
222
+ log.info("GitBranchWatcher started, current branch: %s", git_branch_watcher.current_branch)
223
+
224
+ # --- Project file watcher ---
225
+ # Map watchfiles Change types to OpenCode event types
226
+ change_type_map: dict[Change, str] = {
227
+ Change.added: "add",
228
+ Change.modified: "change",
229
+ Change.deleted: "unlink",
230
+ }
231
+
232
+ async def on_file_change(changes: AbstractSet[tuple[Change, str]]) -> None:
233
+ """Broadcast file changes to all subscribers."""
234
+ for change_type, file_path in changes:
235
+ # Skip .git directory changes
236
+ if "/.git/" in file_path or file_path.endswith("/.git"):
237
+ continue
238
+ event_type = change_type_map.get(change_type, "change")
239
+ log.info("Broadcasting file.watcher.updated: %s %s", event_type, file_path)
240
+ event = FileWatcherUpdatedEvent.create(file=file_path, event=event_type) # type: ignore[arg-type]
241
+ await state.broadcast_event(event)
242
+
243
+ log.info("Setting up project FileWatcher for: %s", state.working_dir)
244
+ project_file_watcher = FileWatcher(
245
+ paths=[state.working_dir],
246
+ callback=on_file_change,
247
+ debounce=500, # 500ms debounce to batch rapid changes
248
+ )
249
+ await project_file_watcher.start()
250
+ log.info("Project FileWatcher started")
251
+
252
+ # --- Version update check (triggered when first client connects) ---
253
+ async def check_for_updates() -> None:
254
+ """Check PyPI for updates and notify via toast."""
255
+ from agentpool import __version__ as current_version
256
+ from agentpool_server.opencode_server.models.events import TuiToastShowEvent
257
+
258
+ latest = await check_pypi_version("agentpool")
259
+ if latest and compare_versions(current_version, latest):
260
+ log.info("Update available: %s -> %s", current_version, latest)
261
+ event = TuiToastShowEvent.create(
262
+ title="Update Available",
263
+ message=f"agentpool {latest} is available (current: {current_version})",
264
+ variant="info",
265
+ duration=10000,
266
+ )
267
+ await state.broadcast_event(event)
268
+
269
+ # Register callback to run when first SSE client connects
270
+ state.on_first_subscriber = check_for_updates
271
+
272
+ # Enter pool context to initialize session store and other components
273
+ async with pool:
274
+ yield
275
+
276
+ # Shutdown - clean up
277
+ pool.todos.on_change = None
278
+ if git_branch_watcher:
279
+ await git_branch_watcher.stop()
280
+ if project_file_watcher:
281
+ await project_file_watcher.stop()
282
+ # Clean up LSP servers
283
+ if state.lsp_manager is not None:
284
+ await state.lsp_manager.stop_all()
285
+
286
+ app = FastAPI(
287
+ title="OpenCode-Compatible API",
288
+ description="AgentPool server with OpenCode API compatibility",
289
+ version=VERSION,
290
+ lifespan=lifespan,
291
+ default_response_class=OpenCodeJSONResponse,
292
+ )
293
+
294
+ # Add CORS middleware (required for OpenCode TUI)
295
+ app.add_middleware(
296
+ CORSMiddleware,
297
+ allow_origins=["*"],
298
+ allow_credentials=True,
299
+ allow_methods=["*"],
300
+ allow_headers=["*"],
301
+ )
302
+
303
+ # Store state on app for access in routes
304
+ app.state.server_state = state
305
+
306
+ @app.exception_handler(RequestValidationError)
307
+ async def validation_exception_handler(
308
+ request: Request, exc: RequestValidationError
309
+ ) -> JSONResponse:
310
+ body = await request.body()
311
+ print(f"Validation error for {request.url}")
312
+ print(f"Body: {body.decode()}")
313
+ print(f"Errors: {exc.errors()}")
314
+ return JSONResponse(
315
+ status_code=422,
316
+ content={"detail": exc.errors(), "body": body.decode()},
317
+ )
318
+
319
+ # Register routers
320
+ app.include_router(global_router)
321
+ app.include_router(app_router)
322
+ app.include_router(config_router)
323
+ app.include_router(session_router)
324
+ app.include_router(message_router)
325
+ app.include_router(file_router)
326
+ app.include_router(agent_router)
327
+ app.include_router(permission_router)
328
+ app.include_router(question_router)
329
+ app.include_router(pty_router)
330
+ app.include_router(tui_router)
331
+ app.include_router(lsp_router)
332
+
333
+ # OpenAPI doc redirect
334
+ @app.get("/doc")
335
+ async def get_doc() -> RedirectResponse:
336
+ """Redirect to OpenAPI docs."""
337
+ return RedirectResponse(url="/docs")
338
+
339
+ # Proxy catch-all for OpenCode's hosted web UI
340
+ # This must be registered LAST so it doesn't catch API routes
341
+ @app.api_route("/{path:path}", methods=["GET", "HEAD", "OPTIONS"])
342
+ async def proxy_web_ui(request: Request, path: str) -> Response:
343
+ """Proxy unmatched GET requests to OpenCode's hosted web UI.
344
+
345
+ This allows users to open http://localhost:4096 in a browser and get
346
+ the full OpenCode web interface, which then makes API calls back to
347
+ this local server for all data operations.
348
+ """
349
+ import httpx
350
+
351
+ # Build target URL
352
+ url = f"https://app.opencode.ai/{path}"
353
+ if request.url.query:
354
+ url += f"?{request.url.query}"
355
+
356
+ async with httpx.AsyncClient(timeout=30.0) as client:
357
+ # Forward the request
358
+ response = await client.request(
359
+ method=request.method,
360
+ url=url,
361
+ headers={"host": "app.opencode.ai"},
362
+ follow_redirects=True,
363
+ )
364
+
365
+ # Filter out hop-by-hop headers that shouldn't be forwarded
366
+ excluded_headers = {
367
+ "content-encoding",
368
+ "content-length",
369
+ "transfer-encoding",
370
+ "connection",
371
+ }
372
+ headers = {
373
+ k: v for k, v in response.headers.items() if k.lower() not in excluded_headers
374
+ }
375
+
376
+ return Response(
377
+ content=response.content,
378
+ status_code=response.status_code,
379
+ headers=headers,
380
+ media_type=response.headers.get("content-type"),
381
+ )
382
+
383
+ logfire.instrument_fastapi(app)
384
+ return app
385
+
386
+
387
+ class OpenCodeServer:
388
+ """OpenCode-compatible server wrapper.
389
+
390
+ Provides a convenient interface for running the server.
391
+ """
392
+
393
+ def __init__(
394
+ self,
395
+ pool: AgentPool[Any],
396
+ *,
397
+ host: str = "127.0.0.1",
398
+ port: int = 4096,
399
+ agent_name: str | None = None,
400
+ working_dir: str | None = None,
401
+ ) -> None:
402
+ """Initialize the server.
403
+
404
+ Args:
405
+ pool: AgentPool for session persistence and agent access.
406
+ host: Host to bind to.
407
+ port: Port to listen on.
408
+ agent_name: Name of the agent to use for handling messages.
409
+ working_dir: Working directory for file operations.
410
+ """
411
+ self.host = host
412
+ self.port = port
413
+ self.pool = pool
414
+ self.agent_name = agent_name
415
+ self.working_dir = working_dir
416
+ self._app: FastAPI | None = None
417
+
418
+ @property
419
+ def app(self) -> FastAPI:
420
+ """Get or create the FastAPI application."""
421
+ if self._app is None:
422
+ self._app = create_app(
423
+ pool=self.pool,
424
+ agent_name=self.agent_name,
425
+ working_dir=self.working_dir,
426
+ )
427
+ return self._app
428
+
429
+ def run(self) -> None:
430
+ """Run the server (blocking)."""
431
+ import uvicorn
432
+
433
+ uvicorn.run(self.app, host=self.host, port=self.port)
434
+
435
+ async def run_async(self) -> None:
436
+ """Run the server asynchronously."""
437
+ import uvicorn
438
+
439
+ config = uvicorn.Config(self.app, host=self.host, port=self.port, ws="websockets-sansio")
440
+ server = uvicorn.Server(config)
441
+ await server.serve()
442
+
443
+
444
+ def run_server(
445
+ pool: AgentPool[Any],
446
+ *,
447
+ host: str = "127.0.0.1",
448
+ port: int = 4096,
449
+ agent_name: str | None = None,
450
+ working_dir: str | None = None,
451
+ ) -> None:
452
+ """Run the OpenCode-compatible server.
453
+
454
+ Args:
455
+ pool: AgentPool for session persistence and agent access.
456
+ host: Host to bind to.
457
+ port: Port to listen on.
458
+ agent_name: Name of the agent to use for handling messages.
459
+ working_dir: Working directory for file operations.
460
+ """
461
+ server = OpenCodeServer(
462
+ pool,
463
+ host=host,
464
+ port=port,
465
+ agent_name=agent_name,
466
+ working_dir=working_dir,
467
+ )
468
+ server.run()
469
+
470
+
471
+ if __name__ == "__main__":
472
+ from agentpool import config_resources
473
+
474
+ pool = AgentPool(config_resources.CLAUDE_CODE_ASSISTANT)
475
+ run_server(pool)