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,462 @@
1
+ """OpenCode text sharing provider.
2
+
3
+ This implementation "fakes" OpenCode sessions by exploiting the fact that OpenCode's
4
+ share API doesn't validate session existence. The workflow is:
5
+
6
+ 1. Generate random UUIDs for session, message, and part IDs
7
+ 2. Call `/share_create` with the session ID to get a secret and share URL
8
+ 3. Use `/share_sync` to populate the session with:
9
+ - Session info (title, timestamps, metadata)
10
+ - Messages (with different roles: user, assistant, system)
11
+ - Text parts (containing the actual shared content)
12
+
13
+ The OpenCode web interface then displays this as if it were a real session.
14
+
15
+ Two sharing modes are supported:
16
+ - `share()`: Simple string sharing (single user message)
17
+ - `share_conversation()`: Structured multi-turn conversations with different roles
18
+
19
+ Note: This is not the intended use of OpenCode's API, which is designed to share
20
+ actual OpenCode sessions created via their desktop app/CLI. However, since no
21
+ authentication is required and sessions aren't validated, this works for sharing
22
+ text and conversations.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import time
28
+ from typing import TYPE_CHECKING, Any, Self
29
+ import uuid
30
+
31
+ import httpx
32
+
33
+ from agentpool_commands.text_sharing.base import ShareResult, TextSharer
34
+
35
+
36
+ if TYPE_CHECKING:
37
+ from anyenv.text_sharing.base import Visibility
38
+
39
+ from agentpool.messaging.message_history import MessageHistory
40
+ from agentpool.messaging.messages import ChatMessage
41
+
42
+
43
+ class OpenCodeSharer(TextSharer):
44
+ """OpenCode text sharing service.
45
+
46
+ Creates fake sessions by generating UUIDs and syncing content via the share API.
47
+ OpenCode doesn't validate session existence, allowing us to create ad-hoc shares.
48
+
49
+ Examples:
50
+ Share a simple string:
51
+ ```python
52
+ async with OpenCodeSharer() as sharer:
53
+ result = await sharer.share("Hello, World!", title="My Share")
54
+ print(result.url) # https://opencode.ai/s/abc12345
55
+ ```
56
+
57
+ Share a conversation:
58
+ ```python
59
+ # Use share_conversation() with a Conversation object from agentpool
60
+ async with OpenCodeSharer() as sharer:
61
+ result = await sharer.share_conversation(
62
+ conversation,
63
+ title="Python Discussion"
64
+ )
65
+ print(result.url)
66
+ ```
67
+ """
68
+
69
+ def __init__(
70
+ self,
71
+ *,
72
+ api_url: str | None = None,
73
+ timeout: float = 30.0,
74
+ ) -> None:
75
+ """Initialize OpenCode sharer.
76
+
77
+ Args:
78
+ api_url: OpenCode API URL (defaults to production)
79
+ timeout: Request timeout in seconds
80
+ """
81
+ self.api_url = api_url or "https://api.opencode.ai"
82
+ self.timeout = timeout
83
+ self._client = httpx.AsyncClient(timeout=timeout)
84
+
85
+ @property
86
+ def name(self) -> str:
87
+ """Name of the sharing service."""
88
+ return "OpenCode"
89
+
90
+ async def share(
91
+ self,
92
+ content: str,
93
+ *,
94
+ title: str | None = None,
95
+ syntax: str | None = None,
96
+ visibility: Visibility = "unlisted",
97
+ expires_in: int | None = None,
98
+ ) -> ShareResult:
99
+ """Share text content via OpenCode.
100
+
101
+ Args:
102
+ content: The text content to share
103
+ title: Optional title for the shared content
104
+ syntax: Syntax highlighting hint (ignored - OpenCode handles this)
105
+ visibility: Visibility level (ignored - OpenCode uses private shares)
106
+ expires_in: Expiration time (ignored - OpenCode doesn't support expiration)
107
+
108
+ Returns:
109
+ ShareResult with OpenCode share URL
110
+ """
111
+ try:
112
+ session_id = str(uuid.uuid4())
113
+ message_id = str(uuid.uuid4())
114
+ part_id = str(uuid.uuid4())
115
+ current_time = int(time.time() * 1000)
116
+
117
+ # Create share (returns secret and URL)
118
+ resp = await self._client.post(
119
+ f"{self.api_url}/share_create",
120
+ json={"sessionID": session_id},
121
+ )
122
+ resp.raise_for_status()
123
+ share_data = resp.json()
124
+ secret = share_data["secret"]
125
+ share_url = share_data["url"]
126
+
127
+ # Sync session info
128
+ info_key = f"session/info/{session_id}"
129
+ info_content = {
130
+ "id": session_id,
131
+ "projectID": "shared-content",
132
+ "directory": "/tmp",
133
+ "title": title or "Shared Content",
134
+ "version": "1.0.0",
135
+ "time": {
136
+ "created": current_time,
137
+ "updated": current_time,
138
+ },
139
+ }
140
+
141
+ resp = await self._client.post(
142
+ f"{self.api_url}/share_sync",
143
+ json={
144
+ "sessionID": session_id,
145
+ "secret": secret,
146
+ "key": info_key,
147
+ "content": info_content,
148
+ },
149
+ )
150
+ resp.raise_for_status()
151
+
152
+ # Sync message
153
+ msg_key = f"session/message/{session_id}/{message_id}"
154
+ msg_content = {
155
+ "id": message_id,
156
+ "sessionID": session_id,
157
+ "role": "user",
158
+ "time": {"created": current_time},
159
+ }
160
+
161
+ resp = await self._client.post(
162
+ f"{self.api_url}/share_sync",
163
+ json={
164
+ "sessionID": session_id,
165
+ "secret": secret,
166
+ "key": msg_key,
167
+ "content": msg_content,
168
+ },
169
+ )
170
+ resp.raise_for_status()
171
+
172
+ # Sync text part with actual content
173
+ part_key = f"session/part/{session_id}/{message_id}/{part_id}"
174
+ part_content = {
175
+ "id": part_id,
176
+ "sessionID": session_id,
177
+ "messageID": message_id,
178
+ "type": "text",
179
+ "text": content,
180
+ }
181
+
182
+ resp = await self._client.post(
183
+ f"{self.api_url}/share_sync",
184
+ json={
185
+ "sessionID": session_id,
186
+ "secret": secret,
187
+ "key": part_key,
188
+ "content": part_content,
189
+ },
190
+ )
191
+ resp.raise_for_status()
192
+
193
+ # Store secret in delete_url for later deletion
194
+ delete_url = f"{self.api_url}/share_delete#{secret}"
195
+
196
+ return ShareResult(
197
+ url=share_url,
198
+ raw_url=f"{self.api_url}/share_data?id={session_id[-8:]}",
199
+ delete_url=delete_url,
200
+ id=session_id,
201
+ )
202
+
203
+ except httpx.HTTPStatusError as e:
204
+ if e.response.status_code == 404: # noqa: PLR2004
205
+ msg = "OpenCode API endpoint not found - service may be unavailable"
206
+ raise RuntimeError(msg) from e
207
+ if e.response.status_code == 429: # noqa: PLR2004
208
+ msg = "Rate limited by OpenCode API"
209
+ raise RuntimeError(msg) from e
210
+ msg = f"OpenCode API error (HTTP {e.response.status_code}): {e.response.text}"
211
+ raise RuntimeError(msg) from e
212
+ except httpx.RequestError as e:
213
+ msg = f"Failed to connect to OpenCode API: {e}"
214
+ raise RuntimeError(msg) from e
215
+
216
+ async def _share_chat_messages(
217
+ self,
218
+ messages: list[ChatMessage[Any]],
219
+ *,
220
+ title: str | None = None,
221
+ visibility: Visibility = "unlisted",
222
+ expires_in: int | None = None,
223
+ ) -> ShareResult:
224
+ """Share ChatMessages using the OpenCode API.
225
+
226
+ Args:
227
+ messages: List of ChatMessage objects to share
228
+
229
+ This allows sharing AI chat sessions, multi-turn conversations,
230
+ or any structured dialogue with different roles.
231
+
232
+ Args:
233
+ messages: List of messages to share (must have at least one)
234
+ title: Optional title for the conversation
235
+ visibility: Visibility level (ignored - OpenCode uses private shares)
236
+ expires_in: Expiration time (ignored - OpenCode doesn't support expiration)
237
+
238
+ Returns:
239
+ ShareResult with OpenCode share URL
240
+
241
+ Example:
242
+ ```python
243
+ messages = [
244
+ Message(role="user", parts=[MessagePart(type="text", text="Hello!")]),
245
+ Message(role="assistant", parts=[MessagePart(type="text", text="Hi!")]),
246
+ ]
247
+ result = await sharer.share_conversation(messages, title="My Chat")
248
+ ```
249
+ """
250
+ if not messages:
251
+ msg = "Must provide at least one message"
252
+ raise ValueError(msg)
253
+
254
+ try:
255
+ session_id = str(uuid.uuid4())
256
+ current_time = int(time.time() * 1000)
257
+
258
+ # Create share
259
+ resp = await self._client.post(
260
+ f"{self.api_url}/share_create",
261
+ json={"sessionID": session_id},
262
+ )
263
+ resp.raise_for_status()
264
+ share_data = resp.json()
265
+ secret = share_data["secret"]
266
+ share_url = share_data["url"]
267
+
268
+ # Sync session info
269
+ info_key = f"session/info/{session_id}"
270
+ info_content = {
271
+ "id": session_id,
272
+ "projectID": "shared-conversation",
273
+ "directory": "/tmp",
274
+ "title": title or "Shared Conversation",
275
+ "version": "1.0.0",
276
+ "time": {
277
+ "created": current_time,
278
+ "updated": current_time,
279
+ },
280
+ }
281
+
282
+ resp = await self._client.post(
283
+ f"{self.api_url}/share_sync",
284
+ json={
285
+ "sessionID": session_id,
286
+ "secret": secret,
287
+ "key": info_key,
288
+ "content": info_content,
289
+ },
290
+ )
291
+ resp.raise_for_status()
292
+
293
+ # Sync each message and its parts
294
+ for chat_msg in messages:
295
+ from agentpool_server.opencode_server.converters import (
296
+ chat_message_to_opencode,
297
+ )
298
+
299
+ # Convert ChatMessage to OpenCode format using the full converter
300
+ message_with_parts = chat_message_to_opencode(
301
+ chat_msg,
302
+ session_id=session_id,
303
+ working_dir="/tmp",
304
+ agent_name=chat_msg.name or "default",
305
+ model_id=chat_msg.model_name or "unknown",
306
+ provider_id="agentpool",
307
+ )
308
+
309
+ # Serialize to dicts with camelCase and no None values
310
+ msg_info = message_with_parts.info.model_dump(by_alias=True, exclude_none=True)
311
+ msg_parts = [
312
+ p.model_dump(by_alias=True, exclude_none=True) for p in message_with_parts.parts
313
+ ]
314
+
315
+ # Sync message
316
+ msg_key = f"session/message/{session_id}/{msg_info['id']}"
317
+ resp = await self._client.post(
318
+ f"{self.api_url}/share_sync",
319
+ json={
320
+ "sessionID": session_id,
321
+ "secret": secret,
322
+ "key": msg_key,
323
+ "content": msg_info,
324
+ },
325
+ )
326
+ resp.raise_for_status()
327
+
328
+ # Sync parts
329
+ for part in msg_parts:
330
+ part_key = f"session/part/{session_id}/{msg_info['id']}/{part['id']}"
331
+
332
+ resp = await self._client.post(
333
+ f"{self.api_url}/share_sync",
334
+ json={
335
+ "sessionID": session_id,
336
+ "secret": secret,
337
+ "key": part_key,
338
+ "content": part,
339
+ },
340
+ )
341
+ resp.raise_for_status()
342
+
343
+ # Store secret in delete_url for later deletion
344
+ delete_url = f"{self.api_url}/share_delete#{secret}"
345
+
346
+ return ShareResult(
347
+ url=share_url,
348
+ raw_url=f"{self.api_url}/share_data?id={session_id[-8:]}",
349
+ delete_url=delete_url,
350
+ id=session_id,
351
+ )
352
+
353
+ except httpx.HTTPStatusError as e:
354
+ if e.response.status_code == 404: # noqa: PLR2004
355
+ msg = "OpenCode API endpoint not found - service may be unavailable"
356
+ raise RuntimeError(msg) from e
357
+ if e.response.status_code == 429: # noqa: PLR2004
358
+ msg = "Rate limited by OpenCode API"
359
+ raise RuntimeError(msg) from e
360
+ msg = f"OpenCode API error (HTTP {e.response.status_code}): {e.response.text}"
361
+ raise RuntimeError(msg) from e
362
+ except httpx.RequestError as e:
363
+ msg = f"Failed to connect to OpenCode API: {e}"
364
+ raise RuntimeError(msg) from e
365
+
366
+ async def share_conversation(
367
+ self,
368
+ conversation: MessageHistory,
369
+ *,
370
+ title: str | None = None,
371
+ visibility: Visibility = "unlisted",
372
+ expires_in: int | None = None,
373
+ num_messages: int | None = None,
374
+ ) -> ShareResult:
375
+ """Share conversation using OpenCode's native structured format.
376
+
377
+ Args:
378
+ conversation: Conversation object to share
379
+ title: Optional title for the conversation
380
+ visibility: Visibility level (ignored)
381
+ expires_in: Expiration time (ignored)
382
+ num_messages: Number of messages to include (None = all)
383
+
384
+ Returns:
385
+ ShareResult with OpenCode share URL
386
+
387
+ Note:
388
+ System prompts are stored as metadata on messages (UserMessage.system field
389
+ in OpenCode, ModelRequest.instructions in pydantic-ai), not as separate
390
+ "system" role messages. ChatMessage.role only supports "user" and "assistant".
391
+ """
392
+ # Get messages to share
393
+ messages_to_share = list(
394
+ conversation.chat_messages[-num_messages:]
395
+ if num_messages
396
+ else conversation.chat_messages
397
+ )
398
+
399
+ return await self._share_chat_messages(
400
+ list(messages_to_share),
401
+ title=title,
402
+ visibility=visibility,
403
+ expires_in=expires_in,
404
+ )
405
+
406
+ async def delete_share(self, result: ShareResult) -> bool:
407
+ """Delete a shared session.
408
+
409
+ Args:
410
+ result: The ShareResult containing session ID and secret
411
+
412
+ Returns:
413
+ True if deletion was successful
414
+ """
415
+ if not result.delete_url or "#" not in result.delete_url:
416
+ return False
417
+
418
+ # Extract secret from delete_url
419
+ secret = result.delete_url.split("#", 1)[1]
420
+
421
+ try:
422
+ resp = await self._client.post(
423
+ f"{self.api_url}/share_delete",
424
+ json={
425
+ "sessionID": result.id,
426
+ "secret": secret,
427
+ },
428
+ )
429
+ resp.raise_for_status()
430
+ except httpx.HTTPError:
431
+ return False
432
+ else:
433
+ return True
434
+
435
+ async def close(self) -> None:
436
+ """Close the HTTP client."""
437
+ await self._client.aclose()
438
+
439
+ async def __aenter__(self) -> Self:
440
+ return self
441
+
442
+ async def __aexit__(self, *args: object) -> None:
443
+ await self.close()
444
+
445
+
446
+ if __name__ == "__main__":
447
+ import asyncio
448
+
449
+ async def main() -> None:
450
+ """Test OpenCode sharing functionality."""
451
+ async with OpenCodeSharer() as sharer:
452
+ # Test simple string sharing
453
+ print("=== Simple String Share ===")
454
+ result = await sharer.share(
455
+ "Hello, World!\n\nThis is a test of OpenCode sharing.",
456
+ title="Test Share",
457
+ )
458
+ print(f"Share URL: {result.url}")
459
+ print(f"Raw API URL: {result.raw_url}")
460
+ print(f"Session ID: {result.id}")
461
+
462
+ asyncio.run(main())
@@ -0,0 +1,59 @@
1
+ """paste.rs text sharing provider."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from agentpool_commands.text_sharing.base import ShareResult, TextSharer
8
+
9
+
10
+ if TYPE_CHECKING:
11
+ from agentpool_commands.text_sharing.base import Visibility
12
+
13
+
14
+ class PasteRsSharer(TextSharer):
15
+ """Share text via paste.rs."""
16
+
17
+ @property
18
+ def name(self) -> str:
19
+ """Name of the sharing service."""
20
+ return "paste.rs"
21
+
22
+ async def share(
23
+ self,
24
+ content: str,
25
+ *,
26
+ title: str | None = None,
27
+ syntax: str | None = None,
28
+ visibility: Visibility = "unlisted",
29
+ expires_in: int | None = None,
30
+ ) -> ShareResult:
31
+ """Share content on paste.rs.
32
+
33
+ Args:
34
+ content: The text content to share
35
+ title: Ignored (paste.rs doesn't support titles)
36
+ syntax: Used as file extension for syntax highlighting
37
+ visibility: Ignored (all pastes are unlisted)
38
+ expires_in: Ignored (paste.rs doesn't support expiration)
39
+ """
40
+ import anyenv
41
+
42
+ headers = {"Content-Type": "text/plain; charset=utf-8"}
43
+ data = content.encode()
44
+ response = await anyenv.request("POST", "https://paste.rs/", data=data, headers=headers)
45
+ url = (await response.text()).strip()
46
+ ext = syntax or "txt"
47
+ return ShareResult(url=f"{url}.{ext}", raw_url=url)
48
+
49
+
50
+ if __name__ == "__main__":
51
+ import asyncio
52
+
53
+ async def main() -> None:
54
+ sharer = PasteRsSharer()
55
+ result = await sharer.share("# Test Paste\n\nHello from anyenv!", syntax="md")
56
+ print(f"URL: {result.url}")
57
+ print(f"Raw: {result.raw_url}")
58
+
59
+ asyncio.run(main())
@@ -0,0 +1,116 @@
1
+ """Pastebin.com text sharing provider."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from typing import TYPE_CHECKING, Any
7
+
8
+ from agentpool_commands.text_sharing.base import ShareResult, TextSharer
9
+
10
+
11
+ if TYPE_CHECKING:
12
+ from agentpool_commands.text_sharing.base import Visibility
13
+
14
+
15
+ # Pastebin expiry values
16
+ EXPIRY_MAP = {
17
+ None: "N", # Never
18
+ 600: "10M", # 10 Minutes
19
+ 3600: "1H", # 1 Hour
20
+ 86400: "1D", # 1 Day
21
+ 604800: "1W", # 1 Week
22
+ 1209600: "2W", # 2 Weeks
23
+ 2592000: "1M", # 1 Month
24
+ 15552000: "6M", # 6 Months
25
+ 31536000: "1Y", # 1 Year
26
+ }
27
+
28
+ # Map visibility to Pastebin's api_paste_private values
29
+ VISIBILITY_MAP = {"public": "0", "unlisted": "1", "private": "2"}
30
+
31
+
32
+ def _get_expiry_code(seconds: int | None) -> str:
33
+ """Convert seconds to Pastebin expiry code, rounding to nearest valid value."""
34
+ if seconds is None:
35
+ return "N"
36
+
37
+ valid_seconds = sorted(k for k in EXPIRY_MAP if k is not None)
38
+ closest = min(valid_seconds, key=lambda x: abs(x - seconds))
39
+ return EXPIRY_MAP[closest]
40
+
41
+
42
+ class PastebinSharer(TextSharer):
43
+ """Share text via Pastebin.com."""
44
+
45
+ def __init__(self, api_key: str | None = None) -> None:
46
+ """Initialize the Pastebin sharer.
47
+
48
+ Args:
49
+ api_key: Pastebin API developer key. If not provided,
50
+ reads from PASTEBIN_API_KEY environment variable.
51
+ """
52
+ self._api_key = api_key or os.environ.get("PASTEBIN_API_KEY")
53
+ if not self._api_key:
54
+ msg = "Pastebin API key required. Set PASTEBIN_API_KEY or pass api_key parameter."
55
+ raise ValueError(msg)
56
+
57
+ @property
58
+ def name(self) -> str:
59
+ """Name of the sharing service."""
60
+ return "Pastebin"
61
+
62
+ async def share(
63
+ self,
64
+ content: str,
65
+ *,
66
+ title: str | None = None,
67
+ syntax: str | None = None,
68
+ visibility: Visibility = "unlisted",
69
+ expires_in: int | None = None,
70
+ ) -> ShareResult:
71
+ """Share content on Pastebin.com.
72
+
73
+ Args:
74
+ content: The text content to share
75
+ title: Title/name of the paste
76
+ syntax: Syntax highlighting (e.g. "python", "markdown", "javascript")
77
+ visibility: "public", "unlisted", or "private"
78
+ expires_in: Expiration time in seconds (rounded to nearest valid value)
79
+ """
80
+ import anyenv
81
+
82
+ data: dict[str, Any] = {
83
+ "api_dev_key": self._api_key,
84
+ "api_option": "paste",
85
+ "api_paste_code": content,
86
+ "api_paste_private": VISIBILITY_MAP.get(visibility, "1"),
87
+ "api_paste_expire_date": _get_expiry_code(expires_in),
88
+ }
89
+
90
+ if title:
91
+ data["api_paste_name"] = title
92
+
93
+ if syntax:
94
+ data["api_paste_format"] = syntax
95
+
96
+ response = await anyenv.post("https://pastebin.com/api/api_post.php", data=data)
97
+ url = (await response.text()).strip()
98
+ if url.startswith("Bad API request"):
99
+ msg = f"Pastebin API error: {url}"
100
+ raise RuntimeError(msg)
101
+ paste_key = url.split("/")[-1]
102
+ raw_url = f"https://pastebin.com/raw/{paste_key}"
103
+ return ShareResult(url=url, raw_url=raw_url, id=paste_key)
104
+
105
+
106
+ if __name__ == "__main__":
107
+ import asyncio
108
+
109
+ async def main() -> None:
110
+ sharer = PastebinSharer()
111
+ result = await sharer.share("# Test Paste\n\nHello from anyenv!", title="test.md")
112
+ print(f"URL: {result.url}")
113
+ print(f"Raw: {result.raw_url}")
114
+ print(f"ID: {result.id}")
115
+
116
+ asyncio.run(main())