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,63 @@
1
+ """Permission routes for OpenCode TUI compatibility."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from fastapi import APIRouter, HTTPException
6
+
7
+ from agentpool_server.opencode_server.dependencies import StateDep
8
+ from agentpool_server.opencode_server.models.events import PermissionResolvedEvent
9
+ from agentpool_server.opencode_server.routes.session_routes import PermissionResponse
10
+
11
+
12
+ router = APIRouter(prefix="/permission", tags=["permission"])
13
+
14
+
15
+ @router.post("/{permission_id}/reply")
16
+ async def reply_to_permission(
17
+ permission_id: str,
18
+ body: PermissionResponse,
19
+ state: StateDep,
20
+ ) -> bool:
21
+ """Respond to a pending permission request (OpenCode TUI compatibility).
22
+
23
+ This endpoint handles the OpenCode TUI's expected format:
24
+ POST /permission/{permission_id}/reply
25
+
26
+ The response can be:
27
+ - "once": Allow this tool execution once
28
+ - "always": Always allow this tool (remembered for session)
29
+ - "reject": Reject this tool execution
30
+ """
31
+ print(f"DEBUG permission endpoint: received reply '{body.reply}' for perm_id={permission_id}")
32
+ print(f"DEBUG permission endpoint: searching in {len(state.input_providers)} sessions")
33
+ # Find which session has this permission request
34
+ for session_id, input_provider in state.input_providers.items():
35
+ pending_perms = list(input_provider._pending_permissions.keys())
36
+ print(
37
+ f"DEBUG permission endpoint: session {session_id} has "
38
+ f"{len(pending_perms)} pending: {pending_perms}"
39
+ )
40
+ # Check if this permission belongs to this session
41
+ if permission_id in input_provider._pending_permissions:
42
+ print(f"DEBUG permission endpoint: found permission in session {session_id}")
43
+ # Resolve the permission
44
+ resolved = input_provider.resolve_permission(permission_id, body.reply)
45
+ print(f"DEBUG permission endpoint: resolve_permission returned {resolved}")
46
+ if not resolved:
47
+ raise HTTPException(
48
+ status_code=404,
49
+ detail="Permission not found or already resolved",
50
+ )
51
+
52
+ await state.broadcast_event(
53
+ PermissionResolvedEvent.create(
54
+ session_id=session_id,
55
+ request_id=permission_id,
56
+ reply=body.reply,
57
+ )
58
+ )
59
+
60
+ return True
61
+
62
+ # Permission not found in any session
63
+ raise HTTPException(status_code=404, detail="Permission not found")
@@ -0,0 +1,300 @@
1
+ """PTY (Pseudo-Terminal) routes.
2
+
3
+ Uses the agent's execution environment PTY manager for terminal sessions.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import asyncio
9
+ import contextlib
10
+ from dataclasses import dataclass, field
11
+ from typing import TYPE_CHECKING, Any
12
+
13
+ from fastapi import APIRouter, HTTPException, WebSocket, WebSocketDisconnect # noqa: TC002
14
+
15
+ from agentpool_server.opencode_server.dependencies import StateDep
16
+ from agentpool_server.opencode_server.models import PtyCreateRequest, PtyInfo, PtyUpdateRequest
17
+
18
+
19
+ if TYPE_CHECKING:
20
+ from exxec.pty_manager import PtyManagerProtocol
21
+
22
+ from agentpool_server.opencode_server.state import ServerState
23
+
24
+
25
+ router = APIRouter(prefix="/pty", tags=["pty"])
26
+
27
+
28
+ @dataclass
29
+ class PtySession:
30
+ """Active PTY session with WebSocket subscribers."""
31
+
32
+ pty_id: str
33
+ subscribers: set[WebSocket] = field(default_factory=set)
34
+ read_task: asyncio.Task[Any] | None = None
35
+ buffer: str = ""
36
+
37
+
38
+ # Track WebSocket subscribers per PTY session
39
+ _pty_sessions: dict[str, PtySession] = {}
40
+
41
+
42
+ def _get_pty_manager(state: StateDep) -> PtyManagerProtocol:
43
+ """Get PTY manager from agent's execution environment.
44
+
45
+ Args:
46
+ state: Server state with agent
47
+
48
+ Returns:
49
+ PTY manager from the agent's execution environment
50
+
51
+ Raises:
52
+ HTTPException: If PTY is not supported
53
+ """
54
+ try:
55
+ return state.agent.env.get_pty_manager()
56
+ except NotImplementedError as e:
57
+ raise HTTPException(
58
+ status_code=501, detail="PTY not supported by this execution environment"
59
+ ) from e
60
+
61
+
62
+ def _convert_pty_info(info: Any, title: str | None = None) -> PtyInfo:
63
+ """Convert exxec PtyInfo to OpenCode PtyInfo model.
64
+
65
+ Args:
66
+ info: PtyInfo from exxec
67
+ title: Optional title override
68
+
69
+ Returns:
70
+ OpenCode PtyInfo model
71
+ """
72
+ return PtyInfo(
73
+ id=info.id,
74
+ title=title or f"Terminal {info.id[-4:]}",
75
+ command=info.command,
76
+ args=info.args,
77
+ cwd=info.cwd or "",
78
+ status=info.status,
79
+ pid=info.pid,
80
+ )
81
+
82
+
83
+ @router.get("")
84
+ async def list_ptys(state: StateDep) -> list[PtyInfo]:
85
+ """List all PTY sessions."""
86
+ manager = _get_pty_manager(state)
87
+ sessions = await manager.list_sessions()
88
+ return [_convert_pty_info(s) for s in sessions]
89
+
90
+
91
+ @router.post("")
92
+ async def create_pty(request: PtyCreateRequest, state: StateDep) -> PtyInfo:
93
+ """Create a new PTY session."""
94
+ from agentpool_server.opencode_server.models.events import PtyCreatedEvent
95
+
96
+ manager = _get_pty_manager(state)
97
+ # Limit number of PTY sessions to prevent resource exhaustion
98
+ sessions = await manager.list_sessions()
99
+ if len(sessions) >= 20: # Max 20 concurrent PTY sessions # noqa: PLR2004
100
+ detail = f"Too many PTY sessions ({len(sessions)}). Close some terminals first."
101
+ raise HTTPException(status_code=429, detail=detail)
102
+
103
+ # Use working dir from state if not specified
104
+ cwd = request.cwd or state.working_dir
105
+ print(f"Creating PTY: command={request.command}, args={request.args}, cwd={cwd}")
106
+ try:
107
+ info = await manager.create(
108
+ command=request.command,
109
+ args=request.args,
110
+ cwd=cwd,
111
+ env=request.env,
112
+ )
113
+ print(f"PTY created successfully: {info.id}, status={info.status}")
114
+ except Exception as e:
115
+ raise HTTPException(status_code=400, detail=f"Failed to create PTY: {e}") from e
116
+
117
+ pty_id = info.id
118
+ title = request.title or f"Terminal {pty_id[-4:]}"
119
+ # Create session tracker for WebSocket subscribers
120
+ session = PtySession(pty_id=pty_id)
121
+ _pty_sessions[pty_id] = session
122
+ print(f"PTY session registered: {pty_id}, total sessions: {len(_pty_sessions)}")
123
+ # Start background task to read output and distribute to subscribers
124
+ session.read_task = asyncio.create_task(_read_pty_output(manager, pty_id, state))
125
+ pty_info = _convert_pty_info(info, title=title)
126
+ # Broadcast PTY created event
127
+ event = PtyCreatedEvent.create(info=pty_info.model_dump(by_alias=True))
128
+ await state.broadcast_event(event)
129
+ return pty_info
130
+
131
+
132
+ async def _read_pty_output(manager: PtyManagerProtocol, pty_id: str, state: ServerState) -> None:
133
+ """Background task to read PTY output and distribute to subscribers."""
134
+ from agentpool_server.opencode_server.models.events import PtyExitedEvent
135
+
136
+ session = _pty_sessions.get(pty_id)
137
+ if not session:
138
+ return
139
+
140
+ exit_code = 0
141
+ try:
142
+ async for data in manager.stream(pty_id):
143
+ decoded = data.decode("utf-8", errors="replace")
144
+
145
+ if session.subscribers:
146
+ # Send to all connected WebSocket clients
147
+ disconnected: set[WebSocket] = set()
148
+ for ws in session.subscribers:
149
+ try:
150
+ await ws.send_text(decoded)
151
+ except Exception: # noqa: BLE001
152
+ disconnected.add(ws)
153
+ session.subscribers -= disconnected
154
+ else:
155
+ # Buffer output if no subscribers
156
+ session.buffer += decoded
157
+ # Limit buffer size
158
+ if len(session.buffer) > 100000: # noqa: PLR2004
159
+ session.buffer = session.buffer[-50000:]
160
+
161
+ except asyncio.CancelledError:
162
+ return # Don't broadcast exit if cancelled
163
+ except Exception: # noqa: BLE001
164
+ exit_code = -1
165
+
166
+ # Stream ended - process exited, broadcast event
167
+ event = PtyExitedEvent.create(pty_id=pty_id, exit_code=exit_code)
168
+ await state.broadcast_event(event)
169
+
170
+
171
+ @router.get("/{pty_id}")
172
+ async def get_pty(pty_id: str, state: StateDep) -> PtyInfo:
173
+ """Get PTY session details."""
174
+ manager = _get_pty_manager(state)
175
+ info = await manager.get_info(pty_id)
176
+ if not info:
177
+ raise HTTPException(status_code=404, detail="PTY session not found")
178
+ return _convert_pty_info(info)
179
+
180
+
181
+ @router.put("/{pty_id}")
182
+ @router.patch("/{pty_id}")
183
+ async def update_pty(pty_id: str, request: PtyUpdateRequest, state: StateDep) -> PtyInfo:
184
+ """Update PTY session (title, resize)."""
185
+ from exxec.pty_manager import PtySize
186
+
187
+ from agentpool_server.opencode_server.models.events import PtyUpdatedEvent
188
+
189
+ manager = _get_pty_manager(state)
190
+ info = await manager.get_info(pty_id)
191
+ if not info:
192
+ raise HTTPException(status_code=404, detail="PTY session not found")
193
+
194
+ # Handle resize if requested
195
+ if request.size:
196
+ await manager.resize(pty_id, PtySize(rows=request.size.rows, cols=request.size.cols))
197
+ # Refresh info after resize
198
+ info = await manager.get_info(pty_id)
199
+ if not info:
200
+ raise HTTPException(status_code=404, detail="PTY session not found after resize")
201
+
202
+ # Title is handled at the API level, not in the PTY manager
203
+ title = request.title if request.title else f"Terminal {pty_id[-4:]}"
204
+ pty_info = _convert_pty_info(info, title=title)
205
+ # Broadcast PTY updated event
206
+ event = PtyUpdatedEvent.create(info=pty_info.model_dump(by_alias=True))
207
+ await state.broadcast_event(event)
208
+ return pty_info
209
+
210
+
211
+ @router.delete("/{pty_id}")
212
+ async def remove_pty(pty_id: str, state: StateDep) -> dict[str, bool]:
213
+ """Remove/kill PTY session."""
214
+ from agentpool_server.opencode_server.models.events import PtyDeletedEvent
215
+
216
+ manager = _get_pty_manager(state)
217
+ # Kill the PTY session
218
+ success = await manager.kill(pty_id)
219
+ if not success:
220
+ raise HTTPException(status_code=404, detail="PTY session not found")
221
+ # Cleanup session tracker
222
+ session = _pty_sessions.pop(pty_id, None)
223
+ if session:
224
+ # Cancel read task
225
+ if session.read_task and not session.read_task.done():
226
+ session.read_task.cancel()
227
+ with contextlib.suppress(asyncio.CancelledError):
228
+ await session.read_task
229
+
230
+ # Close all WebSocket connections
231
+ for ws in session.subscribers:
232
+ with contextlib.suppress(Exception):
233
+ await ws.close()
234
+
235
+ # Broadcast PTY deleted event
236
+ event = PtyDeletedEvent.create(pty_id=pty_id)
237
+ await state.broadcast_event(event)
238
+
239
+ return {"success": True}
240
+
241
+
242
+ @router.websocket("/{pty_id}/connect")
243
+ async def connect_pty(websocket: WebSocket, pty_id: str) -> None:
244
+ """Connect to PTY via WebSocket for interactive terminal."""
245
+ # Get state from websocket's app
246
+
247
+ state: ServerState = websocket.app.state.server_state
248
+ try:
249
+ manager = _get_pty_manager(state)
250
+ except HTTPException:
251
+ # Must accept before we can close
252
+ await websocket.accept()
253
+ await websocket.close(code=1003, reason="PTY not supported")
254
+ return
255
+ except Exception as e: # noqa: BLE001
256
+ await websocket.accept()
257
+ await websocket.close(code=1011, reason=f"Error: {e}")
258
+ return
259
+
260
+ # Check if PTY exists - if not, immediately reject like OpenCode does
261
+ info = await manager.get_info(pty_id)
262
+ if not info:
263
+ await websocket.accept()
264
+ await websocket.close(code=1003, reason="PTY session not found")
265
+ return
266
+
267
+ # PTY exists, accept the WebSocket connection
268
+ await websocket.accept()
269
+ # Get or create session tracker
270
+ if pty_id not in _pty_sessions:
271
+ _pty_sessions[pty_id] = PtySession(pty_id=pty_id)
272
+ session = _pty_sessions[pty_id]
273
+ session.subscribers.add(websocket)
274
+ # Send buffered output
275
+ if session.buffer:
276
+ try:
277
+ await websocket.send_text(session.buffer)
278
+ session.buffer = ""
279
+ except Exception: # noqa: BLE001
280
+ pass
281
+
282
+ try:
283
+ while True:
284
+ # Receive input from client
285
+ data = await websocket.receive_text()
286
+ # Write to PTY stdin
287
+ info = await manager.get_info(pty_id)
288
+ if info and info.status == "running":
289
+ try:
290
+ await manager.write(pty_id, data.encode())
291
+ except Exception: # noqa: BLE001
292
+ break
293
+ else:
294
+ break
295
+ except WebSocketDisconnect:
296
+ pass
297
+ except Exception: # noqa: BLE001
298
+ pass
299
+ finally:
300
+ session.subscribers.discard(websocket)
@@ -0,0 +1,128 @@
1
+ """Question routes for OpenCode compatibility."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from fastapi import APIRouter, HTTPException
6
+
7
+ from agentpool_server.opencode_server.dependencies import StateDep
8
+ from agentpool_server.opencode_server.input_provider import OpenCodeInputProvider
9
+ from agentpool_server.opencode_server.models.events import (
10
+ QuestionRejectedEvent,
11
+ QuestionRepliedEvent,
12
+ )
13
+ from agentpool_server.opencode_server.models.question import QuestionReply, QuestionRequest
14
+
15
+
16
+ router = APIRouter(prefix="/question", tags=["question"])
17
+
18
+
19
+ @router.get("/", response_model=list[QuestionRequest])
20
+ async def list_questions(state: StateDep) -> list[QuestionRequest]:
21
+ """List all pending question requests.
22
+
23
+ Returns a list of all pending questions awaiting user response.
24
+ """
25
+ questions = []
26
+ for question_id, pending in state.pending_questions.items():
27
+ questions.append(
28
+ QuestionRequest(
29
+ id=question_id,
30
+ session_id=pending.session_id,
31
+ questions=pending.questions,
32
+ tool=pending.tool,
33
+ )
34
+ )
35
+ return questions
36
+
37
+
38
+ @router.post("/{requestID}/reply")
39
+ async def reply_to_question(
40
+ requestID: str, # noqa: N803
41
+ reply: QuestionReply,
42
+ state: StateDep,
43
+ ) -> bool:
44
+ """Reply to a question request.
45
+
46
+ The user provides answers to the questions. Answers must be provided
47
+ as an array of arrays, where each inner array contains the selected
48
+ label(s) for that question.
49
+
50
+ Args:
51
+ requestID: The question request ID
52
+ reply: The user's answers
53
+ state: Server state
54
+
55
+ Returns:
56
+ True if the question was resolved successfully
57
+
58
+ Raises:
59
+ HTTPException: If question not found or invalid provider
60
+ """
61
+ pending = state.pending_questions.get(requestID)
62
+ if not pending:
63
+ raise HTTPException(status_code=404, detail="Question request not found")
64
+
65
+ session_id = pending.session_id
66
+ provider = state.input_providers.get(session_id)
67
+
68
+ if not isinstance(provider, OpenCodeInputProvider):
69
+ raise HTTPException(status_code=500, detail="Invalid provider for session")
70
+
71
+ # Resolve via provider
72
+ success = provider.resolve_question(requestID, reply.answers)
73
+
74
+ if not success:
75
+ raise HTTPException(status_code=404, detail="Question already resolved")
76
+
77
+ # Broadcast replied event
78
+ event = QuestionRepliedEvent.create(
79
+ session_id=session_id,
80
+ request_id=requestID,
81
+ answers=reply.answers,
82
+ )
83
+ await state.broadcast_event(event)
84
+
85
+ return True
86
+
87
+
88
+ @router.post("/{requestID}/reject")
89
+ async def reject_question(
90
+ requestID: str, # noqa: N803
91
+ state: StateDep,
92
+ ) -> bool:
93
+ """Reject a question request.
94
+
95
+ Called when the user dismisses the question without providing an answer.
96
+
97
+ Args:
98
+ requestID: The question request ID
99
+ state: Server state
100
+
101
+ Returns:
102
+ True if the question was rejected successfully
103
+
104
+ Raises:
105
+ HTTPException: If question not found
106
+ """
107
+ pending = state.pending_questions.get(requestID)
108
+ if not pending:
109
+ raise HTTPException(status_code=404, detail="Question request not found")
110
+
111
+ session_id = pending.session_id
112
+ future = pending.future
113
+
114
+ # Cancel the future
115
+ if not future.done():
116
+ future.cancel()
117
+
118
+ # Remove from pending
119
+ del state.pending_questions[requestID]
120
+
121
+ # Broadcast rejected event
122
+ event = QuestionRejectedEvent.create(
123
+ session_id=session_id,
124
+ request_id=requestID,
125
+ )
126
+ await state.broadcast_event(event)
127
+
128
+ return True