agentpool 2.2.3__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 (250) hide show
  1. acp/__init__.py +0 -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/client/connection.py +38 -29
  7. acp/client/implementations/default_client.py +3 -2
  8. acp/client/implementations/headless_client.py +2 -2
  9. acp/connection.py +2 -2
  10. acp/notifications.py +18 -49
  11. acp/schema/__init__.py +2 -0
  12. acp/schema/agent_responses.py +21 -0
  13. acp/schema/client_requests.py +3 -3
  14. acp/schema/session_state.py +63 -29
  15. acp/task/supervisor.py +2 -2
  16. acp/utils.py +2 -2
  17. agentpool/__init__.py +2 -0
  18. agentpool/agents/acp_agent/acp_agent.py +278 -263
  19. agentpool/agents/acp_agent/acp_converters.py +150 -17
  20. agentpool/agents/acp_agent/client_handler.py +35 -24
  21. agentpool/agents/acp_agent/session_state.py +14 -6
  22. agentpool/agents/agent.py +471 -643
  23. agentpool/agents/agui_agent/agui_agent.py +104 -107
  24. agentpool/agents/agui_agent/helpers.py +3 -4
  25. agentpool/agents/base_agent.py +485 -32
  26. agentpool/agents/claude_code_agent/FORKING.md +191 -0
  27. agentpool/agents/claude_code_agent/__init__.py +13 -1
  28. agentpool/agents/claude_code_agent/claude_code_agent.py +654 -334
  29. agentpool/agents/claude_code_agent/converters.py +4 -141
  30. agentpool/agents/claude_code_agent/models.py +77 -0
  31. agentpool/agents/claude_code_agent/static_info.py +100 -0
  32. agentpool/agents/claude_code_agent/usage.py +242 -0
  33. agentpool/agents/events/__init__.py +22 -0
  34. agentpool/agents/events/builtin_handlers.py +65 -0
  35. agentpool/agents/events/event_emitter.py +3 -0
  36. agentpool/agents/events/events.py +84 -3
  37. agentpool/agents/events/infer_info.py +145 -0
  38. agentpool/agents/events/processors.py +254 -0
  39. agentpool/agents/interactions.py +41 -6
  40. agentpool/agents/modes.py +13 -0
  41. agentpool/agents/slashed_agent.py +5 -4
  42. agentpool/agents/tool_wrapping.py +18 -6
  43. agentpool/common_types.py +35 -21
  44. agentpool/config_resources/acp_assistant.yml +2 -2
  45. agentpool/config_resources/agents.yml +3 -0
  46. agentpool/config_resources/agents_template.yml +1 -0
  47. agentpool/config_resources/claude_code_agent.yml +9 -8
  48. agentpool/config_resources/external_acp_agents.yml +2 -1
  49. agentpool/delegation/base_team.py +4 -30
  50. agentpool/delegation/pool.py +104 -265
  51. agentpool/delegation/team.py +57 -57
  52. agentpool/delegation/teamrun.py +50 -55
  53. agentpool/functional/run.py +10 -4
  54. agentpool/mcp_server/client.py +73 -38
  55. agentpool/mcp_server/conversions.py +54 -13
  56. agentpool/mcp_server/manager.py +9 -23
  57. agentpool/mcp_server/registries/official_registry_client.py +10 -1
  58. agentpool/mcp_server/tool_bridge.py +114 -79
  59. agentpool/messaging/connection_manager.py +11 -10
  60. agentpool/messaging/event_manager.py +5 -5
  61. agentpool/messaging/message_container.py +6 -30
  62. agentpool/messaging/message_history.py +87 -8
  63. agentpool/messaging/messagenode.py +52 -14
  64. agentpool/messaging/messages.py +2 -26
  65. agentpool/messaging/processing.py +10 -22
  66. agentpool/models/__init__.py +1 -1
  67. agentpool/models/acp_agents/base.py +6 -2
  68. agentpool/models/acp_agents/mcp_capable.py +124 -15
  69. agentpool/models/acp_agents/non_mcp.py +0 -23
  70. agentpool/models/agents.py +66 -66
  71. agentpool/models/agui_agents.py +1 -1
  72. agentpool/models/claude_code_agents.py +111 -17
  73. agentpool/models/file_parsing.py +0 -1
  74. agentpool/models/manifest.py +70 -50
  75. agentpool/prompts/conversion_manager.py +1 -1
  76. agentpool/prompts/prompts.py +5 -2
  77. agentpool/resource_providers/__init__.py +2 -0
  78. agentpool/resource_providers/aggregating.py +4 -2
  79. agentpool/resource_providers/base.py +13 -3
  80. agentpool/resource_providers/codemode/code_executor.py +72 -5
  81. agentpool/resource_providers/codemode/helpers.py +2 -2
  82. agentpool/resource_providers/codemode/provider.py +64 -12
  83. agentpool/resource_providers/codemode/remote_mcp_execution.py +2 -2
  84. agentpool/resource_providers/codemode/remote_provider.py +9 -12
  85. agentpool/resource_providers/filtering.py +3 -1
  86. agentpool/resource_providers/mcp_provider.py +66 -12
  87. agentpool/resource_providers/plan_provider.py +111 -18
  88. agentpool/resource_providers/pool.py +5 -3
  89. agentpool/resource_providers/resource_info.py +111 -0
  90. agentpool/resource_providers/static.py +2 -2
  91. agentpool/sessions/__init__.py +2 -0
  92. agentpool/sessions/manager.py +2 -3
  93. agentpool/sessions/models.py +9 -6
  94. agentpool/sessions/protocol.py +28 -0
  95. agentpool/sessions/session.py +11 -55
  96. agentpool/storage/manager.py +361 -54
  97. agentpool/talk/registry.py +4 -4
  98. agentpool/talk/talk.py +9 -10
  99. agentpool/testing.py +1 -1
  100. agentpool/tool_impls/__init__.py +6 -0
  101. agentpool/tool_impls/agent_cli/__init__.py +42 -0
  102. agentpool/tool_impls/agent_cli/tool.py +95 -0
  103. agentpool/tool_impls/bash/__init__.py +64 -0
  104. agentpool/tool_impls/bash/helpers.py +35 -0
  105. agentpool/tool_impls/bash/tool.py +171 -0
  106. agentpool/tool_impls/delete_path/__init__.py +70 -0
  107. agentpool/tool_impls/delete_path/tool.py +142 -0
  108. agentpool/tool_impls/download_file/__init__.py +80 -0
  109. agentpool/tool_impls/download_file/tool.py +183 -0
  110. agentpool/tool_impls/execute_code/__init__.py +55 -0
  111. agentpool/tool_impls/execute_code/tool.py +163 -0
  112. agentpool/tool_impls/grep/__init__.py +80 -0
  113. agentpool/tool_impls/grep/tool.py +200 -0
  114. agentpool/tool_impls/list_directory/__init__.py +73 -0
  115. agentpool/tool_impls/list_directory/tool.py +197 -0
  116. agentpool/tool_impls/question/__init__.py +42 -0
  117. agentpool/tool_impls/question/tool.py +127 -0
  118. agentpool/tool_impls/read/__init__.py +104 -0
  119. agentpool/tool_impls/read/tool.py +305 -0
  120. agentpool/tools/__init__.py +2 -1
  121. agentpool/tools/base.py +114 -34
  122. agentpool/tools/manager.py +57 -1
  123. agentpool/ui/base.py +2 -2
  124. agentpool/ui/mock_provider.py +2 -2
  125. agentpool/ui/stdlib_provider.py +2 -2
  126. agentpool/utils/streams.py +21 -96
  127. agentpool/vfs_registry.py +7 -2
  128. {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/METADATA +16 -22
  129. {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/RECORD +242 -195
  130. {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/WHEEL +1 -1
  131. agentpool_cli/__main__.py +20 -0
  132. agentpool_cli/create.py +1 -1
  133. agentpool_cli/serve_acp.py +59 -1
  134. agentpool_cli/serve_opencode.py +1 -1
  135. agentpool_cli/ui.py +557 -0
  136. agentpool_commands/__init__.py +12 -5
  137. agentpool_commands/agents.py +1 -1
  138. agentpool_commands/pool.py +260 -0
  139. agentpool_commands/session.py +1 -1
  140. agentpool_commands/text_sharing/__init__.py +119 -0
  141. agentpool_commands/text_sharing/base.py +123 -0
  142. agentpool_commands/text_sharing/github_gist.py +80 -0
  143. agentpool_commands/text_sharing/opencode.py +462 -0
  144. agentpool_commands/text_sharing/paste_rs.py +59 -0
  145. agentpool_commands/text_sharing/pastebin.py +116 -0
  146. agentpool_commands/text_sharing/shittycodingagent.py +112 -0
  147. agentpool_commands/utils.py +31 -32
  148. agentpool_config/__init__.py +30 -2
  149. agentpool_config/agentpool_tools.py +498 -0
  150. agentpool_config/converters.py +1 -1
  151. agentpool_config/event_handlers.py +42 -0
  152. agentpool_config/events.py +1 -1
  153. agentpool_config/forward_targets.py +1 -4
  154. agentpool_config/jinja.py +3 -3
  155. agentpool_config/mcp_server.py +1 -5
  156. agentpool_config/nodes.py +1 -1
  157. agentpool_config/observability.py +44 -0
  158. agentpool_config/session.py +0 -3
  159. agentpool_config/storage.py +38 -39
  160. agentpool_config/task.py +3 -3
  161. agentpool_config/tools.py +11 -28
  162. agentpool_config/toolsets.py +22 -90
  163. agentpool_server/a2a_server/agent_worker.py +307 -0
  164. agentpool_server/a2a_server/server.py +23 -18
  165. agentpool_server/acp_server/acp_agent.py +125 -56
  166. agentpool_server/acp_server/commands/acp_commands.py +46 -216
  167. agentpool_server/acp_server/commands/docs_commands/fetch_repo.py +8 -7
  168. agentpool_server/acp_server/event_converter.py +651 -0
  169. agentpool_server/acp_server/input_provider.py +53 -10
  170. agentpool_server/acp_server/server.py +1 -11
  171. agentpool_server/acp_server/session.py +90 -410
  172. agentpool_server/acp_server/session_manager.py +8 -34
  173. agentpool_server/agui_server/server.py +3 -1
  174. agentpool_server/mcp_server/server.py +5 -2
  175. agentpool_server/opencode_server/ENDPOINTS.md +53 -14
  176. agentpool_server/opencode_server/OPENCODE_UI_TOOLS_COMPLETE.md +202 -0
  177. agentpool_server/opencode_server/__init__.py +0 -8
  178. agentpool_server/opencode_server/converters.py +132 -26
  179. agentpool_server/opencode_server/input_provider.py +160 -8
  180. agentpool_server/opencode_server/models/__init__.py +42 -20
  181. agentpool_server/opencode_server/models/app.py +12 -0
  182. agentpool_server/opencode_server/models/events.py +203 -29
  183. agentpool_server/opencode_server/models/mcp.py +19 -0
  184. agentpool_server/opencode_server/models/message.py +18 -1
  185. agentpool_server/opencode_server/models/parts.py +134 -1
  186. agentpool_server/opencode_server/models/question.py +56 -0
  187. agentpool_server/opencode_server/models/session.py +13 -1
  188. agentpool_server/opencode_server/routes/__init__.py +4 -0
  189. agentpool_server/opencode_server/routes/agent_routes.py +33 -2
  190. agentpool_server/opencode_server/routes/app_routes.py +66 -3
  191. agentpool_server/opencode_server/routes/config_routes.py +66 -5
  192. agentpool_server/opencode_server/routes/file_routes.py +184 -5
  193. agentpool_server/opencode_server/routes/global_routes.py +1 -1
  194. agentpool_server/opencode_server/routes/lsp_routes.py +1 -1
  195. agentpool_server/opencode_server/routes/message_routes.py +122 -66
  196. agentpool_server/opencode_server/routes/permission_routes.py +63 -0
  197. agentpool_server/opencode_server/routes/pty_routes.py +23 -22
  198. agentpool_server/opencode_server/routes/question_routes.py +128 -0
  199. agentpool_server/opencode_server/routes/session_routes.py +139 -68
  200. agentpool_server/opencode_server/routes/tui_routes.py +1 -1
  201. agentpool_server/opencode_server/server.py +47 -2
  202. agentpool_server/opencode_server/state.py +30 -0
  203. agentpool_storage/__init__.py +0 -4
  204. agentpool_storage/base.py +81 -2
  205. agentpool_storage/claude_provider/ARCHITECTURE.md +433 -0
  206. agentpool_storage/claude_provider/__init__.py +42 -0
  207. agentpool_storage/{claude_provider.py → claude_provider/provider.py} +190 -8
  208. agentpool_storage/file_provider.py +149 -15
  209. agentpool_storage/memory_provider.py +132 -12
  210. agentpool_storage/opencode_provider/ARCHITECTURE.md +386 -0
  211. agentpool_storage/opencode_provider/__init__.py +16 -0
  212. agentpool_storage/opencode_provider/helpers.py +414 -0
  213. agentpool_storage/opencode_provider/provider.py +895 -0
  214. agentpool_storage/session_store.py +20 -6
  215. agentpool_storage/sql_provider/sql_provider.py +135 -2
  216. agentpool_storage/sql_provider/utils.py +2 -12
  217. agentpool_storage/zed_provider/__init__.py +16 -0
  218. agentpool_storage/zed_provider/helpers.py +281 -0
  219. agentpool_storage/zed_provider/models.py +130 -0
  220. agentpool_storage/zed_provider/provider.py +442 -0
  221. agentpool_storage/zed_provider.py +803 -0
  222. agentpool_toolsets/__init__.py +0 -2
  223. agentpool_toolsets/builtin/__init__.py +2 -4
  224. agentpool_toolsets/builtin/code.py +4 -4
  225. agentpool_toolsets/builtin/debug.py +115 -40
  226. agentpool_toolsets/builtin/execution_environment.py +54 -165
  227. agentpool_toolsets/builtin/skills.py +0 -77
  228. agentpool_toolsets/builtin/subagent_tools.py +64 -51
  229. agentpool_toolsets/builtin/workers.py +4 -2
  230. agentpool_toolsets/composio_toolset.py +2 -2
  231. agentpool_toolsets/entry_points.py +3 -1
  232. agentpool_toolsets/fsspec_toolset/grep.py +25 -5
  233. agentpool_toolsets/fsspec_toolset/helpers.py +3 -2
  234. agentpool_toolsets/fsspec_toolset/toolset.py +350 -66
  235. agentpool_toolsets/mcp_discovery/data/mcp_servers.parquet +0 -0
  236. agentpool_toolsets/mcp_discovery/toolset.py +74 -17
  237. agentpool_toolsets/mcp_run_toolset.py +8 -11
  238. agentpool_toolsets/notifications.py +33 -33
  239. agentpool_toolsets/openapi.py +3 -1
  240. agentpool_toolsets/search_toolset.py +3 -1
  241. agentpool_config/resources.py +0 -33
  242. agentpool_server/acp_server/acp_tools.py +0 -43
  243. agentpool_server/acp_server/commands/spawn.py +0 -210
  244. agentpool_storage/opencode_provider.py +0 -730
  245. agentpool_storage/text_log_provider.py +0 -276
  246. agentpool_toolsets/builtin/chain.py +0 -288
  247. agentpool_toolsets/builtin/user_interaction.py +0 -52
  248. agentpool_toolsets/semantic_memory_toolset.py +0 -536
  249. {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/entry_points.txt +0 -0
  250. {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/licenses/LICENSE +0 -0
agentpool_cli/ui.py ADDED
@@ -0,0 +1,557 @@
1
+ """UI commands for launching interactive interfaces."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import signal
6
+ import socket
7
+ import subprocess
8
+ import time
9
+ from typing import Annotated
10
+
11
+ import typer as t
12
+
13
+ from agentpool_cli import log
14
+
15
+
16
+ logger = log.get_logger(__name__)
17
+
18
+ # Create UI subcommand group
19
+ ui_app = t.Typer(help="Launch interactive user interfaces")
20
+
21
+
22
+ @ui_app.command("opencode")
23
+ def opencode_ui_command( # noqa: PLR0915
24
+ config: Annotated[
25
+ str | None,
26
+ t.Argument(help="Path to agent configuration (optional, not used with --attach)"),
27
+ ] = None,
28
+ host: Annotated[
29
+ str,
30
+ t.Option("--host", "-h", help="Host to bind/connect to"),
31
+ ] = "127.0.0.1",
32
+ port: Annotated[
33
+ int,
34
+ t.Option("--port", "-p", help="Port for server to listen on / connect to"),
35
+ ] = 4096,
36
+ agent: Annotated[
37
+ str | None,
38
+ t.Option(
39
+ "--agent",
40
+ help="Name of specific agent to use (not used with --attach)",
41
+ ),
42
+ ] = None,
43
+ attach: Annotated[
44
+ bool,
45
+ t.Option("--attach", help="Only attach TUI to existing server (don't start server)"),
46
+ ] = False,
47
+ ) -> None:
48
+ """Launch OpenCode TUI with integrated server or attach to existing one.
49
+
50
+ By default, starts an OpenCode-compatible server in the background and
51
+ automatically attaches the OpenCode TUI to it. When you exit the TUI,
52
+ the server is automatically shut down.
53
+
54
+ With --attach, only launches the TUI and connects to an existing server
55
+ (useful when running the server separately or connecting from multiple clients).
56
+
57
+ Examples:
58
+ # Start server + TUI
59
+ agentpool ui opencode
60
+
61
+ # Use specific config and agent
62
+ agentpool ui opencode agents.yml --agent myagent
63
+
64
+ # Custom port
65
+ agentpool ui opencode --port 8080
66
+
67
+ # Attach to existing server (no server startup)
68
+ agentpool ui opencode --attach
69
+ agentpool ui opencode --attach --port 8080
70
+ """
71
+ url = f"http://{host}:{port}"
72
+
73
+ # Attach-only mode: just launch TUI
74
+ if attach:
75
+ logger.info("Attaching to existing OpenCode server", url=url)
76
+
77
+ # Clear screen for clean TUI
78
+ import os
79
+
80
+ os.system("clear" if os.name != "nt" else "cls")
81
+
82
+ result = subprocess.run(["opencode", "attach", url], check=False)
83
+ if result.returncode not in {0, 130}: # 130 = Ctrl+C
84
+ logger.warning("OpenCode TUI exited with non-zero status", code=result.returncode)
85
+ return
86
+
87
+ # Build server command
88
+ server_cmd = [
89
+ "agentpool",
90
+ "serve-opencode",
91
+ "--host",
92
+ host,
93
+ "--port",
94
+ str(port),
95
+ ]
96
+ if config:
97
+ server_cmd.append(config)
98
+ if agent:
99
+ server_cmd.extend(["--agent", agent])
100
+
101
+ logger.info("Starting OpenCode server", url=url)
102
+
103
+ # Start server in background with suppressed output
104
+ server = subprocess.Popen(
105
+ server_cmd,
106
+ stdout=subprocess.DEVNULL,
107
+ stderr=subprocess.DEVNULL,
108
+ )
109
+
110
+ try:
111
+ # Wait for server to be ready with retry
112
+ max_retries = 30
113
+ for i in range(max_retries):
114
+ try:
115
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
116
+ sock.settimeout(0.5)
117
+ sock.connect((host, port))
118
+ sock.close()
119
+ logger.info("Server is ready", url=url)
120
+ break
121
+ except (TimeoutError, ConnectionRefusedError, OSError):
122
+ if i == max_retries - 1:
123
+ msg = f"Server failed to start after {max_retries} attempts"
124
+ raise RuntimeError(msg) # noqa: B904
125
+ time.sleep(0.5)
126
+
127
+ # Give HTTP layer a moment to be fully ready
128
+ time.sleep(0.5)
129
+
130
+ # Clear screen before launching TUI
131
+ import os
132
+
133
+ os.system("clear" if os.name != "nt" else "cls")
134
+
135
+ # Attach TUI
136
+ result = subprocess.run(["opencode", "attach", url], check=False)
137
+ if result.returncode != 0:
138
+ logger.warning("OpenCode TUI exited with non-zero status", code=result.returncode)
139
+
140
+ except KeyboardInterrupt:
141
+ logger.info("UI interrupted by user")
142
+ except Exception as e:
143
+ logger.exception("Error running OpenCode UI")
144
+ raise t.Exit(1) from e
145
+ finally:
146
+ # Clean up server
147
+ logger.info("Shutting down server")
148
+ server.send_signal(signal.SIGTERM)
149
+ try:
150
+ server.wait(timeout=5)
151
+ except subprocess.TimeoutExpired:
152
+ logger.warning("Server did not shut down gracefully, killing")
153
+ server.kill()
154
+
155
+
156
+ @ui_app.command("toad")
157
+ def toad_ui_command(
158
+ config: Annotated[
159
+ str | None,
160
+ t.Argument(help="Path to agent configuration (optional)"),
161
+ ] = None,
162
+ websocket: Annotated[
163
+ bool,
164
+ t.Option("--websocket", "-w", help="Use WebSocket transport (otherwise stdio)"),
165
+ ] = False,
166
+ port: Annotated[
167
+ int,
168
+ t.Option("--port", "-p", help="Port for WebSocket server (only with --websocket)"),
169
+ ] = 8765,
170
+ ) -> None:
171
+ """Launch Toad TUI for ACP agents.
172
+
173
+ By default uses stdio transport where Toad spawns the agentpool server.
174
+ With --websocket, starts a WebSocket ACP server in the background first.
175
+
176
+ Examples:
177
+ # Direct stdio (Toad spawns server)
178
+ agentpool ui toad
179
+
180
+ # Use specific config
181
+ agentpool ui toad agents.yml
182
+
183
+ # WebSocket transport
184
+ agentpool ui toad --websocket
185
+
186
+ # WebSocket with custom port
187
+ agentpool ui toad --websocket --port 9000
188
+ """
189
+ if websocket:
190
+ _run_toad_websocket(config, port)
191
+ else:
192
+ _run_toad_stdio(config)
193
+
194
+
195
+ def _run_toad_stdio(config: str | None) -> None:
196
+ """Run Toad with stdio transport (Toad spawns server)."""
197
+ # Build agentpool command that Toad will spawn
198
+ agentpool_cmd = "agentpool serve-acp"
199
+ if config:
200
+ agentpool_cmd += f" {config}"
201
+
202
+ # Clear screen for clean TUI
203
+ import os
204
+
205
+ os.system("clear" if os.name != "nt" else "cls")
206
+
207
+ # Run toad with agentpool as subprocess
208
+ result = subprocess.run(
209
+ ["uvx", "--from", "batrachian-toad@latest", "toad", "acp", agentpool_cmd],
210
+ check=False,
211
+ )
212
+
213
+ if result.returncode not in {0, 130}: # 130 = Ctrl+C
214
+ logger.warning("Toad TUI exited with non-zero status", code=result.returncode)
215
+
216
+
217
+ def _run_toad_websocket(config: str | None, port: int) -> None:
218
+ """Run Toad with WebSocket transport."""
219
+ url = f"ws://localhost:{port}"
220
+
221
+ # Build server command
222
+ server_cmd = [
223
+ "agentpool",
224
+ "serve-acp",
225
+ "--transport",
226
+ "websocket",
227
+ "--ws-port",
228
+ str(port),
229
+ ]
230
+ if config:
231
+ server_cmd.append(config)
232
+
233
+ logger.info("Starting ACP WebSocket server", url=url)
234
+
235
+ # Start server in background
236
+ server = subprocess.Popen(
237
+ server_cmd,
238
+ stdout=subprocess.DEVNULL,
239
+ stderr=subprocess.DEVNULL,
240
+ )
241
+
242
+ try:
243
+ # Wait for server startup
244
+ time.sleep(1.5)
245
+
246
+ # Clear screen for clean TUI
247
+ import os
248
+
249
+ os.system("clear" if os.name != "nt" else "cls")
250
+
251
+ # Run toad with mcp-ws client
252
+ result = subprocess.run(
253
+ ["uvx", "--from", "batrachian-toad@latest", "toad", "acp", f"uvx mcp-ws {url}"],
254
+ check=False,
255
+ )
256
+
257
+ if result.returncode not in {0, 130}: # 130 = Ctrl+C
258
+ logger.warning("Toad TUI exited with non-zero status", code=result.returncode)
259
+
260
+ except KeyboardInterrupt:
261
+ logger.info("UI interrupted by user")
262
+ except Exception as e:
263
+ logger.exception("Error running Toad UI")
264
+ raise t.Exit(1) from e
265
+ finally:
266
+ # Clean up server
267
+ logger.info("Shutting down server")
268
+ server.send_signal(signal.SIGTERM)
269
+ try:
270
+ server.wait(timeout=5)
271
+ except subprocess.TimeoutExpired:
272
+ logger.warning("Server did not shut down gracefully, killing")
273
+ server.kill()
274
+
275
+
276
+ @ui_app.command("desktop")
277
+ def opencode_desktop_command( # noqa: PLR0915
278
+ config: Annotated[
279
+ str | None,
280
+ t.Argument(help="Path to agent configuration (optional, not used with --attach)"),
281
+ ] = None,
282
+ host: Annotated[
283
+ str,
284
+ t.Option("--host", "-h", help="Host to bind/connect to"),
285
+ ] = "127.0.0.1",
286
+ port: Annotated[
287
+ int,
288
+ t.Option("--port", "-p", help="Port for server to listen on / connect to"),
289
+ ] = 4096,
290
+ agent: Annotated[
291
+ str | None,
292
+ t.Option(
293
+ "--agent",
294
+ help="Name of specific agent to use (not used with --attach)",
295
+ ),
296
+ ] = None,
297
+ attach: Annotated[
298
+ bool,
299
+ t.Option("--attach", help="Connect desktop app to existing server (don't start server)"),
300
+ ] = False,
301
+ ) -> None:
302
+ """Launch OpenCode desktop app with integrated server or attach to existing one.
303
+
304
+ By default, starts an OpenCode-compatible server in the background and
305
+ configures the desktop app to connect to it. The desktop app will run
306
+ independently and you can close the terminal.
307
+
308
+ With --attach, configures the desktop app to connect to an existing server
309
+ without starting a new one.
310
+
311
+ Note: This command requires the OpenCode desktop app to be installed.
312
+ The app will be configured to use the specified server URL via its config.
313
+
314
+ Examples:
315
+ # Start server, configure desktop app, and launch it
316
+ agentpool ui desktop
317
+
318
+ # Use specific config and agent
319
+ agentpool ui desktop agents.yml --agent myagent
320
+
321
+ # Custom port
322
+ agentpool ui desktop --port 8080
323
+
324
+ # Configure desktop to attach to existing server
325
+ agentpool ui desktop --attach
326
+ agentpool ui desktop --attach --port 8080
327
+
328
+ # After using --attach, reset to default (spawn local server)
329
+ agentpool ui desktop --attach --port 0 # port 0 clears the setting
330
+ """
331
+ import json
332
+ from pathlib import Path
333
+
334
+ url = f"http://{host}:{port}"
335
+
336
+ # Determine config path based on platform
337
+ config_dir = Path.home() / ".config" / "opencode"
338
+ config_file = config_dir / "config.json"
339
+
340
+ # Handle attach mode - configure desktop app to use external server
341
+ if attach:
342
+ if port == 0:
343
+ # Special case: port 0 means clear the setting
344
+ logger.info("Clearing desktop app server configuration")
345
+ if config_file.exists():
346
+ try:
347
+ with config_file.open() as f:
348
+ existing_config = json.load(f)
349
+ # Remove server config
350
+ if "server" in existing_config:
351
+ del existing_config["server"]
352
+ with config_file.open("w") as f:
353
+ json.dump(existing_config, f, indent=2)
354
+ logger.info("Cleared server configuration from config file")
355
+ except Exception as e: # noqa: BLE001
356
+ logger.warning("Failed to clear config", error=str(e))
357
+ else:
358
+ # Configure desktop app to use specified server
359
+ logger.info("Configuring desktop app to attach to server", url=url)
360
+
361
+ config_dir.mkdir(parents=True, exist_ok=True)
362
+
363
+ # Read existing config or create new
364
+ existing_config = {}
365
+ if config_file.exists():
366
+ try:
367
+ with config_file.open() as f:
368
+ existing_config = json.load(f)
369
+ except Exception as e: # noqa: BLE001
370
+ logger.warning("Failed to read existing config", error=str(e))
371
+
372
+ # Update server configuration
373
+ existing_config["server"] = {
374
+ "hostname": host,
375
+ "port": port,
376
+ }
377
+
378
+ try:
379
+ with config_file.open("w") as f:
380
+ json.dump(existing_config, f, indent=2)
381
+ logger.info("Updated desktop app configuration", config=str(config_file))
382
+ except Exception as e:
383
+ logger.exception("Failed to write config", error=str(e))
384
+ raise t.Exit(1) from e
385
+
386
+ # Launch desktop app
387
+ logger.info("Launching OpenCode desktop app")
388
+ try:
389
+ # Try common desktop app launch commands
390
+ # On macOS: open -a OpenCode
391
+ # On Linux: OpenCode (capital O) is the desktop app
392
+ # On Windows: start opencode
393
+ import platform
394
+
395
+ system = platform.system()
396
+ if system == "Darwin":
397
+ subprocess.Popen(["open", "-a", "OpenCode"])
398
+ elif system == "Windows":
399
+ subprocess.Popen(["start", "opencode"], shell=True)
400
+ else: # Linux and others
401
+ # Try different possible command names - OpenCode (capital O) is the desktop app
402
+ for cmd in ["OpenCode", "opencode-desktop"]:
403
+ try:
404
+ subprocess.Popen([cmd])
405
+ break
406
+ except FileNotFoundError:
407
+ continue
408
+ else:
409
+ msg = (
410
+ "Could not find OpenCode desktop app. Please install it or launch manually."
411
+ )
412
+ raise FileNotFoundError(msg) # noqa: TRY301
413
+
414
+ if port != 0:
415
+ logger.info(
416
+ "Desktop app launched and configured to use server",
417
+ url=url,
418
+ note="The app will connect to the server. You can close this terminal.",
419
+ )
420
+ else:
421
+ logger.info(
422
+ "Desktop app launched with default configuration",
423
+ note="The app will spawn its own local server. You can close this terminal.",
424
+ )
425
+
426
+ except Exception as e:
427
+ logger.exception("Failed to launch desktop app", error=str(e))
428
+ logger.info(
429
+ "Configuration has been updated. Please launch the OpenCode desktop app manually.",
430
+ config=str(config_file),
431
+ )
432
+ raise t.Exit(1) from e
433
+
434
+ return
435
+
436
+ # Default mode: Start server + launch desktop app
437
+ # Build server command
438
+ server_cmd = [
439
+ "agentpool",
440
+ "serve-opencode",
441
+ "--host",
442
+ host,
443
+ "--port",
444
+ str(port),
445
+ ]
446
+ if config:
447
+ server_cmd.append(config)
448
+ if agent:
449
+ server_cmd.extend(["--agent", agent])
450
+
451
+ logger.info("Starting OpenCode server for desktop app", url=url)
452
+
453
+ # Start server in background
454
+ server = subprocess.Popen(
455
+ server_cmd,
456
+ stdout=subprocess.DEVNULL,
457
+ stderr=subprocess.DEVNULL,
458
+ )
459
+
460
+ try:
461
+ # Wait for server to be ready
462
+ max_retries = 30
463
+ for i in range(max_retries):
464
+ try:
465
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
466
+ sock.settimeout(0.5)
467
+ sock.connect((host, port))
468
+ sock.close()
469
+ logger.info("Server is ready", url=url)
470
+ break
471
+ except (TimeoutError, ConnectionRefusedError, OSError):
472
+ if i == max_retries - 1:
473
+ msg = f"Server failed to start after {max_retries} attempts"
474
+ raise RuntimeError(msg) # noqa: B904
475
+ time.sleep(0.5)
476
+
477
+ # Give HTTP layer a moment to be fully ready
478
+ time.sleep(0.5)
479
+
480
+ # Configure desktop app to use this server
481
+ config_dir.mkdir(parents=True, exist_ok=True)
482
+
483
+ existing_config = {}
484
+ if config_file.exists():
485
+ try:
486
+ with config_file.open() as f:
487
+ existing_config = json.load(f)
488
+ except Exception as e: # noqa: BLE001
489
+ logger.warning("Failed to read existing config", error=str(e))
490
+
491
+ existing_config["server"] = {
492
+ "hostname": host,
493
+ "port": port,
494
+ }
495
+
496
+ try:
497
+ with config_file.open("w") as f:
498
+ json.dump(existing_config, f, indent=2)
499
+ logger.info("Configured desktop app", config=str(config_file))
500
+ except Exception as e: # noqa: BLE001
501
+ logger.warning("Failed to write config", error=str(e))
502
+
503
+ # Launch desktop app
504
+ logger.info("Launching OpenCode desktop app")
505
+ import platform
506
+
507
+ system = platform.system()
508
+ try:
509
+ if system == "Darwin":
510
+ subprocess.Popen(["open", "-a", "OpenCode"])
511
+ elif system == "Windows":
512
+ subprocess.Popen(["start", "opencode"], shell=True)
513
+ else: # Linux
514
+ # OpenCode (capital O) is the desktop app
515
+ for cmd in ["OpenCode", "opencode-desktop"]:
516
+ try:
517
+ subprocess.Popen([cmd])
518
+ break
519
+ except FileNotFoundError:
520
+ continue
521
+ else:
522
+ msg = "Could not find OpenCode desktop app"
523
+ raise FileNotFoundError(msg) # noqa: TRY301
524
+
525
+ logger.info(
526
+ "Desktop app launched",
527
+ note="Server is running in background. Press Ctrl+C to stop the server.",
528
+ )
529
+
530
+ # Keep server running until interrupted
531
+ logger.info("Server running. Press Ctrl+C to stop.")
532
+ server.wait()
533
+
534
+ except FileNotFoundError as e:
535
+ logger.exception(
536
+ "Desktop app not found. Please install OpenCode desktop app or launch it manually.",
537
+ config=str(config_file),
538
+ )
539
+ raise t.Exit(1) from e
540
+
541
+ except KeyboardInterrupt:
542
+ logger.info("Shutting down server")
543
+ except Exception as e:
544
+ logger.exception("Error running desktop app")
545
+ raise t.Exit(1) from e
546
+ finally:
547
+ # Clean up server
548
+ server.send_signal(signal.SIGTERM)
549
+ try:
550
+ server.wait(timeout=5)
551
+ except subprocess.TimeoutExpired:
552
+ logger.warning("Server did not shut down gracefully, killing")
553
+ server.kill()
554
+
555
+
556
+ if __name__ == "__main__":
557
+ t.run(ui_app)
@@ -39,17 +39,18 @@ from agentpool_commands.tools import (
39
39
  RegisterToolCommand,
40
40
  ShowToolCommand,
41
41
  )
42
- from agentpool_commands.workers import (
43
- AddWorkerCommand,
44
- RemoveWorkerCommand,
45
- ListWorkersCommand,
46
- )
42
+ from agentpool_commands.workers import AddWorkerCommand, RemoveWorkerCommand, ListWorkersCommand
47
43
  from agentpool_commands.utils import (
48
44
  CopyClipboardCommand,
49
45
  EditAgentFileCommand,
50
46
  GetLogsCommand,
51
47
  ShareHistoryCommand,
52
48
  )
49
+ from agentpool_commands.pool import ListPoolsCommand, SpawnCommand
50
+
51
+ # CompactCommand is only for Native Agent (has its own history)
52
+ # Other agents (ClaudeCode, ACP, AGUI) don't control their own history
53
+ from agentpool_commands.pool import CompactCommand # noqa: F401
53
54
  from typing import TYPE_CHECKING, Any
54
55
 
55
56
  if TYPE_CHECKING:
@@ -104,6 +105,8 @@ def get_pool_commands(**kwargs: Any) -> Sequence[BaseCommand | type[SlashedComma
104
105
  "enable_list_agents": ListAgentsCommand,
105
106
  "enable_show_agent": ShowAgentCommand,
106
107
  "enable_edit_agent_file": EditAgentFileCommand,
108
+ "enable_list_pools": ListPoolsCommand,
109
+ "enable_spawn": SpawnCommand,
107
110
  }
108
111
  return [command for flag, command in command_map.items() if kwargs.get(flag, True)]
109
112
 
@@ -163,6 +166,8 @@ def get_commands(
163
166
  enable_list_agents: bool = True,
164
167
  enable_show_agent: bool = True,
165
168
  enable_edit_agent_file: bool = True,
169
+ enable_list_pools: bool = True,
170
+ enable_spawn: bool = True,
166
171
  ) -> list[BaseCommand | type[SlashedCommand]]:
167
172
  """Get all built-in commands."""
168
173
  agent_kwargs = {
@@ -202,6 +207,8 @@ def get_commands(
202
207
  "enable_list_agents": enable_list_agents,
203
208
  "enable_show_agent": enable_show_agent,
204
209
  "enable_edit_agent_file": enable_edit_agent_file,
210
+ "enable_list_pools": enable_list_pools,
211
+ "enable_spawn": enable_spawn,
205
212
  }
206
213
 
207
214
  return [
@@ -77,7 +77,7 @@ class CreateAgentCommand(NodeCommand):
77
77
  # Create and register the new agent
78
78
  await ctx.context.pool.add_agent(
79
79
  name=agent_name,
80
- model=model or current_agent.model_name,
80
+ model=model or current_agent.model_name or "openai:gpt-4o-mini",
81
81
  system_prompt=system_prompt or (),
82
82
  description=description,
83
83
  tools=tool_list,