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
@@ -15,7 +15,12 @@ from agentpool.utils.now import get_now
15
15
  from agentpool.utils.parse_time import parse_time_period
16
16
  from agentpool_storage.base import StorageProvider
17
17
  from agentpool_storage.models import QueryFilters
18
- from agentpool_storage.sql_provider.models import CommandHistory, Conversation, Message
18
+ from agentpool_storage.sql_provider.models import (
19
+ CommandHistory,
20
+ Conversation,
21
+ Message,
22
+ Project,
23
+ )
19
24
  from agentpool_storage.sql_provider.utils import (
20
25
  build_message_query,
21
26
  format_conversation,
@@ -33,6 +38,7 @@ if TYPE_CHECKING:
33
38
 
34
39
  from agentpool.common_types import JsonValue
35
40
  from agentpool.messaging import ChatMessage
41
+ from agentpool.sessions.models import ProjectData
36
42
  from agentpool_config.session import SessionQuery
37
43
  from agentpool_config.storage import SQLStorageConfig
38
44
  from agentpool_storage.models import ConversationData, StatsFilters
@@ -120,10 +126,10 @@ class SQLModelProvider(StorageProvider):
120
126
  content: str,
121
127
  role: str,
122
128
  name: str | None = None,
129
+ parent_id: str | None = None,
123
130
  cost_info: TokenCost | None = None,
124
131
  model: str | None = None,
125
132
  response_time: float | None = None,
126
- forwarded_from: list[str] | None = None,
127
133
  provider_name: str | None = None,
128
134
  provider_response_id: str | None = None,
129
135
  messages: str | None = None,
@@ -138,6 +144,7 @@ class SQLModelProvider(StorageProvider):
138
144
  msg = Message(
139
145
  conversation_id=conversation_id,
140
146
  id=message_id,
147
+ parent_id=parent_id,
141
148
  content=content,
142
149
  role=role,
143
150
  name=name,
@@ -149,7 +156,6 @@ class SQLModelProvider(StorageProvider):
149
156
  input_tokens=cost_info.token_usage.input_tokens if cost_info else None,
150
157
  output_tokens=cost_info.token_usage.output_tokens if cost_info else None,
151
158
  cost=float(cost_info.total_cost) if cost_info else None,
152
- forwarded_from=forwarded_from,
153
159
  provider_name=provider_name,
154
160
  provider_response_id=provider_response_id,
155
161
  messages=messages,
@@ -202,6 +208,141 @@ class SQLModelProvider(StorageProvider):
202
208
  )
203
209
  return result.scalar_one_or_none()
204
210
 
211
+ async def get_conversation_messages(
212
+ self,
213
+ conversation_id: str,
214
+ *,
215
+ include_ancestors: bool = False,
216
+ ) -> list[ChatMessage[str]]:
217
+ """Get all messages for a conversation.
218
+
219
+ Args:
220
+ conversation_id: ID of the conversation
221
+ include_ancestors: If True, traverse parent_id chain to include
222
+ messages from ancestor conversations (for forked convos).
223
+
224
+ Returns:
225
+ List of messages ordered by timestamp.
226
+ """
227
+ async with AsyncSession(self.engine) as session:
228
+ # Get messages for this conversation
229
+ result = await session.execute(
230
+ select(Message)
231
+ .where(Message.conversation_id == conversation_id)
232
+ .order_by(Message.timestamp.asc()) # type: ignore
233
+ )
234
+ messages = [to_chat_message(m) for m in result.scalars().all()]
235
+
236
+ if not include_ancestors or not messages:
237
+ return messages
238
+
239
+ # Find the first message's parent_id to get ancestor chain
240
+ first_msg = messages[0]
241
+ if first_msg.parent_id:
242
+ ancestors = await self.get_message_ancestry(first_msg.parent_id)
243
+ return ancestors + messages
244
+
245
+ return messages
246
+
247
+ async def get_message(
248
+ self,
249
+ message_id: str,
250
+ ) -> ChatMessage[str] | None:
251
+ """Get a single message by ID."""
252
+ async with AsyncSession(self.engine) as session:
253
+ result = await session.execute(select(Message).where(Message.id == message_id))
254
+ msg = result.scalar_one_or_none()
255
+ return to_chat_message(msg) if msg else None
256
+
257
+ async def get_message_ancestry(
258
+ self,
259
+ message_id: str,
260
+ ) -> list[ChatMessage[str]]:
261
+ """Get the ancestry chain of a message.
262
+
263
+ Traverses parent_id chain to build full history.
264
+ """
265
+ ancestors: list[ChatMessage[str]] = []
266
+ current_id: str | None = message_id
267
+
268
+ async with AsyncSession(self.engine) as session:
269
+ while current_id:
270
+ result = await session.execute(select(Message).where(Message.id == current_id))
271
+ msg = result.scalar_one_or_none()
272
+ if not msg:
273
+ break
274
+ ancestors.append(to_chat_message(msg))
275
+ current_id = msg.parent_id
276
+
277
+ # Reverse to get oldest first
278
+ ancestors.reverse()
279
+ return ancestors
280
+
281
+ async def fork_conversation(
282
+ self,
283
+ *,
284
+ source_conversation_id: str,
285
+ new_conversation_id: str,
286
+ fork_from_message_id: str | None = None,
287
+ new_agent_name: str | None = None,
288
+ ) -> str | None:
289
+ """Fork a conversation at a specific point.
290
+
291
+ Creates a new conversation record. The fork point message_id is returned
292
+ so callers can set it as parent_id for new messages.
293
+ """
294
+ async with AsyncSession(self.engine) as session:
295
+ # Get source conversation
296
+ result = await session.execute(
297
+ select(Conversation).where(Conversation.id == source_conversation_id)
298
+ )
299
+ source_conv = result.scalar_one_or_none()
300
+ if not source_conv:
301
+ msg = f"Source conversation not found: {source_conversation_id}"
302
+ raise ValueError(msg)
303
+
304
+ # Determine fork point
305
+ fork_point_id: str | None = None
306
+ if fork_from_message_id:
307
+ # Verify the message exists and belongs to the source conversation
308
+ msg_result = await session.execute(
309
+ select(Message).where(
310
+ Message.id == fork_from_message_id,
311
+ Message.conversation_id == source_conversation_id,
312
+ )
313
+ )
314
+ fork_msg = msg_result.scalar_one_or_none()
315
+ if not fork_msg:
316
+ err = f"Message {fork_from_message_id} not found in conversation"
317
+ raise ValueError(err)
318
+ fork_point_id = fork_from_message_id
319
+ else:
320
+ # Fork from the last message
321
+ msg_result = await session.execute(
322
+ select(Message)
323
+ .where(Message.conversation_id == source_conversation_id)
324
+ .order_by(desc(Message.timestamp))
325
+ .limit(1)
326
+ )
327
+ last_msg = msg_result.scalar_one_or_none()
328
+ if last_msg:
329
+ fork_point_id = last_msg.id
330
+
331
+ # Create new conversation
332
+ agent_name = new_agent_name or source_conv.agent_name
333
+ new_conv = Conversation(
334
+ id=new_conversation_id,
335
+ agent_name=agent_name,
336
+ title=f"{source_conv.title or 'Conversation'} (fork)"
337
+ if source_conv.title
338
+ else None,
339
+ start_time=get_now(),
340
+ )
341
+ session.add(new_conv)
342
+ await session.commit()
343
+
344
+ return fork_point_id
345
+
205
346
  async def log_command(
206
347
  self,
207
348
  *,
@@ -442,3 +583,128 @@ class SQLModelProvider(StorageProvider):
442
583
  msg_count = len(msg_result.scalars().all())
443
584
 
444
585
  return conv_count, msg_count
586
+
587
+ async def delete_conversation_messages(
588
+ self,
589
+ conversation_id: str,
590
+ ) -> int:
591
+ """Delete all messages for a conversation."""
592
+ from sqlalchemy import delete, func
593
+
594
+ async with AsyncSession(self.engine) as session:
595
+ # First count messages to return
596
+ count_result = await session.execute(
597
+ select(func.count()).where(Message.conversation_id == conversation_id)
598
+ )
599
+ count = count_result.scalar() or 0
600
+
601
+ # Then delete
602
+ await session.execute(
603
+ delete(Message).where(Message.conversation_id == conversation_id) # type: ignore[arg-type]
604
+ )
605
+ await session.commit()
606
+ return count
607
+
608
+ # Project methods
609
+
610
+ def _to_project_data(self, row: Project) -> ProjectData:
611
+ """Convert database model to ProjectData."""
612
+ from agentpool.sessions.models import ProjectData
613
+
614
+ return ProjectData(
615
+ project_id=row.project_id,
616
+ worktree=row.worktree,
617
+ name=row.name,
618
+ vcs=row.vcs,
619
+ config_path=row.config_path,
620
+ created_at=row.created_at,
621
+ last_active=row.last_active,
622
+ settings=row.settings_json or {},
623
+ )
624
+
625
+ def _to_project_model(self, data: ProjectData) -> Project:
626
+ """Convert ProjectData to database model."""
627
+ return Project(
628
+ project_id=data.project_id,
629
+ worktree=data.worktree,
630
+ name=data.name,
631
+ vcs=data.vcs,
632
+ config_path=data.config_path,
633
+ created_at=data.created_at,
634
+ last_active=data.last_active,
635
+ settings_json=data.settings,
636
+ )
637
+
638
+ async def save_project(self, project: ProjectData) -> None:
639
+ """Save or update a project."""
640
+ from sqlalchemy import delete
641
+
642
+ async with AsyncSession(self.engine) as session:
643
+ # Delete existing if present (upsert via delete+insert)
644
+ stmt = delete(Project).where(Project.project_id == project.project_id) # type: ignore[arg-type]
645
+ await session.execute(stmt)
646
+
647
+ # Insert new/updated
648
+ db_project = self._to_project_model(project)
649
+ session.add(db_project)
650
+ await session.commit()
651
+ logger.debug("Saved project", project_id=project.project_id)
652
+
653
+ async def get_project(self, project_id: str) -> ProjectData | None:
654
+ """Get a project by ID."""
655
+ async with AsyncSession(self.engine) as session:
656
+ stmt = select(Project).where(Project.project_id == project_id)
657
+ result = await session.execute(stmt)
658
+ row = result.scalars().first()
659
+ return self._to_project_data(row) if row else None
660
+
661
+ async def get_project_by_worktree(self, worktree: str) -> ProjectData | None:
662
+ """Get a project by worktree path."""
663
+ async with AsyncSession(self.engine) as session:
664
+ stmt = select(Project).where(Project.worktree == worktree)
665
+ result = await session.execute(stmt)
666
+ row = result.scalars().first()
667
+ return self._to_project_data(row) if row else None
668
+
669
+ async def get_project_by_name(self, name: str) -> ProjectData | None:
670
+ """Get a project by friendly name."""
671
+ async with AsyncSession(self.engine) as session:
672
+ stmt = select(Project).where(Project.name == name)
673
+ result = await session.execute(stmt)
674
+ row = result.scalars().first()
675
+ return self._to_project_data(row) if row else None
676
+
677
+ async def list_projects(self, limit: int | None = None) -> list[ProjectData]:
678
+ """List all projects, ordered by last_active descending."""
679
+ async with AsyncSession(self.engine) as session:
680
+ stmt = select(Project).order_by(desc(Project.last_active))
681
+ if limit is not None:
682
+ stmt = stmt.limit(limit)
683
+ result = await session.execute(stmt)
684
+ return [self._to_project_data(row) for row in result.scalars().all()]
685
+
686
+ async def delete_project(self, project_id: str) -> bool:
687
+ """Delete a project."""
688
+ from sqlalchemy import delete
689
+
690
+ async with AsyncSession(self.engine) as session:
691
+ stmt = delete(Project).where(Project.project_id == project_id) # type: ignore[arg-type]
692
+ result = await session.execute(stmt)
693
+ await session.commit()
694
+ deleted: bool = result.rowcount > 0 # type: ignore[attr-defined]
695
+ if deleted:
696
+ logger.debug("Deleted project", project_id=project_id)
697
+ return deleted
698
+
699
+ async def touch_project(self, project_id: str) -> None:
700
+ """Update project's last_active timestamp."""
701
+ from sqlalchemy import update
702
+
703
+ async with AsyncSession(self.engine) as session:
704
+ stmt = (
705
+ update(Project)
706
+ .where(Project.project_id == project_id) # type: ignore[arg-type]
707
+ .values(last_active=get_now())
708
+ )
709
+ await session.execute(stmt)
710
+ await session.commit()
@@ -2,13 +2,13 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import contextlib
5
6
  from datetime import datetime
6
7
  from decimal import Decimal
7
8
  from typing import TYPE_CHECKING, Any
8
9
 
9
10
  from pydantic_ai import RunUsage
10
- from sqlalchemy import JSON, Column, and_, or_
11
- from sqlalchemy.sql import expression
11
+ from sqlalchemy import Column, and_
12
12
  from sqlmodel import select
13
13
 
14
14
  from agentpool.messaging import ChatMessage, TokenCost
@@ -67,7 +67,6 @@ def to_chat_message(db_message: Message) -> ChatMessage[str]:
67
67
  model_name=db_message.model,
68
68
  cost_info=cost_info,
69
69
  response_time=db_message.response_time,
70
- forwarded_from=db_message.forwarded_from or [],
71
70
  timestamp=db_message.timestamp,
72
71
  provider_name=db_message.provider_name,
73
72
  provider_response_id=db_message.provider_response_id,
@@ -101,9 +100,14 @@ def auto_migrate_columns(sync_conn: Any, dialect: Any) -> None:
101
100
  from sqlmodel import SQLModel
102
101
 
103
102
  inspector = inspect(sync_conn)
103
+ existing_tables = set(inspector.get_table_names())
104
104
 
105
105
  # For each table in our models
106
106
  for table_name, table in SQLModel.metadata.tables.items():
107
+ # Skip tables that don't exist yet (they'll be created fresh)
108
+ if table_name not in existing_tables:
109
+ continue
110
+
107
111
  existing = {col["name"] for col in inspector.get_columns(table_name)}
108
112
 
109
113
  # For each column in model that doesn't exist in DB
@@ -116,7 +120,9 @@ def auto_migrate_columns(sync_conn: Any, dialect: Any) -> None:
116
120
  sql = (
117
121
  f"ALTER TABLE {table_name} ADD COLUMN {col.name} {type_sql}{nullable}{default}"
118
122
  )
119
- sync_conn.execute(text(sql))
123
+ # Column may already exist (race condition or stale inspector cache)
124
+ with contextlib.suppress(Exception):
125
+ sync_conn.execute(text(sql))
120
126
 
121
127
 
122
128
  def parse_model_info(model: str | None) -> tuple[str | None, str | None]:
@@ -160,15 +166,7 @@ def build_message_query(query: SessionQuery) -> SelectOfScalar[Any]:
160
166
  if query.name:
161
167
  conditions.append(Message.conversation_id == query.name)
162
168
  if query.agents:
163
- agent_conditions = [Column("name").in_(query.agents)]
164
- if query.include_forwarded:
165
- agent_conditions.append(
166
- and_(
167
- Column("forwarded_from").isnot(None),
168
- expression.cast(Column("forwarded_from"), JSON).contains(list(query.agents)), # type: ignore
169
- )
170
- )
171
- conditions.append(or_(*agent_conditions))
169
+ conditions.append(Column("name").in_(query.agents))
172
170
  if query.since and (cutoff := query.get_time_cutoff()):
173
171
  conditions.append(Message.timestamp >= cutoff)
174
172
  if query.until:
@@ -222,6 +220,7 @@ def format_conversation(
222
220
  },
223
221
  cost=float(msg.cost_info.total_cost) if msg.cost_info else None,
224
222
  response_time=msg.response_time,
223
+ parent_id=msg.parent_id,
225
224
  )
226
225
  for msg in chat_messages
227
226
  ],
@@ -0,0 +1,16 @@
1
+ """Zed IDE storage provider.
2
+
3
+ This package implements a read-only storage backend that reads Zed IDE's
4
+ native thread format from ~/.local/share/zed/threads/threads.db.
5
+
6
+ Zed stores conversations as zstd-compressed JSON in a SQLite database.
7
+ This provider enables importing and analyzing Zed conversations.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from agentpool_storage.zed_provider.provider import ZedStorageProvider
13
+
14
+ __all__ = [
15
+ "ZedStorageProvider",
16
+ ]
@@ -0,0 +1,281 @@
1
+ """Helper functions for Zed storage provider.
2
+
3
+ Stateless conversion and utility functions for working with Zed format.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import base64
9
+ from datetime import datetime
10
+ import io
11
+ import json
12
+ from typing import Any
13
+
14
+ from pydantic_ai.messages import (
15
+ BinaryContent,
16
+ ModelRequest,
17
+ ModelResponse,
18
+ TextPart,
19
+ ThinkingPart,
20
+ ToolCallPart,
21
+ ToolReturnPart,
22
+ UserPromptPart,
23
+ )
24
+ from pydantic_ai.usage import RequestUsage
25
+ import zstandard
26
+
27
+ from agentpool.log import get_logger
28
+ from agentpool.messaging import ChatMessage
29
+ from agentpool.utils.now import get_now
30
+ from agentpool_storage.zed_provider.models import ZedThread
31
+
32
+
33
+ logger = get_logger(__name__)
34
+
35
+
36
+ def decompress_thread(data: bytes, data_type: str) -> ZedThread:
37
+ """Decompress and parse thread data.
38
+
39
+ Args:
40
+ data: Compressed thread data
41
+ data_type: Type of compression ("zstd" or plain)
42
+
43
+ Returns:
44
+ Parsed ZedThread object
45
+ """
46
+ if data_type == "zstd":
47
+ dctx = zstandard.ZstdDecompressor()
48
+ # Use stream_reader for data without content size in header
49
+ reader = dctx.stream_reader(io.BytesIO(data))
50
+ json_data = reader.read()
51
+ else:
52
+ json_data = data
53
+
54
+ thread_dict = json.loads(json_data)
55
+ return ZedThread.model_validate(thread_dict)
56
+
57
+
58
+ def parse_user_content( # noqa: PLR0915
59
+ content_list: list[dict[str, Any]],
60
+ ) -> tuple[str, list[str | BinaryContent]]:
61
+ """Parse user message content blocks.
62
+
63
+ Args:
64
+ content_list: List of content blocks from Zed user message
65
+
66
+ Returns:
67
+ Tuple of (display_text, pydantic_ai_content_list)
68
+ """
69
+ display_parts: list[str] = []
70
+ pydantic_content: list[str | BinaryContent] = []
71
+
72
+ for item in content_list:
73
+ if "Text" in item:
74
+ text = item["Text"]
75
+ display_parts.append(text)
76
+ pydantic_content.append(text)
77
+
78
+ elif "Image" in item:
79
+ image_data = item["Image"]
80
+ source = image_data.get("source", "")
81
+ try:
82
+ binary_data = base64.b64decode(source)
83
+ # Try to detect image type from magic bytes
84
+ media_type = "image/png" # default
85
+ if binary_data[:3] == b"\xff\xd8\xff":
86
+ media_type = "image/jpeg"
87
+ elif binary_data[:4] == b"\x89PNG":
88
+ media_type = "image/png"
89
+ elif binary_data[:6] in (b"GIF87a", b"GIF89a"):
90
+ media_type = "image/gif"
91
+ elif binary_data[:4] == b"RIFF" and binary_data[8:12] == b"WEBP":
92
+ media_type = "image/webp"
93
+
94
+ pydantic_content.append(BinaryContent(data=binary_data, media_type=media_type))
95
+ display_parts.append("[image]")
96
+ except (ValueError, TypeError, IndexError) as e:
97
+ logger.warning("Failed to decode image", error=str(e))
98
+ display_parts.append("[image decode error]")
99
+
100
+ elif "Mention" in item:
101
+ mention = item["Mention"]
102
+ uri = mention.get("uri", {})
103
+ content = mention.get("content", "")
104
+
105
+ # Format mention based on type
106
+ if "File" in uri:
107
+ file_info = uri["File"]
108
+ path = file_info.get("abs_path", "unknown")
109
+ formatted = f"[File: {path}]\n{content}"
110
+ elif "Directory" in uri:
111
+ dir_info = uri["Directory"]
112
+ path = dir_info.get("abs_path", "unknown")
113
+ formatted = f"[Directory: {path}]\n{content}"
114
+ elif "Symbol" in uri:
115
+ symbol_info = uri["Symbol"]
116
+ path = symbol_info.get("abs_path", "unknown")
117
+ name = symbol_info.get("name", "")
118
+ formatted = f"[Symbol: {name} in {path}]\n{content}"
119
+ elif "Selection" in uri:
120
+ sel_info = uri["Selection"]
121
+ path = sel_info.get("abs_path", "unknown")
122
+ formatted = f"[Selection: {path}]\n{content}"
123
+ elif "Fetch" in uri:
124
+ fetch_info = uri["Fetch"]
125
+ url = fetch_info.get("url", "unknown")
126
+ formatted = f"[Fetched: {url}]\n{content}"
127
+ else:
128
+ formatted = content
129
+
130
+ display_parts.append(formatted)
131
+ pydantic_content.append(formatted)
132
+
133
+ display_text = "\n".join(display_parts)
134
+ return display_text, pydantic_content
135
+
136
+
137
+ def parse_agent_content(
138
+ content_list: list[dict[str, Any]],
139
+ ) -> tuple[str, list[TextPart | ThinkingPart | ToolCallPart]]:
140
+ """Parse agent message content blocks.
141
+
142
+ Args:
143
+ content_list: List of content blocks from Zed agent message
144
+
145
+ Returns:
146
+ Tuple of (display_text, pydantic_ai_parts)
147
+ """
148
+ display_parts: list[str] = []
149
+ pydantic_parts: list[TextPart | ThinkingPart | ToolCallPart] = []
150
+
151
+ for item in content_list:
152
+ if "Text" in item:
153
+ text = item["Text"]
154
+ display_parts.append(text)
155
+ pydantic_parts.append(TextPart(content=text))
156
+
157
+ elif "Thinking" in item:
158
+ thinking = item["Thinking"]
159
+ text = thinking.get("text", "")
160
+ signature = thinking.get("signature")
161
+ display_parts.append(f"<thinking>\n{text}\n</thinking>")
162
+ pydantic_parts.append(ThinkingPart(content=text, signature=signature))
163
+
164
+ elif "ToolUse" in item:
165
+ tool_use = item["ToolUse"]
166
+ tool_id = tool_use.get("id", "")
167
+ tool_name = tool_use.get("name", "")
168
+ tool_input = tool_use.get("input", {})
169
+ display_parts.append(f"[Tool: {tool_name}]")
170
+ pydantic_parts.append(
171
+ ToolCallPart(tool_name=tool_name, args=tool_input, tool_call_id=tool_id)
172
+ )
173
+
174
+ display_text = "\n".join(display_parts)
175
+ return display_text, pydantic_parts
176
+
177
+
178
+ def parse_tool_results(tool_results: dict[str, Any]) -> list[ToolReturnPart]:
179
+ """Parse tool results into ToolReturnParts.
180
+
181
+ Args:
182
+ tool_results: Dictionary of tool results from Zed agent message
183
+
184
+ Returns:
185
+ List of ToolReturnPart objects
186
+ """
187
+ parts: list[ToolReturnPart] = []
188
+
189
+ for tool_id, result in tool_results.items():
190
+ if isinstance(result, dict):
191
+ tool_name = result.get("tool_name", "")
192
+ output = result.get("output", "")
193
+ content = result.get("content", {})
194
+
195
+ # Handle output being a dict like {"Text": "..."}
196
+ if isinstance(output, dict) and "Text" in output:
197
+ output = output["Text"]
198
+ # Extract text content if available
199
+ elif isinstance(content, dict) and "Text" in content:
200
+ output = content["Text"]
201
+ elif isinstance(content, str):
202
+ output = content
203
+
204
+ parts.append(
205
+ ToolReturnPart(tool_name=tool_name, content=output or "", tool_call_id=tool_id)
206
+ )
207
+
208
+ return parts
209
+
210
+
211
+ def thread_to_chat_messages(thread: ZedThread, thread_id: str) -> list[ChatMessage[str]]:
212
+ """Convert a Zed thread to ChatMessages.
213
+
214
+ Args:
215
+ thread: Zed thread object
216
+ thread_id: Thread identifier
217
+
218
+ Returns:
219
+ List of ChatMessage objects
220
+ """
221
+ messages: list[ChatMessage[str]] = []
222
+ try:
223
+ updated_at = datetime.fromisoformat(thread.updated_at.replace("Z", "+00:00"))
224
+ except (ValueError, AttributeError):
225
+ updated_at = get_now()
226
+ # Get model info
227
+ model_name = None
228
+ if thread.model:
229
+ model_name = f"{thread.model.provider}:{thread.model.model}"
230
+
231
+ for idx, msg in enumerate(thread.messages):
232
+ if msg == "Resume":
233
+ continue # Skip control messages
234
+ msg_id = f"{thread_id}_{idx}"
235
+
236
+ if msg.User is not None:
237
+ user_msg = msg.User
238
+ display_text, pydantic_content = parse_user_content(user_msg.content)
239
+ part = UserPromptPart(content=pydantic_content)
240
+ model_request = ModelRequest(parts=[part])
241
+
242
+ messages.append(
243
+ ChatMessage[str](
244
+ content=display_text,
245
+ conversation_id=thread_id,
246
+ role="user",
247
+ message_id=user_msg.id or msg_id,
248
+ name=None,
249
+ model_name=None,
250
+ timestamp=updated_at, # Zed doesn't store per-message timestamps
251
+ messages=[model_request],
252
+ )
253
+ )
254
+
255
+ elif msg.Agent is not None:
256
+ agent_msg = msg.Agent
257
+ display_text, pydantic_parts = parse_agent_content(agent_msg.content)
258
+ # Build ModelResponse
259
+ usage = RequestUsage()
260
+ model_response = ModelResponse(parts=pydantic_parts, usage=usage, model_name=model_name)
261
+ # Build tool return parts for next request if there are tool results
262
+ tool_return_parts = parse_tool_results(agent_msg.tool_results)
263
+ # Create the messages list - response, then optionally tool returns
264
+ pydantic_messages: list[ModelResponse | ModelRequest] = [model_response]
265
+ if tool_return_parts:
266
+ pydantic_messages.append(ModelRequest(parts=tool_return_parts))
267
+
268
+ messages.append(
269
+ ChatMessage[str](
270
+ content=display_text,
271
+ conversation_id=thread_id,
272
+ role="assistant",
273
+ message_id=msg_id,
274
+ name="zed",
275
+ model_name=model_name,
276
+ timestamp=updated_at,
277
+ messages=pydantic_messages,
278
+ )
279
+ )
280
+
281
+ return messages