agentpool 2.2.3__py3-none-any.whl → 2.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (250) hide show
  1. acp/__init__.py +0 -4
  2. acp/acp_requests.py +20 -77
  3. acp/agent/connection.py +8 -0
  4. acp/agent/implementations/debug_server/debug_server.py +6 -2
  5. acp/agent/protocol.py +6 -0
  6. acp/client/connection.py +38 -29
  7. acp/client/implementations/default_client.py +3 -2
  8. acp/client/implementations/headless_client.py +2 -2
  9. acp/connection.py +2 -2
  10. acp/notifications.py +18 -49
  11. acp/schema/__init__.py +2 -0
  12. acp/schema/agent_responses.py +21 -0
  13. acp/schema/client_requests.py +3 -3
  14. acp/schema/session_state.py +63 -29
  15. acp/task/supervisor.py +2 -2
  16. acp/utils.py +2 -2
  17. agentpool/__init__.py +2 -0
  18. agentpool/agents/acp_agent/acp_agent.py +278 -263
  19. agentpool/agents/acp_agent/acp_converters.py +150 -17
  20. agentpool/agents/acp_agent/client_handler.py +35 -24
  21. agentpool/agents/acp_agent/session_state.py +14 -6
  22. agentpool/agents/agent.py +471 -643
  23. agentpool/agents/agui_agent/agui_agent.py +104 -107
  24. agentpool/agents/agui_agent/helpers.py +3 -4
  25. agentpool/agents/base_agent.py +485 -32
  26. agentpool/agents/claude_code_agent/FORKING.md +191 -0
  27. agentpool/agents/claude_code_agent/__init__.py +13 -1
  28. agentpool/agents/claude_code_agent/claude_code_agent.py +654 -334
  29. agentpool/agents/claude_code_agent/converters.py +4 -141
  30. agentpool/agents/claude_code_agent/models.py +77 -0
  31. agentpool/agents/claude_code_agent/static_info.py +100 -0
  32. agentpool/agents/claude_code_agent/usage.py +242 -0
  33. agentpool/agents/events/__init__.py +22 -0
  34. agentpool/agents/events/builtin_handlers.py +65 -0
  35. agentpool/agents/events/event_emitter.py +3 -0
  36. agentpool/agents/events/events.py +84 -3
  37. agentpool/agents/events/infer_info.py +145 -0
  38. agentpool/agents/events/processors.py +254 -0
  39. agentpool/agents/interactions.py +41 -6
  40. agentpool/agents/modes.py +13 -0
  41. agentpool/agents/slashed_agent.py +5 -4
  42. agentpool/agents/tool_wrapping.py +18 -6
  43. agentpool/common_types.py +35 -21
  44. agentpool/config_resources/acp_assistant.yml +2 -2
  45. agentpool/config_resources/agents.yml +3 -0
  46. agentpool/config_resources/agents_template.yml +1 -0
  47. agentpool/config_resources/claude_code_agent.yml +9 -8
  48. agentpool/config_resources/external_acp_agents.yml +2 -1
  49. agentpool/delegation/base_team.py +4 -30
  50. agentpool/delegation/pool.py +104 -265
  51. agentpool/delegation/team.py +57 -57
  52. agentpool/delegation/teamrun.py +50 -55
  53. agentpool/functional/run.py +10 -4
  54. agentpool/mcp_server/client.py +73 -38
  55. agentpool/mcp_server/conversions.py +54 -13
  56. agentpool/mcp_server/manager.py +9 -23
  57. agentpool/mcp_server/registries/official_registry_client.py +10 -1
  58. agentpool/mcp_server/tool_bridge.py +114 -79
  59. agentpool/messaging/connection_manager.py +11 -10
  60. agentpool/messaging/event_manager.py +5 -5
  61. agentpool/messaging/message_container.py +6 -30
  62. agentpool/messaging/message_history.py +87 -8
  63. agentpool/messaging/messagenode.py +52 -14
  64. agentpool/messaging/messages.py +2 -26
  65. agentpool/messaging/processing.py +10 -22
  66. agentpool/models/__init__.py +1 -1
  67. agentpool/models/acp_agents/base.py +6 -2
  68. agentpool/models/acp_agents/mcp_capable.py +124 -15
  69. agentpool/models/acp_agents/non_mcp.py +0 -23
  70. agentpool/models/agents.py +66 -66
  71. agentpool/models/agui_agents.py +1 -1
  72. agentpool/models/claude_code_agents.py +111 -17
  73. agentpool/models/file_parsing.py +0 -1
  74. agentpool/models/manifest.py +70 -50
  75. agentpool/prompts/conversion_manager.py +1 -1
  76. agentpool/prompts/prompts.py +5 -2
  77. agentpool/resource_providers/__init__.py +2 -0
  78. agentpool/resource_providers/aggregating.py +4 -2
  79. agentpool/resource_providers/base.py +13 -3
  80. agentpool/resource_providers/codemode/code_executor.py +72 -5
  81. agentpool/resource_providers/codemode/helpers.py +2 -2
  82. agentpool/resource_providers/codemode/provider.py +64 -12
  83. agentpool/resource_providers/codemode/remote_mcp_execution.py +2 -2
  84. agentpool/resource_providers/codemode/remote_provider.py +9 -12
  85. agentpool/resource_providers/filtering.py +3 -1
  86. agentpool/resource_providers/mcp_provider.py +66 -12
  87. agentpool/resource_providers/plan_provider.py +111 -18
  88. agentpool/resource_providers/pool.py +5 -3
  89. agentpool/resource_providers/resource_info.py +111 -0
  90. agentpool/resource_providers/static.py +2 -2
  91. agentpool/sessions/__init__.py +2 -0
  92. agentpool/sessions/manager.py +2 -3
  93. agentpool/sessions/models.py +9 -6
  94. agentpool/sessions/protocol.py +28 -0
  95. agentpool/sessions/session.py +11 -55
  96. agentpool/storage/manager.py +361 -54
  97. agentpool/talk/registry.py +4 -4
  98. agentpool/talk/talk.py +9 -10
  99. agentpool/testing.py +1 -1
  100. agentpool/tool_impls/__init__.py +6 -0
  101. agentpool/tool_impls/agent_cli/__init__.py +42 -0
  102. agentpool/tool_impls/agent_cli/tool.py +95 -0
  103. agentpool/tool_impls/bash/__init__.py +64 -0
  104. agentpool/tool_impls/bash/helpers.py +35 -0
  105. agentpool/tool_impls/bash/tool.py +171 -0
  106. agentpool/tool_impls/delete_path/__init__.py +70 -0
  107. agentpool/tool_impls/delete_path/tool.py +142 -0
  108. agentpool/tool_impls/download_file/__init__.py +80 -0
  109. agentpool/tool_impls/download_file/tool.py +183 -0
  110. agentpool/tool_impls/execute_code/__init__.py +55 -0
  111. agentpool/tool_impls/execute_code/tool.py +163 -0
  112. agentpool/tool_impls/grep/__init__.py +80 -0
  113. agentpool/tool_impls/grep/tool.py +200 -0
  114. agentpool/tool_impls/list_directory/__init__.py +73 -0
  115. agentpool/tool_impls/list_directory/tool.py +197 -0
  116. agentpool/tool_impls/question/__init__.py +42 -0
  117. agentpool/tool_impls/question/tool.py +127 -0
  118. agentpool/tool_impls/read/__init__.py +104 -0
  119. agentpool/tool_impls/read/tool.py +305 -0
  120. agentpool/tools/__init__.py +2 -1
  121. agentpool/tools/base.py +114 -34
  122. agentpool/tools/manager.py +57 -1
  123. agentpool/ui/base.py +2 -2
  124. agentpool/ui/mock_provider.py +2 -2
  125. agentpool/ui/stdlib_provider.py +2 -2
  126. agentpool/utils/streams.py +21 -96
  127. agentpool/vfs_registry.py +7 -2
  128. {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/METADATA +16 -22
  129. {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/RECORD +242 -195
  130. {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/WHEEL +1 -1
  131. agentpool_cli/__main__.py +20 -0
  132. agentpool_cli/create.py +1 -1
  133. agentpool_cli/serve_acp.py +59 -1
  134. agentpool_cli/serve_opencode.py +1 -1
  135. agentpool_cli/ui.py +557 -0
  136. agentpool_commands/__init__.py +12 -5
  137. agentpool_commands/agents.py +1 -1
  138. agentpool_commands/pool.py +260 -0
  139. agentpool_commands/session.py +1 -1
  140. agentpool_commands/text_sharing/__init__.py +119 -0
  141. agentpool_commands/text_sharing/base.py +123 -0
  142. agentpool_commands/text_sharing/github_gist.py +80 -0
  143. agentpool_commands/text_sharing/opencode.py +462 -0
  144. agentpool_commands/text_sharing/paste_rs.py +59 -0
  145. agentpool_commands/text_sharing/pastebin.py +116 -0
  146. agentpool_commands/text_sharing/shittycodingagent.py +112 -0
  147. agentpool_commands/utils.py +31 -32
  148. agentpool_config/__init__.py +30 -2
  149. agentpool_config/agentpool_tools.py +498 -0
  150. agentpool_config/converters.py +1 -1
  151. agentpool_config/event_handlers.py +42 -0
  152. agentpool_config/events.py +1 -1
  153. agentpool_config/forward_targets.py +1 -4
  154. agentpool_config/jinja.py +3 -3
  155. agentpool_config/mcp_server.py +1 -5
  156. agentpool_config/nodes.py +1 -1
  157. agentpool_config/observability.py +44 -0
  158. agentpool_config/session.py +0 -3
  159. agentpool_config/storage.py +38 -39
  160. agentpool_config/task.py +3 -3
  161. agentpool_config/tools.py +11 -28
  162. agentpool_config/toolsets.py +22 -90
  163. agentpool_server/a2a_server/agent_worker.py +307 -0
  164. agentpool_server/a2a_server/server.py +23 -18
  165. agentpool_server/acp_server/acp_agent.py +125 -56
  166. agentpool_server/acp_server/commands/acp_commands.py +46 -216
  167. agentpool_server/acp_server/commands/docs_commands/fetch_repo.py +8 -7
  168. agentpool_server/acp_server/event_converter.py +651 -0
  169. agentpool_server/acp_server/input_provider.py +53 -10
  170. agentpool_server/acp_server/server.py +1 -11
  171. agentpool_server/acp_server/session.py +90 -410
  172. agentpool_server/acp_server/session_manager.py +8 -34
  173. agentpool_server/agui_server/server.py +3 -1
  174. agentpool_server/mcp_server/server.py +5 -2
  175. agentpool_server/opencode_server/ENDPOINTS.md +53 -14
  176. agentpool_server/opencode_server/OPENCODE_UI_TOOLS_COMPLETE.md +202 -0
  177. agentpool_server/opencode_server/__init__.py +0 -8
  178. agentpool_server/opencode_server/converters.py +132 -26
  179. agentpool_server/opencode_server/input_provider.py +160 -8
  180. agentpool_server/opencode_server/models/__init__.py +42 -20
  181. agentpool_server/opencode_server/models/app.py +12 -0
  182. agentpool_server/opencode_server/models/events.py +203 -29
  183. agentpool_server/opencode_server/models/mcp.py +19 -0
  184. agentpool_server/opencode_server/models/message.py +18 -1
  185. agentpool_server/opencode_server/models/parts.py +134 -1
  186. agentpool_server/opencode_server/models/question.py +56 -0
  187. agentpool_server/opencode_server/models/session.py +13 -1
  188. agentpool_server/opencode_server/routes/__init__.py +4 -0
  189. agentpool_server/opencode_server/routes/agent_routes.py +33 -2
  190. agentpool_server/opencode_server/routes/app_routes.py +66 -3
  191. agentpool_server/opencode_server/routes/config_routes.py +66 -5
  192. agentpool_server/opencode_server/routes/file_routes.py +184 -5
  193. agentpool_server/opencode_server/routes/global_routes.py +1 -1
  194. agentpool_server/opencode_server/routes/lsp_routes.py +1 -1
  195. agentpool_server/opencode_server/routes/message_routes.py +122 -66
  196. agentpool_server/opencode_server/routes/permission_routes.py +63 -0
  197. agentpool_server/opencode_server/routes/pty_routes.py +23 -22
  198. agentpool_server/opencode_server/routes/question_routes.py +128 -0
  199. agentpool_server/opencode_server/routes/session_routes.py +139 -68
  200. agentpool_server/opencode_server/routes/tui_routes.py +1 -1
  201. agentpool_server/opencode_server/server.py +47 -2
  202. agentpool_server/opencode_server/state.py +30 -0
  203. agentpool_storage/__init__.py +0 -4
  204. agentpool_storage/base.py +81 -2
  205. agentpool_storage/claude_provider/ARCHITECTURE.md +433 -0
  206. agentpool_storage/claude_provider/__init__.py +42 -0
  207. agentpool_storage/{claude_provider.py → claude_provider/provider.py} +190 -8
  208. agentpool_storage/file_provider.py +149 -15
  209. agentpool_storage/memory_provider.py +132 -12
  210. agentpool_storage/opencode_provider/ARCHITECTURE.md +386 -0
  211. agentpool_storage/opencode_provider/__init__.py +16 -0
  212. agentpool_storage/opencode_provider/helpers.py +414 -0
  213. agentpool_storage/opencode_provider/provider.py +895 -0
  214. agentpool_storage/session_store.py +20 -6
  215. agentpool_storage/sql_provider/sql_provider.py +135 -2
  216. agentpool_storage/sql_provider/utils.py +2 -12
  217. agentpool_storage/zed_provider/__init__.py +16 -0
  218. agentpool_storage/zed_provider/helpers.py +281 -0
  219. agentpool_storage/zed_provider/models.py +130 -0
  220. agentpool_storage/zed_provider/provider.py +442 -0
  221. agentpool_storage/zed_provider.py +803 -0
  222. agentpool_toolsets/__init__.py +0 -2
  223. agentpool_toolsets/builtin/__init__.py +2 -4
  224. agentpool_toolsets/builtin/code.py +4 -4
  225. agentpool_toolsets/builtin/debug.py +115 -40
  226. agentpool_toolsets/builtin/execution_environment.py +54 -165
  227. agentpool_toolsets/builtin/skills.py +0 -77
  228. agentpool_toolsets/builtin/subagent_tools.py +64 -51
  229. agentpool_toolsets/builtin/workers.py +4 -2
  230. agentpool_toolsets/composio_toolset.py +2 -2
  231. agentpool_toolsets/entry_points.py +3 -1
  232. agentpool_toolsets/fsspec_toolset/grep.py +25 -5
  233. agentpool_toolsets/fsspec_toolset/helpers.py +3 -2
  234. agentpool_toolsets/fsspec_toolset/toolset.py +350 -66
  235. agentpool_toolsets/mcp_discovery/data/mcp_servers.parquet +0 -0
  236. agentpool_toolsets/mcp_discovery/toolset.py +74 -17
  237. agentpool_toolsets/mcp_run_toolset.py +8 -11
  238. agentpool_toolsets/notifications.py +33 -33
  239. agentpool_toolsets/openapi.py +3 -1
  240. agentpool_toolsets/search_toolset.py +3 -1
  241. agentpool_config/resources.py +0 -33
  242. agentpool_server/acp_server/acp_tools.py +0 -43
  243. agentpool_server/acp_server/commands/spawn.py +0 -210
  244. agentpool_storage/opencode_provider.py +0 -730
  245. agentpool_storage/text_log_provider.py +0 -276
  246. agentpool_toolsets/builtin/chain.py +0 -288
  247. agentpool_toolsets/builtin/user_interaction.py +0 -52
  248. agentpool_toolsets/semantic_memory_toolset.py +0 -536
  249. {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/entry_points.txt +0 -0
  250. {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -5,10 +5,6 @@ from __future__ import annotations
5
5
  from datetime import timedelta
6
6
  from typing import TYPE_CHECKING, Any, Self
7
7
 
8
- from sqlalchemy import delete, select
9
- from sqlalchemy.ext.asyncio import AsyncSession
10
- from sqlmodel import SQLModel
11
-
12
8
  from agentpool.log import get_logger
13
9
  from agentpool.sessions.models import SessionData
14
10
  from agentpool.utils.now import get_now
@@ -45,6 +41,8 @@ class SQLSessionStore:
45
41
 
46
42
  async def __aenter__(self) -> Self:
47
43
  """Initialize database connection and create tables."""
44
+ from sqlmodel import SQLModel
45
+
48
46
  self._engine = self._config.get_engine()
49
47
 
50
48
  async with self._engine.begin() as conn:
@@ -98,7 +96,6 @@ class SQLSessionStore:
98
96
  project_id=data.project_id,
99
97
  parent_id=data.parent_id,
100
98
  version=data.version,
101
- title=data.title,
102
99
  cwd=data.cwd,
103
100
  created_at=data.created_at,
104
101
  last_active=data.last_active,
@@ -115,7 +112,6 @@ class SQLSessionStore:
115
112
  project_id=row.project_id,
116
113
  parent_id=row.parent_id,
117
114
  version=row.version,
118
- title=row.title,
119
115
  cwd=row.cwd,
120
116
  created_at=row.created_at,
121
117
  last_active=row.last_active,
@@ -130,6 +126,9 @@ class SQLSessionStore:
130
126
  Args:
131
127
  data: Session data to persist
132
128
  """
129
+ from sqlalchemy import delete
130
+ from sqlalchemy.ext.asyncio import AsyncSession
131
+
133
132
  engine = self._get_engine()
134
133
 
135
134
  async with AsyncSession(engine) as session:
@@ -152,6 +151,9 @@ class SQLSessionStore:
152
151
  Returns:
153
152
  Session data if found, None otherwise
154
153
  """
154
+ from sqlalchemy import select
155
+ from sqlalchemy.ext.asyncio import AsyncSession
156
+
155
157
  engine = self._get_engine()
156
158
 
157
159
  async with AsyncSession(engine) as session:
@@ -173,6 +175,9 @@ class SQLSessionStore:
173
175
  Returns:
174
176
  True if session was deleted, False if not found
175
177
  """
178
+ from sqlalchemy import delete
179
+ from sqlalchemy.ext.asyncio import AsyncSession
180
+
176
181
  engine = self._get_engine()
177
182
 
178
183
  async with AsyncSession(engine) as session:
@@ -199,6 +204,9 @@ class SQLSessionStore:
199
204
  Returns:
200
205
  List of session IDs
201
206
  """
207
+ from sqlalchemy import select
208
+ from sqlalchemy.ext.asyncio import AsyncSession
209
+
202
210
  engine = self._get_engine()
203
211
 
204
212
  async with AsyncSession(engine) as session:
@@ -223,6 +231,9 @@ class SQLSessionStore:
223
231
  Returns:
224
232
  Number of sessions removed
225
233
  """
234
+ from sqlalchemy import delete
235
+ from sqlalchemy.ext.asyncio import AsyncSession
236
+
226
237
  engine = self._get_engine()
227
238
  cutoff = get_now() - timedelta(hours=max_age_hours)
228
239
 
@@ -250,6 +261,9 @@ class SQLSessionStore:
250
261
  Returns:
251
262
  List of session data objects
252
263
  """
264
+ from sqlalchemy import select
265
+ from sqlalchemy.ext.asyncio import AsyncSession
266
+
253
267
  engine = self._get_engine()
254
268
 
255
269
  async with AsyncSession(engine) as session:
@@ -130,7 +130,6 @@ class SQLModelProvider(StorageProvider):
130
130
  cost_info: TokenCost | None = None,
131
131
  model: str | None = None,
132
132
  response_time: float | None = None,
133
- forwarded_from: list[str] | None = None,
134
133
  provider_name: str | None = None,
135
134
  provider_response_id: str | None = None,
136
135
  messages: str | None = None,
@@ -157,7 +156,6 @@ class SQLModelProvider(StorageProvider):
157
156
  input_tokens=cost_info.token_usage.input_tokens if cost_info else None,
158
157
  output_tokens=cost_info.token_usage.output_tokens if cost_info else None,
159
158
  cost=float(cost_info.total_cost) if cost_info else None,
160
- forwarded_from=forwarded_from,
161
159
  provider_name=provider_name,
162
160
  provider_response_id=provider_response_id,
163
161
  messages=messages,
@@ -210,6 +208,141 @@ class SQLModelProvider(StorageProvider):
210
208
  )
211
209
  return result.scalar_one_or_none()
212
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
+
213
346
  async def log_command(
214
347
  self,
215
348
  *,
@@ -8,8 +8,7 @@ from decimal import Decimal
8
8
  from typing import TYPE_CHECKING, Any
9
9
 
10
10
  from pydantic_ai import RunUsage
11
- from sqlalchemy import JSON, Column, and_, or_
12
- from sqlalchemy.sql import expression
11
+ from sqlalchemy import Column, and_
13
12
  from sqlmodel import select
14
13
 
15
14
  from agentpool.messaging import ChatMessage, TokenCost
@@ -68,7 +67,6 @@ def to_chat_message(db_message: Message) -> ChatMessage[str]:
68
67
  model_name=db_message.model,
69
68
  cost_info=cost_info,
70
69
  response_time=db_message.response_time,
71
- forwarded_from=db_message.forwarded_from or [],
72
70
  timestamp=db_message.timestamp,
73
71
  provider_name=db_message.provider_name,
74
72
  provider_response_id=db_message.provider_response_id,
@@ -168,15 +166,7 @@ def build_message_query(query: SessionQuery) -> SelectOfScalar[Any]:
168
166
  if query.name:
169
167
  conditions.append(Message.conversation_id == query.name)
170
168
  if query.agents:
171
- agent_conditions = [Column("name").in_(query.agents)]
172
- if query.include_forwarded:
173
- agent_conditions.append(
174
- and_(
175
- Column("forwarded_from").isnot(None),
176
- expression.cast(Column("forwarded_from"), JSON).contains(list(query.agents)), # type: ignore
177
- )
178
- )
179
- conditions.append(or_(*agent_conditions))
169
+ conditions.append(Column("name").in_(query.agents))
180
170
  if query.since and (cutoff := query.get_time_cutoff()):
181
171
  conditions.append(Message.timestamp >= cutoff)
182
172
  if query.until:
@@ -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