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,803 @@
1
+ """Zed IDE storage provider - reads from ~/.local/share/zed/threads format."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ from collections import defaultdict
7
+ from datetime import datetime
8
+ import io
9
+ import json
10
+ from pathlib import Path
11
+ import sqlite3
12
+ from typing import TYPE_CHECKING, Any, Literal
13
+
14
+ from pydantic import AliasChoices, BaseModel, Field
15
+ from pydantic_ai.messages import (
16
+ BinaryContent,
17
+ ModelRequest,
18
+ ModelResponse,
19
+ TextPart,
20
+ ThinkingPart,
21
+ ToolCallPart,
22
+ ToolReturnPart,
23
+ UserPromptPart,
24
+ )
25
+ from pydantic_ai.usage import RequestUsage
26
+ import zstandard
27
+
28
+ from agentpool.log import get_logger
29
+ from agentpool.messaging import ChatMessage
30
+ from agentpool.utils.now import get_now
31
+ from agentpool_storage.base import StorageProvider
32
+
33
+
34
+ if TYPE_CHECKING:
35
+ from collections.abc import Sequence
36
+
37
+ from pydantic_ai import FinishReason
38
+
39
+ from agentpool.messaging import TokenCost
40
+ from agentpool_config.session import SessionQuery
41
+ from agentpool_config.storage import ZedStorageConfig
42
+ from agentpool_storage.models import (
43
+ ConversationData,
44
+ MessageData,
45
+ QueryFilters,
46
+ StatsFilters,
47
+ TokenUsage,
48
+ )
49
+
50
+ logger = get_logger(__name__)
51
+
52
+
53
+ # Zed data models
54
+
55
+
56
+ class ZedMentionUri(BaseModel):
57
+ """Mention URI - can be File, Directory, Symbol, etc."""
58
+
59
+ File: dict[str, Any] | None = None
60
+ Directory: dict[str, Any] | None = None
61
+ Symbol: dict[str, Any] | None = None
62
+ Selection: dict[str, Any] | None = None
63
+ Thread: dict[str, Any] | None = None
64
+ TextThread: dict[str, Any] | None = None
65
+ Rule: dict[str, Any] | None = None
66
+ Fetch: dict[str, Any] | None = None
67
+ PastedImage: bool | None = None
68
+
69
+
70
+ class ZedMention(BaseModel):
71
+ """A file/symbol mention in Zed."""
72
+
73
+ uri: ZedMentionUri
74
+ content: str
75
+
76
+
77
+ class ZedImage(BaseModel):
78
+ """An image in Zed (base64 encoded)."""
79
+
80
+ source: str # base64 encoded
81
+
82
+
83
+ class ZedThinking(BaseModel):
84
+ """Thinking block from model."""
85
+
86
+ text: str
87
+ signature: str | None = None
88
+
89
+
90
+ class ZedToolUse(BaseModel):
91
+ """Tool use block."""
92
+
93
+ id: str
94
+ name: str
95
+ raw_input: str
96
+ input: dict[str, Any]
97
+ is_input_complete: bool = True
98
+ thought_signature: str | None = None
99
+
100
+
101
+ class ZedToolResult(BaseModel):
102
+ """Tool result."""
103
+
104
+ tool_use_id: str
105
+ tool_name: str
106
+ is_error: bool = False
107
+ content: dict[str, Any] | str | None = None
108
+ output: dict[str, Any] | str | None = None
109
+
110
+
111
+ class ZedUserMessage(BaseModel):
112
+ """User message in Zed thread."""
113
+
114
+ id: str
115
+ content: list[dict[str, Any]] # Can contain Text, Image, Mention
116
+
117
+
118
+ class ZedAgentMessage(BaseModel):
119
+ """Agent message in Zed thread."""
120
+
121
+ content: list[dict[str, Any]] # Can contain Text, Thinking, ToolUse
122
+ tool_results: dict[str, ZedToolResult] = Field(default_factory=dict)
123
+ reasoning_details: Any | None = None
124
+
125
+
126
+ class ZedMessage(BaseModel):
127
+ """A message in Zed thread - either User or Agent."""
128
+
129
+ User: ZedUserMessage | None = None
130
+ Agent: ZedAgentMessage | None = None
131
+
132
+
133
+ class ZedLanguageModel(BaseModel):
134
+ """Model configuration."""
135
+
136
+ provider: str
137
+ model: str
138
+
139
+
140
+ class ZedWorktreeSnapshot(BaseModel):
141
+ """Git worktree snapshot."""
142
+
143
+ worktree_path: str
144
+ git_state: dict[str, Any] | None = None
145
+
146
+
147
+ class ZedProjectSnapshot(BaseModel):
148
+ """Project snapshot with git state."""
149
+
150
+ worktree_snapshots: list[ZedWorktreeSnapshot] = Field(default_factory=list)
151
+ unsaved_buffer_paths: list[str] = Field(default_factory=list)
152
+ timestamp: str | None = None
153
+
154
+
155
+ class ZedThread(BaseModel):
156
+ """A Zed conversation thread."""
157
+
158
+ model_config = {"populate_by_name": True}
159
+
160
+ # v0.3.0 uses "title", v0.2.0 uses "summary"
161
+ title: str = Field(alias="title", validation_alias=AliasChoices("title", "summary"))
162
+ messages: list[ZedMessage | Literal["Resume"]] # Control messages
163
+ updated_at: str
164
+ version: str | None = None
165
+ detailed_summary: str | None = None # v0.3.0 field
166
+ detailed_summary_state: str | dict[str, Any] | None = None # v0.2.0 field
167
+ initial_project_snapshot: ZedProjectSnapshot | None = None
168
+ cumulative_token_usage: dict[str, int] = Field(default_factory=dict)
169
+ request_token_usage: list[dict[str, int]] | dict[str, dict[str, int]] = Field(
170
+ default_factory=list
171
+ ) # Can be list or dict depending on version
172
+ model: ZedLanguageModel | None = None
173
+ completion_mode: str | None = None
174
+ profile: str | None = None
175
+ exceeded_window_error: Any | None = None
176
+ tool_use_limit_reached: bool = False
177
+
178
+
179
+ def _decompress_thread(data: bytes, data_type: str) -> ZedThread:
180
+ """Decompress and parse thread data."""
181
+ if data_type == "zstd":
182
+ dctx = zstandard.ZstdDecompressor()
183
+ # Use stream_reader for data without content size in header
184
+ reader = dctx.stream_reader(io.BytesIO(data))
185
+ json_data = reader.read()
186
+ else:
187
+ json_data = data
188
+
189
+ thread_dict = json.loads(json_data)
190
+ return ZedThread.model_validate(thread_dict)
191
+
192
+
193
+ def _parse_user_content( # noqa: PLR0915
194
+ content_list: list[dict[str, Any]],
195
+ ) -> tuple[str, list[str | BinaryContent]]:
196
+ """Parse user message content blocks.
197
+
198
+ Returns:
199
+ Tuple of (display_text, pydantic_ai_content_list)
200
+ """
201
+ display_parts: list[str] = []
202
+ pydantic_content: list[str | BinaryContent] = []
203
+
204
+ for item in content_list:
205
+ if "Text" in item:
206
+ text = item["Text"]
207
+ display_parts.append(text)
208
+ pydantic_content.append(text)
209
+
210
+ elif "Image" in item:
211
+ image_data = item["Image"]
212
+ source = image_data.get("source", "")
213
+ try:
214
+ binary_data = base64.b64decode(source)
215
+ # Try to detect image type from magic bytes
216
+ media_type = "image/png" # default
217
+ if binary_data[:3] == b"\xff\xd8\xff":
218
+ media_type = "image/jpeg"
219
+ elif binary_data[:4] == b"\x89PNG":
220
+ media_type = "image/png"
221
+ elif binary_data[:6] in (b"GIF87a", b"GIF89a"):
222
+ media_type = "image/gif"
223
+ elif binary_data[:4] == b"RIFF" and binary_data[8:12] == b"WEBP":
224
+ media_type = "image/webp"
225
+
226
+ pydantic_content.append(BinaryContent(data=binary_data, media_type=media_type))
227
+ display_parts.append("[image]")
228
+ except (ValueError, TypeError, IndexError) as e:
229
+ logger.warning("Failed to decode image", error=str(e))
230
+ display_parts.append("[image decode error]")
231
+
232
+ elif "Mention" in item:
233
+ mention = item["Mention"]
234
+ uri = mention.get("uri", {})
235
+ content = mention.get("content", "")
236
+
237
+ # Format mention based on type
238
+ if "File" in uri:
239
+ file_info = uri["File"]
240
+ path = file_info.get("abs_path", "unknown")
241
+ formatted = f"[File: {path}]\n{content}"
242
+ elif "Directory" in uri:
243
+ dir_info = uri["Directory"]
244
+ path = dir_info.get("abs_path", "unknown")
245
+ formatted = f"[Directory: {path}]\n{content}"
246
+ elif "Symbol" in uri:
247
+ symbol_info = uri["Symbol"]
248
+ path = symbol_info.get("abs_path", "unknown")
249
+ name = symbol_info.get("name", "")
250
+ formatted = f"[Symbol: {name} in {path}]\n{content}"
251
+ elif "Selection" in uri:
252
+ sel_info = uri["Selection"]
253
+ path = sel_info.get("abs_path", "unknown")
254
+ formatted = f"[Selection: {path}]\n{content}"
255
+ elif "Fetch" in uri:
256
+ fetch_info = uri["Fetch"]
257
+ url = fetch_info.get("url", "unknown")
258
+ formatted = f"[Fetched: {url}]\n{content}"
259
+ else:
260
+ formatted = content
261
+
262
+ display_parts.append(formatted)
263
+ pydantic_content.append(formatted)
264
+
265
+ display_text = "\n".join(display_parts)
266
+ return display_text, pydantic_content
267
+
268
+
269
+ def _parse_agent_content(
270
+ content_list: list[dict[str, Any]],
271
+ ) -> tuple[str, list[TextPart | ThinkingPart | ToolCallPart]]:
272
+ """Parse agent message content blocks.
273
+
274
+ Returns:
275
+ Tuple of (display_text, pydantic_ai_parts)
276
+ """
277
+ display_parts: list[str] = []
278
+ pydantic_parts: list[TextPart | ThinkingPart | ToolCallPart] = []
279
+
280
+ for item in content_list:
281
+ if "Text" in item:
282
+ text = item["Text"]
283
+ display_parts.append(text)
284
+ pydantic_parts.append(TextPart(content=text))
285
+
286
+ elif "Thinking" in item:
287
+ thinking = item["Thinking"]
288
+ text = thinking.get("text", "")
289
+ signature = thinking.get("signature")
290
+ display_parts.append(f"<thinking>\n{text}\n</thinking>")
291
+ pydantic_parts.append(ThinkingPart(content=text, signature=signature))
292
+
293
+ elif "ToolUse" in item:
294
+ tool_use = item["ToolUse"]
295
+ tool_id = tool_use.get("id", "")
296
+ tool_name = tool_use.get("name", "")
297
+ tool_input = tool_use.get("input", {})
298
+ display_parts.append(f"[Tool: {tool_name}]")
299
+ pydantic_parts.append(
300
+ ToolCallPart(tool_name=tool_name, args=tool_input, tool_call_id=tool_id)
301
+ )
302
+
303
+ display_text = "\n".join(display_parts)
304
+ return display_text, pydantic_parts
305
+
306
+
307
+ def _parse_tool_results(tool_results: dict[str, Any]) -> list[ToolReturnPart]:
308
+ """Parse tool results into ToolReturnParts."""
309
+ parts: list[ToolReturnPart] = []
310
+
311
+ for tool_id, result in tool_results.items():
312
+ if isinstance(result, dict):
313
+ tool_name = result.get("tool_name", "")
314
+ output = result.get("output", "")
315
+ content = result.get("content", {})
316
+
317
+ # Handle output being a dict like {"Text": "..."}
318
+ if isinstance(output, dict) and "Text" in output:
319
+ output = output["Text"]
320
+ # Extract text content if available
321
+ elif isinstance(content, dict) and "Text" in content:
322
+ output = content["Text"]
323
+ elif isinstance(content, str):
324
+ output = content
325
+
326
+ parts.append(
327
+ ToolReturnPart(tool_name=tool_name, content=output or "", tool_call_id=tool_id)
328
+ )
329
+
330
+ return parts
331
+
332
+
333
+ def _thread_to_chat_messages(thread: ZedThread, thread_id: str) -> list[ChatMessage[str]]:
334
+ """Convert a Zed thread to ChatMessages."""
335
+ messages: list[ChatMessage[str]] = []
336
+ try:
337
+ updated_at = datetime.fromisoformat(thread.updated_at.replace("Z", "+00:00"))
338
+ except (ValueError, AttributeError):
339
+ updated_at = get_now()
340
+ # Get model info
341
+ model_name = None
342
+ if thread.model:
343
+ model_name = f"{thread.model.provider}:{thread.model.model}"
344
+
345
+ for idx, msg in enumerate(thread.messages):
346
+ if msg == "Resume":
347
+ continue # Skip control messages
348
+ msg_id = f"{thread_id}_{idx}"
349
+
350
+ if msg.User is not None:
351
+ user_msg = msg.User
352
+ display_text, pydantic_content = _parse_user_content(user_msg.content)
353
+ part = UserPromptPart(content=pydantic_content)
354
+ model_request = ModelRequest(parts=[part])
355
+
356
+ messages.append(
357
+ ChatMessage[str](
358
+ content=display_text,
359
+ conversation_id=thread_id,
360
+ role="user",
361
+ message_id=user_msg.id or msg_id,
362
+ name=None,
363
+ model_name=None,
364
+ timestamp=updated_at, # Zed doesn't store per-message timestamps
365
+ messages=[model_request],
366
+ )
367
+ )
368
+
369
+ elif msg.Agent is not None:
370
+ agent_msg = msg.Agent
371
+ display_text, pydantic_parts = _parse_agent_content(agent_msg.content)
372
+ # Build ModelResponse
373
+ usage = RequestUsage()
374
+ model_response = ModelResponse(parts=pydantic_parts, usage=usage, model_name=model_name)
375
+ # Build tool return parts for next request if there are tool results
376
+ tool_return_parts = _parse_tool_results(agent_msg.tool_results)
377
+ # Create the messages list - response, then optionally tool returns
378
+ pydantic_messages: list[ModelResponse | ModelRequest] = [model_response]
379
+ if tool_return_parts:
380
+ pydantic_messages.append(ModelRequest(parts=tool_return_parts))
381
+
382
+ messages.append(
383
+ ChatMessage[str](
384
+ content=display_text,
385
+ conversation_id=thread_id,
386
+ role="assistant",
387
+ message_id=msg_id,
388
+ name="zed",
389
+ model_name=model_name,
390
+ timestamp=updated_at,
391
+ messages=pydantic_messages,
392
+ )
393
+ )
394
+
395
+ return messages
396
+
397
+
398
+ class ZedStorageProvider(StorageProvider):
399
+ """Storage provider that reads Zed IDE's native thread format.
400
+
401
+ Zed stores conversations as zstd-compressed JSON in:
402
+ - ~/.local/share/zed/threads/threads.db (SQLite)
403
+
404
+ This is a READ-ONLY provider - it cannot write back to Zed's format.
405
+
406
+ ## Supported Zed content types:
407
+ - Text → str / TextPart
408
+ - Image → BinaryContent
409
+ - Mention (File, Directory, Symbol, Selection) → formatted str
410
+ - Thinking → ThinkingPart
411
+ - ToolUse → ToolCallPart
412
+ - tool_results → ToolReturnPart
413
+
414
+ ## Fields NOT available in Zed format:
415
+ - Per-message token costs (only cumulative per thread)
416
+ - Response time
417
+ - Parent message ID (flat structure)
418
+ - Forwarded from chain
419
+ - Finish reason
420
+ """
421
+
422
+ can_load_history = True
423
+
424
+ def __init__(self, config: ZedStorageConfig) -> None:
425
+ """Initialize Zed storage provider.
426
+
427
+ Args:
428
+ config: Configuration for the provider
429
+ """
430
+ super().__init__(config)
431
+ self.db_path = Path(config.path).expanduser()
432
+ if not self.db_path.name.endswith(".db"):
433
+ # If path is directory, add default db location
434
+ self.db_path = self.db_path / "threads" / "threads.db"
435
+
436
+ def _get_connection(self) -> sqlite3.Connection:
437
+ """Get a SQLite connection."""
438
+ if not self.db_path.exists():
439
+ msg = f"Zed threads database not found: {self.db_path}"
440
+ raise FileNotFoundError(msg)
441
+ return sqlite3.connect(self.db_path)
442
+
443
+ def _list_threads(self) -> list[tuple[str, str, str]]:
444
+ """List all threads.
445
+
446
+ Returns:
447
+ List of (id, summary, updated_at) tuples
448
+ """
449
+ try:
450
+ conn = self._get_connection()
451
+ query = "SELECT id, summary, updated_at FROM threads ORDER BY updated_at DESC"
452
+ cursor = conn.execute(query)
453
+ threads = cursor.fetchall()
454
+ conn.close()
455
+ except FileNotFoundError:
456
+ return []
457
+ except sqlite3.Error as e:
458
+ logger.warning("Failed to list Zed threads", error=str(e))
459
+ return []
460
+ else:
461
+ return threads
462
+
463
+ def _load_thread(self, thread_id: str) -> ZedThread | None:
464
+ """Load a single thread by ID."""
465
+ try:
466
+ conn = self._get_connection()
467
+ query = "SELECT data_type, data FROM threads WHERE id = ? LIMIT 1"
468
+ cursor = conn.execute(query, (thread_id,))
469
+ row = cursor.fetchone()
470
+ conn.close()
471
+ if row is None:
472
+ return None
473
+
474
+ data_type, data = row
475
+ return _decompress_thread(data, data_type)
476
+ except FileNotFoundError:
477
+ return None
478
+ except (sqlite3.Error, json.JSONDecodeError) as e:
479
+ logger.warning("Failed to load Zed thread", thread_id=thread_id, error=str(e))
480
+ return None
481
+
482
+ async def filter_messages(self, query: SessionQuery) -> list[ChatMessage[str]]:
483
+ """Filter messages based on query."""
484
+ messages: list[ChatMessage[str]] = []
485
+ for thread_id, summary, _updated_at in self._list_threads():
486
+ # Filter by conversation name if specified
487
+ if query.name and query.name not in (thread_id, summary):
488
+ continue
489
+
490
+ thread = self._load_thread(thread_id)
491
+ if thread is None:
492
+ continue
493
+ for msg in _thread_to_chat_messages(thread, thread_id):
494
+ # Apply filters
495
+ if query.agents and msg.name not in query.agents:
496
+ continue
497
+ cutoff = query.get_time_cutoff()
498
+ if query.since and cutoff and msg.timestamp and msg.timestamp < cutoff:
499
+ continue
500
+ if query.until and msg.timestamp:
501
+ until_dt = datetime.fromisoformat(query.until)
502
+ if msg.timestamp > until_dt:
503
+ continue
504
+ if query.contains and query.contains not in msg.content:
505
+ continue
506
+ if query.roles and msg.role not in query.roles:
507
+ continue
508
+ messages.append(msg)
509
+ if query.limit and len(messages) >= query.limit:
510
+ return messages
511
+
512
+ return messages
513
+
514
+ async def log_message(
515
+ self,
516
+ *,
517
+ message_id: str,
518
+ conversation_id: str,
519
+ content: str,
520
+ role: str,
521
+ name: str | None = None,
522
+ parent_id: str | None = None,
523
+ cost_info: TokenCost | None = None,
524
+ model: str | None = None,
525
+ response_time: float | None = None,
526
+ provider_name: str | None = None,
527
+ provider_response_id: str | None = None,
528
+ messages: str | None = None,
529
+ finish_reason: FinishReason | None = None,
530
+ ) -> None:
531
+ """Log a message - NOT SUPPORTED (read-only provider)."""
532
+ logger.warning("ZedStorageProvider is read-only, cannot log messages")
533
+
534
+ async def log_conversation(
535
+ self,
536
+ *,
537
+ conversation_id: str,
538
+ node_name: str,
539
+ start_time: datetime | None = None,
540
+ ) -> None:
541
+ """Log a conversation - NOT SUPPORTED (read-only provider)."""
542
+ logger.warning("ZedStorageProvider is read-only, cannot log conversations")
543
+
544
+ async def get_conversations(
545
+ self,
546
+ filters: QueryFilters,
547
+ ) -> list[tuple[ConversationData, Sequence[ChatMessage[str]]]]:
548
+ """Get filtered conversations with their messages."""
549
+ from agentpool_storage.models import ConversationData as ConvData
550
+
551
+ result: list[tuple[ConvData, Sequence[ChatMessage[str]]]] = []
552
+ for thread_id, summary, updated_at_str in self._list_threads():
553
+ thread = self._load_thread(thread_id)
554
+ if thread is None:
555
+ continue
556
+ # Parse timestamp
557
+ try:
558
+ updated_at = datetime.fromisoformat(updated_at_str.replace("Z", "+00:00"))
559
+ except (ValueError, AttributeError):
560
+ updated_at = get_now()
561
+ # Apply filters
562
+ if filters.since and updated_at < filters.since:
563
+ continue
564
+ messages = _thread_to_chat_messages(thread, thread_id)
565
+ if not messages:
566
+ continue
567
+ if filters.agent_name and not any(m.name == filters.agent_name for m in messages):
568
+ continue
569
+ if filters.query and not any(filters.query in m.content for m in messages):
570
+ continue
571
+ # Build MessageData list
572
+ msg_data_list: list[MessageData] = []
573
+ for msg in messages:
574
+ msg_data: MessageData = {
575
+ "role": msg.role,
576
+ "content": msg.content,
577
+ "timestamp": (msg.timestamp or get_now()).isoformat(),
578
+ "parent_id": msg.parent_id,
579
+ "model": msg.model_name,
580
+ "name": msg.name,
581
+ "token_usage": None,
582
+ "cost": None,
583
+ "response_time": None,
584
+ }
585
+ msg_data_list.append(msg_data)
586
+
587
+ # Get token usage from thread-level cumulative data
588
+ total_tokens = sum(thread.cumulative_token_usage.values())
589
+ token_usage_data: TokenUsage | None = (
590
+ {"total": total_tokens, "prompt": 0, "completion": 0} if total_tokens else None
591
+ )
592
+
593
+ conv_data = ConvData(
594
+ id=thread_id,
595
+ agent="zed",
596
+ title=summary or thread.title,
597
+ start_time=updated_at.isoformat(),
598
+ messages=msg_data_list,
599
+ token_usage=token_usage_data,
600
+ )
601
+
602
+ result.append((conv_data, messages))
603
+ if filters.limit and len(result) >= filters.limit:
604
+ break
605
+
606
+ return result
607
+
608
+ async def get_conversation_stats(self, filters: StatsFilters) -> dict[str, dict[str, Any]]:
609
+ """Get conversation statistics."""
610
+ stats: dict[str, dict[str, Any]] = defaultdict(
611
+ lambda: {"total_tokens": 0, "messages": 0, "models": set()}
612
+ )
613
+ for thread_id, _summary, updated_at_str in self._list_threads():
614
+ try:
615
+ timestamp = datetime.fromisoformat(updated_at_str.replace("Z", "+00:00"))
616
+ except (ValueError, AttributeError):
617
+ timestamp = get_now()
618
+ # Apply time filter
619
+ if timestamp < filters.cutoff:
620
+ continue
621
+ thread = self._load_thread(thread_id)
622
+ if thread is None:
623
+ continue
624
+ model_name = "unknown"
625
+ if thread.model:
626
+ model_name = f"{thread.model.provider}:{thread.model.model}"
627
+ total_tokens = sum(thread.cumulative_token_usage.values())
628
+ message_count = len(thread.messages)
629
+ # Group by specified criterion
630
+ match filters.group_by:
631
+ case "model":
632
+ key = model_name
633
+ case "hour":
634
+ key = timestamp.strftime("%Y-%m-%d %H:00")
635
+ case "day":
636
+ key = timestamp.strftime("%Y-%m-%d")
637
+ case _:
638
+ key = "zed" # Default agent grouping
639
+
640
+ stats[key]["messages"] += message_count
641
+ stats[key]["total_tokens"] += total_tokens
642
+ stats[key]["models"].add(model_name)
643
+
644
+ # Convert sets to lists for JSON serialization
645
+ for value in stats.values():
646
+ value["models"] = list(value["models"])
647
+
648
+ return dict(stats)
649
+
650
+ async def reset(self, *, agent_name: str | None = None, hard: bool = False) -> tuple[int, int]:
651
+ """Reset storage - NOT SUPPORTED (read-only provider)."""
652
+ logger.warning("ZedStorageProvider is read-only, cannot reset")
653
+ return 0, 0
654
+
655
+ async def get_conversation_counts(
656
+ self,
657
+ *,
658
+ agent_name: str | None = None,
659
+ ) -> tuple[int, int]:
660
+ """Get counts of conversations and messages."""
661
+ conv_count = 0
662
+ msg_count = 0
663
+ threads = self._list_threads()
664
+ for thread_id, _summary, _updated_at in threads:
665
+ thread = self._load_thread(thread_id)
666
+ if thread is None:
667
+ continue
668
+
669
+ conv_count += 1
670
+ msg_count += len(thread.messages)
671
+ return conv_count, msg_count
672
+
673
+ async def get_conversation_title(self, conversation_id: str) -> str | None:
674
+ """Get the title of a conversation."""
675
+ thread = self._load_thread(conversation_id)
676
+ if thread is None:
677
+ return None
678
+ return thread.title
679
+
680
+ async def get_conversation_messages(
681
+ self,
682
+ conversation_id: str,
683
+ *,
684
+ include_ancestors: bool = False,
685
+ ) -> list[ChatMessage[str]]:
686
+ """Get all messages for a conversation.
687
+
688
+ Args:
689
+ conversation_id: Thread ID (conversation ID in Zed format)
690
+ include_ancestors: If True, traverse parent_id chain to include
691
+ messages from ancestor conversations (not supported in Zed format)
692
+
693
+ Returns:
694
+ List of messages ordered by timestamp
695
+
696
+ Note:
697
+ Zed threads don't have parent_id chain, so include_ancestors has no effect.
698
+ """
699
+ thread = self._load_thread(conversation_id)
700
+ if thread is None:
701
+ return []
702
+
703
+ messages = _thread_to_chat_messages(thread, conversation_id)
704
+ # Sort by timestamp (though they should already be in order)
705
+ messages.sort(key=lambda m: m.timestamp or get_now())
706
+ return messages
707
+
708
+ async def get_message(self, message_id: str) -> ChatMessage[str] | None:
709
+ """Get a single message by ID.
710
+
711
+ Args:
712
+ message_id: ID of the message
713
+
714
+ Returns:
715
+ The message if found, None otherwise
716
+
717
+ Note:
718
+ Zed doesn't store individual message IDs, so this searches all threads.
719
+ This is inefficient for large datasets.
720
+ """
721
+ for thread_id, _summary, _updated_at in self._list_threads():
722
+ thread = self._load_thread(thread_id)
723
+ if thread is None:
724
+ continue
725
+ messages = _thread_to_chat_messages(thread, thread_id)
726
+ for msg in messages:
727
+ if msg.message_id == message_id:
728
+ return msg
729
+ return None
730
+
731
+ async def get_message_ancestry(self, message_id: str) -> list[ChatMessage[str]]:
732
+ """Get the ancestry chain of a message.
733
+
734
+ Args:
735
+ message_id: ID of the message
736
+
737
+ Returns:
738
+ List of messages from oldest ancestor to the specified message
739
+
740
+ Note:
741
+ Zed threads don't support parent_id chains, so this only returns
742
+ the single message if found.
743
+ """
744
+ msg = await self.get_message(message_id)
745
+ return [msg] if msg else []
746
+
747
+ async def fork_conversation(
748
+ self,
749
+ *,
750
+ source_conversation_id: str,
751
+ new_conversation_id: str,
752
+ fork_from_message_id: str | None = None,
753
+ new_agent_name: str | None = None,
754
+ ) -> str | None:
755
+ """Fork a conversation at a specific point.
756
+
757
+ Args:
758
+ source_conversation_id: Source thread ID
759
+ new_conversation_id: New thread ID
760
+ fork_from_message_id: Message ID to fork from (not used - Zed is read-only)
761
+ new_agent_name: Not used in Zed format
762
+
763
+ Returns:
764
+ None, as Zed storage is read-only
765
+
766
+ Note:
767
+ This is a READ-ONLY provider. Forking creates no persistent state.
768
+ Returns None to indicate no fork point is available.
769
+ """
770
+ msg = "Fork conversation not supported for Zed storage (read-only)"
771
+ logger.warning(msg, source=source_conversation_id, new=new_conversation_id)
772
+ return None
773
+
774
+
775
+ if __name__ == "__main__":
776
+ import asyncio
777
+ import datetime as dt
778
+
779
+ from agentpool_config.storage import ZedStorageConfig
780
+ from agentpool_storage.models import QueryFilters, StatsFilters
781
+
782
+ async def main() -> None:
783
+ config = ZedStorageConfig()
784
+ provider = ZedStorageProvider(config)
785
+ print(f"Database: {provider.db_path}")
786
+ print(f"Exists: {provider.db_path.exists()}")
787
+ # List conversations
788
+ filters = QueryFilters(limit=10)
789
+ conversations = await provider.get_conversations(filters)
790
+ print(f"\nFound {len(conversations)} conversations")
791
+ for conv_data, messages in conversations[:5]:
792
+ print(f" - {conv_data['id'][:8]}... | {conv_data['title'] or 'Untitled'}")
793
+ print(f" Messages: {len(messages)}, Updated: {conv_data['start_time']}")
794
+ # Get counts
795
+ conv_count, msg_count = await provider.get_conversation_counts()
796
+ print(f"\nTotal: {conv_count} conversations, {msg_count} messages")
797
+ # Get stats
798
+ cutoff = dt.datetime.now(dt.UTC) - dt.timedelta(days=30)
799
+ stats_filters = StatsFilters(cutoff=cutoff, group_by="day")
800
+ stats = await provider.get_conversation_stats(stats_filters)
801
+ print(f"\nStats: {stats}")
802
+
803
+ asyncio.run(main())