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,24 @@
1
+ """Shared FastAPI dependencies."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Annotated, cast
6
+
7
+ from fastapi import Depends, Request # noqa: TC002
8
+
9
+
10
+ if TYPE_CHECKING:
11
+ from agentpool_server.opencode_server.state import ServerState
12
+
13
+
14
+ def get_state(request: Request) -> ServerState:
15
+ """Get server state from request.
16
+
17
+ The state is stored on app.state.server_state during app creation.
18
+ """
19
+ from agentpool_server.opencode_server.state import ServerState
20
+
21
+ return cast(ServerState, request.app.state.server_state)
22
+
23
+
24
+ StateDep = Annotated["ServerState", Depends(get_state)]
@@ -0,0 +1,421 @@
1
+ """OpenCode-based input provider for agent interactions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from dataclasses import dataclass, field
7
+ from typing import TYPE_CHECKING, Any
8
+
9
+ from mcp import types
10
+
11
+ from agentpool.log import get_logger
12
+ from agentpool.ui.base import InputProvider
13
+ from agentpool_server.opencode_server.models.events import PermissionRequestEvent
14
+
15
+
16
+ if TYPE_CHECKING:
17
+ from agentpool.agents.context import AgentContext, ConfirmationResult
18
+ from agentpool.messaging import ChatMessage
19
+ from agentpool.tools.base import Tool
20
+ from agentpool_server.opencode_server.state import ServerState
21
+
22
+ logger = get_logger(__name__)
23
+
24
+ # OpenCode permission responses
25
+ PermissionResponse = str # "once" | "always" | "reject"
26
+
27
+
28
+ @dataclass
29
+ class PendingPermission:
30
+ """A pending permission request awaiting user response."""
31
+
32
+ permission_id: str
33
+ tool_name: str
34
+ args: dict[str, Any]
35
+ future: asyncio.Future[PermissionResponse]
36
+ created_at: float = field(default_factory=lambda: __import__("time").time())
37
+
38
+
39
+ class OpenCodeInputProvider(InputProvider):
40
+ """Input provider that uses OpenCode SSE/REST for user interactions.
41
+
42
+ This provider enables tool confirmation and elicitation requests
43
+ through the OpenCode protocol. When a tool needs confirmation:
44
+ 1. A permission request is created and stored
45
+ 2. An SSE event is broadcast to notify clients
46
+ 3. The provider awaits a response via the REST endpoint
47
+ 4. The client POSTs to /session/{id}/permissions/{permissionID} to respond
48
+ """
49
+
50
+ def __init__(self, state: ServerState, session_id: str) -> None:
51
+ """Initialize OpenCode input provider.
52
+
53
+ Args:
54
+ state: Server state for broadcasting events
55
+ session_id: The session ID for this provider
56
+ """
57
+ self.state = state
58
+ self.session_id = session_id
59
+ self._pending_permissions: dict[str, PendingPermission] = {}
60
+ self._tool_approvals: dict[str, str] = {} # tool_name -> "always" | "reject"
61
+ self._id_counter = 0
62
+
63
+ def _generate_permission_id(self) -> str:
64
+ """Generate a unique permission ID."""
65
+ self._id_counter += 1
66
+ return f"perm_{self._id_counter}_{int(__import__('time').time() * 1000)}"
67
+
68
+ async def get_tool_confirmation(
69
+ self,
70
+ context: AgentContext[Any],
71
+ tool: Tool,
72
+ args: dict[str, Any],
73
+ message_history: list[ChatMessage[Any]] | None = None,
74
+ ) -> ConfirmationResult:
75
+ """Get tool execution confirmation via OpenCode permission request.
76
+
77
+ Creates a pending permission, broadcasts an SSE event, and waits
78
+ for the client to respond via POST /session/{id}/permissions/{permissionID}.
79
+
80
+ Args:
81
+ context: Current node context
82
+ tool: Information about the tool to be executed
83
+ args: Tool arguments that will be passed to the tool
84
+ message_history: Optional conversation history
85
+
86
+ Returns:
87
+ Confirmation result indicating whether to allow, skip, or abort
88
+ """
89
+ try:
90
+ # Check if we have a standing approval/rejection for this tool
91
+ if tool.name in self._tool_approvals:
92
+ standing_decision = self._tool_approvals[tool.name]
93
+ if standing_decision == "always":
94
+ logger.debug("Auto-allowing tool", tool_name=tool.name, reason="always")
95
+ return "allow"
96
+ if standing_decision == "reject":
97
+ logger.debug("Auto-rejecting tool", tool_name=tool.name, reason="reject")
98
+ return "skip"
99
+
100
+ # Create a pending permission request
101
+ permission_id = self._generate_permission_id()
102
+ future: asyncio.Future[PermissionResponse] = asyncio.get_event_loop().create_future()
103
+
104
+ pending = PendingPermission(
105
+ permission_id=permission_id,
106
+ tool_name=tool.name,
107
+ args=args,
108
+ future=future,
109
+ )
110
+ self._pending_permissions[permission_id] = pending
111
+
112
+ max_preview_args = 3
113
+ args_preview = ", ".join(f"{k}={v!r}" for k, v in list(args.items())[:max_preview_args])
114
+ if len(args) > max_preview_args:
115
+ args_preview += ", ..."
116
+
117
+ # Extract call_id from AgentContext if available (set by ClaudeCodeAgent from streaming)
118
+ # Fall back to a generated ID if not available
119
+ call_id = getattr(context, "tool_call_id", None)
120
+ if call_id is None:
121
+ # Generate a synthetic call_id - won't match TUI tool parts but allows display
122
+ call_id = f"toolu_{permission_id}"
123
+ # TODO: Extract message_id from context when available
124
+
125
+ event = PermissionRequestEvent.create(
126
+ session_id=self.session_id,
127
+ permission_id=permission_id,
128
+ tool_name=tool.name,
129
+ args_preview=args_preview,
130
+ message=f"Tool '{tool.name}' wants to execute with args: {args_preview}",
131
+ call_id=call_id,
132
+ )
133
+
134
+ await self.state.broadcast_event(event)
135
+ logger.info("Permission requested", permission_id=permission_id, tool_name=tool.name)
136
+ # Wait for the client to respond
137
+ try:
138
+ response = await future
139
+ except asyncio.CancelledError:
140
+ logger.warning("Permission request cancelled", permission_id=permission_id)
141
+ return "skip"
142
+ finally:
143
+ # Clean up the pending permission
144
+ self._pending_permissions.pop(permission_id, None)
145
+
146
+ # Map OpenCode response to our confirmation result
147
+ return self._handle_permission_response(response, tool.name)
148
+
149
+ except Exception:
150
+ logger.exception("Failed to get tool confirmation")
151
+ return "abort_run"
152
+
153
+ def _handle_permission_response(
154
+ self, response: PermissionResponse, tool_name: str
155
+ ) -> ConfirmationResult:
156
+ """Handle permission response and update tool approval state."""
157
+ match response:
158
+ case "once":
159
+ return "allow"
160
+ case "always":
161
+ self._tool_approvals[tool_name] = "always"
162
+ logger.info("Tool approval set", tool_name=tool_name, approval="always")
163
+ return "allow"
164
+ case "reject":
165
+ return "skip"
166
+ case _:
167
+ logger.warning("Unknown permission response", response=response)
168
+ return "abort_run"
169
+
170
+ def resolve_permission(self, permission_id: str, response: PermissionResponse) -> bool:
171
+ """Resolve a pending permission request.
172
+
173
+ Called by the REST endpoint when the client responds.
174
+
175
+ Args:
176
+ permission_id: The permission request ID
177
+ response: The client's response ("once", "always", or "reject")
178
+
179
+ Returns:
180
+ True if the permission was found and resolved, False otherwise
181
+ """
182
+ pending = self._pending_permissions.get(permission_id)
183
+ if pending is None:
184
+ logger.warning("Permission not found", permission_id=permission_id)
185
+ return False
186
+
187
+ if pending.future.done():
188
+ logger.warning("Permission already resolved", permission_id=permission_id)
189
+ return False
190
+
191
+ pending.future.set_result(response)
192
+ logger.info(
193
+ "Permission resolved",
194
+ permission_id=permission_id,
195
+ response=response,
196
+ )
197
+ return True
198
+
199
+ def get_pending_permissions(self) -> list[dict[str, Any]]:
200
+ """Get all pending permission requests.
201
+
202
+ Returns:
203
+ List of pending permission info dicts
204
+ """
205
+ return [
206
+ {
207
+ "permission_id": p.permission_id,
208
+ "tool_name": p.tool_name,
209
+ "args": p.args,
210
+ "created_at": p.created_at,
211
+ }
212
+ for p in self._pending_permissions.values()
213
+ ]
214
+
215
+ async def get_elicitation(
216
+ self,
217
+ params: types.ElicitRequestParams,
218
+ ) -> types.ElicitResult | types.ErrorData:
219
+ """Get user response to elicitation request via OpenCode questions.
220
+
221
+ Translates MCP elicitation requests to OpenCode question system.
222
+ Supports both single-select and multi-select questions.
223
+
224
+ Args:
225
+ params: MCP elicit request parameters
226
+
227
+ Returns:
228
+ Elicit result with user's response or error data
229
+ """
230
+ # For URL elicitation, we could open the URL
231
+ if isinstance(params, types.ElicitRequestURLParams):
232
+ logger.info(
233
+ "URL elicitation request",
234
+ message=params.message,
235
+ url=params.url,
236
+ )
237
+ # Could potentially open URL in browser here
238
+ return types.ElicitResult(action="decline")
239
+
240
+ # For form elicitation with enum schema, use OpenCode questions
241
+ if isinstance(params, types.ElicitRequestFormParams):
242
+ schema = params.requestedSchema
243
+
244
+ # Check if schema defines options (enum)
245
+ enum_values = schema.get("enum")
246
+ if enum_values:
247
+ return await self._handle_question_elicitation(params, schema)
248
+
249
+ # Check if it's an array schema with enum items
250
+ if schema.get("type") == "array":
251
+ items = schema.get("items", {})
252
+ if items.get("enum"):
253
+ return await self._handle_question_elicitation(params, schema)
254
+
255
+ # For other form elicitation, we don't have UI support yet
256
+ logger.info(
257
+ "Form elicitation request (not supported)",
258
+ message=params.message,
259
+ schema=getattr(params, "requestedSchema", None),
260
+ )
261
+ return types.ElicitResult(action="decline")
262
+
263
+ async def _handle_question_elicitation(
264
+ self,
265
+ params: types.ElicitRequestFormParams,
266
+ schema: dict[str, Any],
267
+ ) -> types.ElicitResult | types.ErrorData:
268
+ """Handle elicitation via OpenCode question system.
269
+
270
+ Args:
271
+ params: Form elicitation parameters
272
+ schema: JSON schema with enum values
273
+
274
+ Returns:
275
+ Elicit result with user's answer
276
+ """
277
+ import asyncio
278
+
279
+ from agentpool_server.opencode_server.models.events import QuestionAskedEvent
280
+
281
+ # Extract enum values
282
+ is_multi = schema.get("type") == "array"
283
+ if is_multi:
284
+ enum_values = schema.get("items", {}).get("enum", [])
285
+ else:
286
+ enum_values = schema.get("enum", [])
287
+
288
+ if not enum_values:
289
+ return types.ElicitResult(action="decline")
290
+
291
+ # Extract descriptions if available (custom x-option-descriptions field)
292
+ descriptions = schema.get("x-option-descriptions", {})
293
+
294
+ # Build OpenCode question format
295
+ from agentpool_server.opencode_server.models.question import (
296
+ QuestionInfo,
297
+ QuestionOption,
298
+ )
299
+
300
+ question_id = self._generate_permission_id() # Reuse ID generator
301
+ question_info = QuestionInfo(
302
+ question=params.message,
303
+ header=params.message[:12], # Truncate to 12 chars
304
+ options=[
305
+ QuestionOption(
306
+ label=str(val),
307
+ description=descriptions.get(str(val), ""),
308
+ )
309
+ for val in enum_values
310
+ ],
311
+ multiple=is_multi or None,
312
+ )
313
+
314
+ # Create future to wait for answer
315
+ future: asyncio.Future[list[list[str]]] = asyncio.get_event_loop().create_future()
316
+
317
+ # Store pending question
318
+ from agentpool_server.opencode_server.state import PendingQuestion
319
+
320
+ self.state.pending_questions[question_id] = PendingQuestion(
321
+ session_id=self.session_id,
322
+ questions=[question_info],
323
+ future=future,
324
+ tool=None, # Not associated with a specific tool call
325
+ )
326
+
327
+ # Broadcast event (serialize QuestionInfo to dict)
328
+ event = QuestionAskedEvent.create(
329
+ request_id=question_id,
330
+ session_id=self.session_id,
331
+ questions=[question_info.model_dump(mode="json", by_alias=True)],
332
+ )
333
+ await self.state.broadcast_event(event)
334
+
335
+ logger.info(
336
+ "Question asked",
337
+ question_id=question_id,
338
+ message=params.message,
339
+ is_multi=is_multi,
340
+ )
341
+
342
+ # Wait for answer
343
+ try:
344
+ answers = await future # list[list[str]]
345
+ answer = answers[0] if answers else [] # Get first question's answer
346
+
347
+ # ElicitResult content must be a dict, not a plain value
348
+ # Wrap the answer in a dict with a "value" key
349
+ # Multi-select: return list in dict
350
+ # Single-select: return string in dict
351
+ content: dict[str, str | list[str]] = (
352
+ {"value": answer} if is_multi else {"value": answer[0] if answer else ""}
353
+ )
354
+ return types.ElicitResult(action="accept", content=content)
355
+ except asyncio.CancelledError:
356
+ logger.info("Question cancelled", question_id=question_id)
357
+ return types.ElicitResult(action="cancel")
358
+ except Exception as e:
359
+ logger.exception("Question failed", question_id=question_id)
360
+ return types.ErrorData(
361
+ code=-1, # Generic error code
362
+ message=f"Elicitation failed: {e}",
363
+ )
364
+ finally:
365
+ # Clean up pending question
366
+ self.state.pending_questions.pop(question_id, None)
367
+
368
+ def clear_tool_approvals(self) -> None:
369
+ """Clear all stored tool approval decisions."""
370
+ approval_count = len(self._tool_approvals)
371
+ self._tool_approvals.clear()
372
+ logger.info("Cleared tool approval decisions", count=approval_count)
373
+
374
+ def resolve_question(
375
+ self,
376
+ question_id: str,
377
+ answers: list[list[str]],
378
+ ) -> bool:
379
+ """Resolve a pending question request.
380
+
381
+ Called by the REST endpoint when the client responds.
382
+
383
+ Args:
384
+ question_id: The question request ID
385
+ answers: User's answers (array of arrays per OpenCode format)
386
+
387
+ Returns:
388
+ True if the question was found and resolved, False otherwise
389
+ """
390
+ pending = self.state.pending_questions.get(question_id)
391
+ if pending is None:
392
+ logger.warning("Question not found", question_id=question_id)
393
+ return False
394
+
395
+ future = pending.future
396
+ if future.done():
397
+ logger.warning("Question already resolved", question_id=question_id)
398
+ return False
399
+
400
+ future.set_result(answers)
401
+ logger.info(
402
+ "Question resolved",
403
+ question_id=question_id,
404
+ answers=answers,
405
+ )
406
+ return True
407
+
408
+ def cancel_all_pending(self) -> int:
409
+ """Cancel all pending permission requests.
410
+
411
+ Returns:
412
+ Number of permissions cancelled
413
+ """
414
+ count = 0
415
+ for pending in list(self._pending_permissions.values()):
416
+ if not pending.future.done():
417
+ pending.future.cancel()
418
+ count += 1
419
+ self._pending_permissions.clear()
420
+ logger.info("Cancelled all pending permissions", count=count)
421
+ return count
@@ -0,0 +1,250 @@
1
+ """OpenCode API models.
2
+
3
+ All models inherit from OpenCodeBaseModel which provides:
4
+ - populate_by_name=True for camelCase alias support
5
+ - by_alias=True serialization by default
6
+ """
7
+
8
+ from agentpool_server.opencode_server.models.base import OpenCodeBaseModel
9
+ from agentpool_server.opencode_server.models.common import (
10
+ TimeCreated,
11
+ TimeCreatedUpdated,
12
+ )
13
+ from agentpool_server.opencode_server.models.app import (
14
+ App,
15
+ AppTimeInfo,
16
+ HealthResponse,
17
+ PathInfo,
18
+ Project,
19
+ ProjectTime,
20
+ ProjectUpdateRequest,
21
+ VcsInfo,
22
+ )
23
+ from agentpool_server.opencode_server.models.provider import (
24
+ Model,
25
+ ModelCost,
26
+ ModelLimit,
27
+ Mode,
28
+ ModeModel,
29
+ Provider,
30
+ ProviderListResponse,
31
+ ProvidersResponse,
32
+ )
33
+ from agentpool_server.opencode_server.models.session import (
34
+ Session,
35
+ SessionCreateRequest,
36
+ SessionForkRequest,
37
+ SessionInitRequest,
38
+ SessionRevert,
39
+ SessionShare,
40
+ SessionStatus,
41
+ SessionSummary,
42
+ SessionUpdateRequest,
43
+ SummarizeRequest,
44
+ Todo,
45
+ )
46
+ from agentpool_server.opencode_server.models.message import (
47
+ AssistantMessage,
48
+ CommandRequest,
49
+ FilePartInput,
50
+ MessageInfo,
51
+ MessagePath,
52
+ MessageRequest,
53
+ MessageSummary,
54
+ MessageTime,
55
+ MessageWithParts,
56
+ PartInput,
57
+ ShellRequest,
58
+ TextPartInput,
59
+ Tokens,
60
+ TokensCache,
61
+ UserMessage,
62
+ UserMessageModel,
63
+ )
64
+ from agentpool_server.opencode_server.models.parts import (
65
+ AgentPart,
66
+ CompactionPart,
67
+ FilePart,
68
+ Part,
69
+ PatchPart,
70
+ ReasoningPart,
71
+ RetryPart,
72
+ SnapshotPart,
73
+ StepFinishPart,
74
+ StepStartPart,
75
+ SubtaskPart,
76
+ TextPart,
77
+ TimeStart,
78
+ TimeStartEnd,
79
+ TimeStartEndCompacted,
80
+ TimeStartEndOptional,
81
+ ToolPart,
82
+ ToolState,
83
+ ToolStateCompleted,
84
+ ToolStateError,
85
+ ToolStatePending,
86
+ ToolStateRunning,
87
+ )
88
+ from agentpool_server.opencode_server.models.file import (
89
+ FileContent,
90
+ FileNode,
91
+ FileStatus,
92
+ FindMatch,
93
+ Symbol,
94
+ )
95
+ from agentpool_server.opencode_server.models.agent import (
96
+ Agent,
97
+ Command,
98
+ )
99
+ from agentpool_server.opencode_server.models.pty import (
100
+ PtyCreateRequest,
101
+ PtyInfo,
102
+ PtySize,
103
+ PtyUpdateRequest,
104
+ )
105
+ from agentpool_server.opencode_server.models.events import (
106
+ Event,
107
+ MessageRemovedEvent,
108
+ MessageUpdatedEvent,
109
+ MessageUpdatedEventProperties,
110
+ PartRemovedEvent,
111
+ PartUpdatedEvent,
112
+ PartUpdatedEventProperties,
113
+ ProjectUpdatedEvent,
114
+ ServerConnectedEvent,
115
+ SessionCompactedEvent,
116
+ SessionCompactedProperties,
117
+ SessionCreatedEvent,
118
+ SessionDeletedEvent,
119
+ SessionDeletedProperties,
120
+ SessionErrorEvent,
121
+ SessionErrorInfo,
122
+ SessionErrorProperties,
123
+ SessionIdleEvent,
124
+ SessionIdleProperties,
125
+ SessionInfoProperties,
126
+ SessionStatusEvent,
127
+ SessionStatusProperties,
128
+ SessionUpdatedEvent,
129
+ )
130
+ from agentpool_server.opencode_server.models.mcp import LogRequest, MCPStatus, McpResource
131
+ from agentpool_server.opencode_server.models.config import Config
132
+ from agentpool_server.opencode_server.models.question import (
133
+ QuestionInfo,
134
+ QuestionOption,
135
+ QuestionReply,
136
+ QuestionRequest,
137
+ )
138
+
139
+ __all__ = [
140
+ "Agent",
141
+ "AgentPart",
142
+ "App",
143
+ "AppTimeInfo",
144
+ "AssistantMessage",
145
+ "Command",
146
+ "CommandRequest",
147
+ "CompactionPart",
148
+ "Config",
149
+ "Event",
150
+ "FileContent",
151
+ "FileNode",
152
+ "FilePart",
153
+ "FilePartInput",
154
+ "FileStatus",
155
+ "FindMatch",
156
+ "HealthResponse",
157
+ "LogRequest",
158
+ "MCPStatus",
159
+ "McpResource",
160
+ "MessageInfo",
161
+ "MessagePath",
162
+ "MessageRemovedEvent",
163
+ "MessageRequest",
164
+ "MessageSummary",
165
+ "MessageTime",
166
+ "MessageUpdatedEvent",
167
+ "MessageUpdatedEventProperties",
168
+ "MessageWithParts",
169
+ "Mode",
170
+ "ModeModel",
171
+ "Model",
172
+ "ModelCost",
173
+ "ModelLimit",
174
+ "OpenCodeBaseModel",
175
+ "Part",
176
+ "PartInput",
177
+ "PartRemovedEvent",
178
+ "PartUpdatedEvent",
179
+ "PartUpdatedEventProperties",
180
+ "PatchPart",
181
+ "PathInfo",
182
+ "Project",
183
+ "ProjectTime",
184
+ "ProjectUpdateRequest",
185
+ "ProjectUpdatedEvent",
186
+ "Provider",
187
+ "ProviderListResponse",
188
+ "ProvidersResponse",
189
+ "PtyCreateRequest",
190
+ "PtyInfo",
191
+ "PtySize",
192
+ "PtyUpdateRequest",
193
+ "QuestionInfo",
194
+ "QuestionOption",
195
+ "QuestionReply",
196
+ "QuestionRequest",
197
+ "ReasoningPart",
198
+ "RetryPart",
199
+ "ServerConnectedEvent",
200
+ "Session",
201
+ "SessionCompactedEvent",
202
+ "SessionCompactedProperties",
203
+ "SessionCreateRequest",
204
+ "SessionCreatedEvent",
205
+ "SessionDeletedEvent",
206
+ "SessionDeletedProperties",
207
+ "SessionErrorEvent",
208
+ "SessionErrorInfo",
209
+ "SessionErrorProperties",
210
+ "SessionForkRequest",
211
+ "SessionIdleEvent",
212
+ "SessionIdleProperties",
213
+ "SessionInfoProperties",
214
+ "SessionInitRequest",
215
+ "SessionRevert",
216
+ "SessionShare",
217
+ "SessionStatus",
218
+ "SessionStatusEvent",
219
+ "SessionStatusProperties",
220
+ "SessionSummary",
221
+ "SessionUpdateRequest",
222
+ "SessionUpdatedEvent",
223
+ "ShellRequest",
224
+ "SnapshotPart",
225
+ "StepFinishPart",
226
+ "StepStartPart",
227
+ "SubtaskPart",
228
+ "SummarizeRequest",
229
+ "Symbol",
230
+ "TextPart",
231
+ "TextPartInput",
232
+ "TimeCreated",
233
+ "TimeCreatedUpdated",
234
+ "TimeStart",
235
+ "TimeStartEnd",
236
+ "TimeStartEndCompacted",
237
+ "TimeStartEndOptional",
238
+ "Todo",
239
+ "Tokens",
240
+ "TokensCache",
241
+ "ToolPart",
242
+ "ToolState",
243
+ "ToolStateCompleted",
244
+ "ToolStateError",
245
+ "ToolStatePending",
246
+ "ToolStateRunning",
247
+ "UserMessage",
248
+ "UserMessageModel",
249
+ "VcsInfo",
250
+ ]