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
@@ -1,730 +0,0 @@
1
- """OpenCode storage provider - reads/writes to ~/.local/share/opencode format."""
2
-
3
- from __future__ import annotations
4
-
5
- from collections import defaultdict
6
- from datetime import UTC, datetime
7
- from decimal import Decimal
8
- from pathlib import Path
9
- from typing import TYPE_CHECKING, Annotated, Any, Literal
10
-
11
- import anyenv
12
- from pydantic import BaseModel, ConfigDict, Field, TypeAdapter
13
- from pydantic_ai import RunUsage
14
- from pydantic_ai.messages import (
15
- ModelRequest,
16
- ModelResponse,
17
- TextPart,
18
- ThinkingPart,
19
- ToolCallPart,
20
- ToolReturnPart,
21
- UserPromptPart,
22
- )
23
- from pydantic_ai.usage import RequestUsage
24
-
25
- from agentpool.log import get_logger
26
- from agentpool.messaging import ChatMessage, TokenCost
27
- from agentpool.utils.now import get_now
28
- from agentpool_storage.base import StorageProvider
29
- from agentpool_storage.models import TokenUsage
30
-
31
-
32
- if TYPE_CHECKING:
33
- from collections.abc import Sequence
34
-
35
- from agentpool_config.session import SessionQuery
36
- from agentpool_config.storage import OpenCodeStorageConfig
37
- from agentpool_storage.models import (
38
- ConversationData,
39
- MessageData,
40
- QueryFilters,
41
- StatsFilters,
42
- )
43
-
44
- logger = get_logger(__name__)
45
-
46
-
47
- # OpenCode data models
48
-
49
- PartType = Literal["text", "step-start", "step-finish", "reasoning", "tool", "patch", "compaction"]
50
- ToolStatus = Literal["pending", "running", "completed", "error"]
51
- FinishReason = Literal["stop", "tool-calls", "length", "error"]
52
-
53
-
54
- class OpenCodeTime(BaseModel):
55
- """Timestamp fields used in OpenCode."""
56
-
57
- created: int # Unix timestamp in milliseconds
58
- updated: int | None = None
59
- completed: int | None = None
60
-
61
-
62
- class OpenCodeSummary(BaseModel):
63
- """Summary information for sessions/messages."""
64
-
65
- additions: int | None = None
66
- deletions: int | None = None
67
- files: int = 0
68
- title: str | None = None
69
- diffs: list[Any] = Field(default_factory=list)
70
-
71
-
72
- class OpenCodeModel(BaseModel):
73
- """Model information in messages."""
74
-
75
- provider_id: str = Field(alias="providerID")
76
- model_id: str = Field(alias="modelID")
77
-
78
- model_config = ConfigDict(populate_by_name=True)
79
-
80
-
81
- class OpenCodePath(BaseModel):
82
- """Path context in messages."""
83
-
84
- cwd: str
85
- root: str
86
-
87
-
88
- class OpenCodeTokens(BaseModel):
89
- """Token usage information."""
90
-
91
- input: int = 0
92
- output: int = 0
93
- reasoning: int = 0
94
- cache: dict[str, int] = Field(default_factory=dict)
95
-
96
-
97
- class OpenCodeSession(BaseModel):
98
- """OpenCode session metadata."""
99
-
100
- id: str
101
- version: str
102
- project_id: str = Field(alias="projectID")
103
- directory: str
104
- title: str
105
- time: OpenCodeTime
106
- summary: OpenCodeSummary = Field(default_factory=OpenCodeSummary)
107
-
108
- model_config = ConfigDict(populate_by_name=True)
109
-
110
-
111
- class OpenCodeMessage(BaseModel):
112
- """OpenCode message metadata."""
113
-
114
- id: str
115
- session_id: str = Field(alias="sessionID")
116
- role: Literal["user", "assistant"]
117
- time: OpenCodeTime
118
- summary: OpenCodeSummary | bool | None = None
119
- agent: str | None = None
120
- model: OpenCodeModel | None = None
121
- parent_id: str | None = Field(default=None, alias="parentID")
122
- model_id: str | None = Field(default=None, alias="modelID")
123
- provider_id: str | None = Field(default=None, alias="providerID")
124
- mode: str | None = None
125
- path: OpenCodePath | None = None
126
- cost: float | None = None
127
- tokens: OpenCodeTokens | None = None
128
- finish: FinishReason | None = None
129
-
130
- model_config = ConfigDict(populate_by_name=True)
131
-
132
-
133
- class OpenCodeToolState(BaseModel):
134
- """Tool execution state."""
135
-
136
- status: ToolStatus
137
- input: dict[str, Any] | None = None
138
- output: str | None = None
139
- title: str | None = None
140
- metadata: dict[str, Any] | None = None
141
- time: dict[str, int] | None = None
142
-
143
-
144
- class OpenCodePartBase(BaseModel):
145
- """Base for all part types."""
146
-
147
- id: str
148
- session_id: str = Field(alias="sessionID")
149
- message_id: str = Field(alias="messageID")
150
- type: PartType
151
-
152
- model_config = ConfigDict(populate_by_name=True)
153
-
154
-
155
- class OpenCodeTextPart(OpenCodePartBase):
156
- """Text content part."""
157
-
158
- type: Literal["text"]
159
- text: str
160
- time: dict[str, int] | None = None
161
-
162
-
163
- class OpenCodeReasoningPart(OpenCodePartBase):
164
- """Reasoning/thinking content part."""
165
-
166
- type: Literal["reasoning"]
167
- text: str
168
- time: dict[str, int] | None = None
169
-
170
-
171
- class OpenCodeToolPart(OpenCodePartBase):
172
- """Tool call/result part."""
173
-
174
- type: Literal["tool"]
175
- call_id: str = Field(alias="callID")
176
- tool: str
177
- state: OpenCodeToolState
178
-
179
-
180
- class OpenCodeStepStartPart(OpenCodePartBase):
181
- """Step start marker."""
182
-
183
- type: Literal["step-start"]
184
-
185
-
186
- class OpenCodeStepFinishPart(OpenCodePartBase):
187
- """Step finish marker with stats."""
188
-
189
- type: Literal["step-finish"]
190
- reason: FinishReason | None = None
191
- cost: float | None = None
192
- tokens: OpenCodeTokens | None = None
193
-
194
-
195
- class OpenCodePatchPart(OpenCodePartBase):
196
- """File patch/diff part."""
197
-
198
- type: Literal["patch"]
199
- hash: str | None = None
200
- files: list[str] = Field(default_factory=list)
201
-
202
-
203
- class OpenCodeCompactionPart(OpenCodePartBase):
204
- """Compaction marker."""
205
-
206
- type: Literal["compaction"]
207
-
208
-
209
- # Discriminated union for all part types
210
- OpenCodePart = Annotated[
211
- OpenCodeTextPart
212
- | OpenCodeReasoningPart
213
- | OpenCodeToolPart
214
- | OpenCodeStepStartPart
215
- | OpenCodeStepFinishPart
216
- | OpenCodePatchPart
217
- | OpenCodeCompactionPart,
218
- Field(discriminator="type"),
219
- ]
220
-
221
-
222
- class OpenCodeStorageProvider(StorageProvider):
223
- """Storage provider that reads/writes OpenCode's native format.
224
-
225
- OpenCode stores data in:
226
- - ~/.local/share/opencode/storage/session/{project_id}/ - Session JSON files
227
- - ~/.local/share/opencode/storage/message/{session_id}/ - Message JSON files
228
- - ~/.local/share/opencode/storage/part/{message_id}/ - Part JSON files
229
-
230
- Each file is a single JSON object (not JSONL).
231
- """
232
-
233
- can_load_history = True
234
-
235
- def __init__(self, config: OpenCodeStorageConfig) -> None:
236
- """Initialize OpenCode storage provider."""
237
- super().__init__(config)
238
- self.base_path = Path(config.path).expanduser()
239
- self.sessions_path = self.base_path / "session"
240
- self.messages_path = self.base_path / "message"
241
- self.parts_path = self.base_path / "part"
242
-
243
- def _ms_to_datetime(self, ms: int) -> datetime:
244
- """Convert milliseconds timestamp to datetime."""
245
- return datetime.fromtimestamp(ms / 1000, tz=UTC)
246
-
247
- def _list_sessions(self, project_id: str | None = None) -> list[tuple[str, Path]]:
248
- """List all sessions, optionally filtered by project."""
249
- sessions: list[tuple[str, Path]] = []
250
- if not self.sessions_path.exists():
251
- return sessions
252
-
253
- if project_id:
254
- project_dir = self.sessions_path / project_id
255
- if project_dir.exists():
256
- sessions.extend((f.stem, f) for f in project_dir.glob("*.json"))
257
- else:
258
- for project_dir in self.sessions_path.iterdir():
259
- if project_dir.is_dir():
260
- sessions.extend((f.stem, f) for f in project_dir.glob("*.json"))
261
- return sessions
262
-
263
- def _read_session(self, session_path: Path) -> OpenCodeSession | None:
264
- """Read session metadata."""
265
- if not session_path.exists():
266
- return None
267
- try:
268
- content = session_path.read_text(encoding="utf-8")
269
- return anyenv.load_json(content, return_type=OpenCodeSession)
270
- except anyenv.JsonLoadError as e:
271
- logger.warning("Failed to parse session", path=str(session_path), error=str(e))
272
- return None
273
-
274
- def _read_messages(self, session_id: str) -> list[OpenCodeMessage]:
275
- """Read all messages for a session."""
276
- messages: list[OpenCodeMessage] = []
277
- msg_dir = self.messages_path / session_id
278
- if not msg_dir.exists():
279
- return messages
280
-
281
- for msg_file in sorted(msg_dir.glob("*.json")):
282
- try:
283
- content = msg_file.read_text(encoding="utf-8")
284
- data = anyenv.load_json(content, return_type=OpenCodeMessage)
285
- messages.append(data)
286
- except anyenv.JsonLoadError as e:
287
- logger.warning("Failed to parse message", path=str(msg_file), error=str(e))
288
- return messages
289
-
290
- def _read_parts(self, message_id: str) -> list[OpenCodePart]:
291
- """Read all parts for a message."""
292
- parts: list[OpenCodePart] = []
293
- parts_dir = self.parts_path / message_id
294
- if not parts_dir.exists():
295
- return parts
296
-
297
- adapter = TypeAdapter[Any](OpenCodePart)
298
- for part_file in sorted(parts_dir.glob("*.json")):
299
- try:
300
- content = part_file.read_text(encoding="utf-8")
301
- data = anyenv.load_json(content)
302
- parts.append(adapter.validate_python(data))
303
- except anyenv.JsonLoadError as e:
304
- logger.warning("Failed to parse part", path=str(part_file), error=str(e))
305
- return parts
306
-
307
- def _build_tool_id_mapping(self, parts: list[OpenCodePart]) -> dict[str, str]:
308
- """Build mapping from tool callID to tool name."""
309
- mapping: dict[str, str] = {}
310
- for part in parts:
311
- if isinstance(part, OpenCodeToolPart):
312
- mapping[part.call_id] = part.tool
313
- return mapping
314
-
315
- def _message_to_chat_message(
316
- self,
317
- msg: OpenCodeMessage,
318
- parts: list[OpenCodePart],
319
- conversation_id: str,
320
- tool_id_mapping: dict[str, str] | None = None,
321
- ) -> ChatMessage[str]:
322
- """Convert OpenCode message + parts to ChatMessage."""
323
- timestamp = self._ms_to_datetime(msg.time.created)
324
-
325
- # Extract text content for display
326
- content = self._extract_text_content(parts)
327
-
328
- # Build pydantic-ai messages
329
- pydantic_messages = self._build_pydantic_messages(
330
- msg, parts, timestamp, tool_id_mapping or {}
331
- )
332
-
333
- # Extract cost info
334
- cost_info = None
335
- if msg.tokens:
336
- input_tokens = msg.tokens.input + msg.tokens.cache.get("read", 0)
337
- output_tokens = msg.tokens.output
338
- if input_tokens or output_tokens:
339
- cost_info = TokenCost(
340
- token_usage=RunUsage(
341
- input_tokens=input_tokens,
342
- output_tokens=output_tokens,
343
- ),
344
- total_cost=Decimal(str(msg.cost)) if msg.cost else Decimal(0),
345
- )
346
-
347
- # Get model name
348
- model_name = msg.model_id
349
- if not model_name and msg.model:
350
- model_name = msg.model.model_id
351
-
352
- return ChatMessage[str](
353
- content=content,
354
- conversation_id=conversation_id,
355
- role=msg.role,
356
- message_id=msg.id,
357
- name=msg.agent,
358
- model_name=model_name,
359
- cost_info=cost_info,
360
- timestamp=timestamp,
361
- parent_id=msg.parent_id,
362
- messages=pydantic_messages,
363
- provider_details={"finish_reason": msg.finish} if msg.finish else {},
364
- )
365
-
366
- def _extract_text_content(self, parts: list[OpenCodePart]) -> str:
367
- """Extract text content from parts for display."""
368
- text_parts: list[str] = []
369
- for part in parts:
370
- if isinstance(part, OpenCodeTextPart) and part.text:
371
- text_parts.append(part.text)
372
- elif isinstance(part, OpenCodeReasoningPart) and part.text:
373
- text_parts.append(f"<thinking>\n{part.text}\n</thinking>")
374
- return "\n".join(text_parts)
375
-
376
- def _build_pydantic_messages(
377
- self,
378
- msg: OpenCodeMessage,
379
- parts: list[OpenCodePart],
380
- timestamp: datetime,
381
- tool_id_mapping: dict[str, str],
382
- ) -> list[ModelRequest | ModelResponse]:
383
- """Build pydantic-ai ModelRequest and/or ModelResponse.
384
-
385
- In OpenCode's model, assistant messages contain both tool calls AND their
386
- results in the same message. We split these into:
387
- - ModelResponse with ToolCallPart (the call)
388
- - ModelRequest with ToolReturnPart (the result)
389
- """
390
- result: list[ModelRequest | ModelResponse] = []
391
-
392
- if msg.role == "user":
393
- request_parts: list[UserPromptPart | ToolReturnPart] = [
394
- UserPromptPart(content=part.text, timestamp=timestamp)
395
- for part in parts
396
- if isinstance(part, OpenCodeTextPart) and part.text
397
- ]
398
- if request_parts:
399
- result.append(ModelRequest(parts=request_parts, timestamp=timestamp))
400
- return result
401
-
402
- # Assistant message - may contain both tool calls and results
403
- response_parts: list[TextPart | ToolCallPart | ThinkingPart] = []
404
- tool_return_parts: list[ToolReturnPart] = []
405
-
406
- # Build usage
407
- usage = RequestUsage()
408
- if msg.tokens:
409
- usage = RequestUsage(
410
- input_tokens=msg.tokens.input,
411
- output_tokens=msg.tokens.output,
412
- cache_read_tokens=msg.tokens.cache.get("read", 0),
413
- cache_write_tokens=msg.tokens.cache.get("write", 0),
414
- )
415
-
416
- for part in parts:
417
- if isinstance(part, OpenCodeTextPart) and part.text:
418
- response_parts.append(TextPart(content=part.text))
419
- elif isinstance(part, OpenCodeReasoningPart) and part.text:
420
- response_parts.append(ThinkingPart(content=part.text))
421
- elif isinstance(part, OpenCodeToolPart):
422
- # Add tool call to response
423
- args = part.state.input or {}
424
- tc_part = ToolCallPart(tool_name=part.tool, args=args, tool_call_id=part.call_id)
425
- response_parts.append(tc_part)
426
- # If completed, also create a tool return
427
- if part.state.status == "completed" and part.state.output:
428
- return_part = ToolReturnPart(
429
- tool_name=part.tool,
430
- content=part.state.output,
431
- tool_call_id=part.call_id,
432
- timestamp=timestamp,
433
- )
434
- tool_return_parts.append(return_part)
435
-
436
- # Add the response if we have parts
437
- if response_parts:
438
- model_name = msg.model_id or (msg.model.model_id if msg.model else None)
439
- result.append(
440
- ModelResponse(
441
- parts=response_parts,
442
- usage=usage,
443
- model_name=model_name,
444
- timestamp=timestamp,
445
- )
446
- )
447
-
448
- # Add tool returns as a separate request (simulating user sending results back)
449
- if tool_return_parts:
450
- result.append(ModelRequest(parts=tool_return_parts, timestamp=timestamp))
451
-
452
- return result
453
-
454
- async def filter_messages(self, query: SessionQuery) -> list[ChatMessage[str]]:
455
- """Filter messages based on query."""
456
- messages: list[ChatMessage[str]] = []
457
- sessions = self._list_sessions()
458
-
459
- for session_id, session_path in sessions:
460
- if query.name and session_id != query.name:
461
- continue
462
-
463
- session = self._read_session(session_path)
464
- if not session:
465
- continue
466
-
467
- oc_messages = self._read_messages(session_id)
468
-
469
- # Build tool mapping from all parts
470
- all_parts: list[OpenCodePart] = []
471
- msg_parts_map: dict[str, list[OpenCodePart]] = {}
472
- for oc_msg in oc_messages:
473
- parts = self._read_parts(oc_msg.id)
474
- msg_parts_map[oc_msg.id] = parts
475
- all_parts.extend(parts)
476
- tool_mapping = self._build_tool_id_mapping(all_parts)
477
-
478
- for oc_msg in oc_messages:
479
- parts = msg_parts_map.get(oc_msg.id, [])
480
- chat_msg = self._message_to_chat_message(oc_msg, parts, session_id, tool_mapping)
481
-
482
- # Apply filters
483
- if query.agents and chat_msg.name not in query.agents:
484
- continue
485
- cutoff = query.get_time_cutoff()
486
- if query.since and cutoff and chat_msg.timestamp < cutoff:
487
- continue
488
- if query.until:
489
- until_dt = datetime.fromisoformat(query.until)
490
- if chat_msg.timestamp > until_dt:
491
- continue
492
- if query.contains and query.contains not in chat_msg.content:
493
- continue
494
- if query.roles and chat_msg.role not in query.roles:
495
- continue
496
- messages.append(chat_msg)
497
-
498
- if query.limit and len(messages) >= query.limit:
499
- return messages
500
-
501
- return messages
502
-
503
- async def log_message(
504
- self,
505
- *,
506
- message_id: str,
507
- conversation_id: str,
508
- content: str,
509
- role: str,
510
- name: str | None = None,
511
- parent_id: str | None = None,
512
- cost_info: TokenCost | None = None,
513
- model: str | None = None,
514
- response_time: float | None = None,
515
- forwarded_from: list[str] | None = None,
516
- provider_name: str | None = None,
517
- provider_response_id: str | None = None,
518
- messages: str | None = None,
519
- finish_reason: Any | None = None,
520
- ) -> None:
521
- """Log a message to OpenCode format.
522
-
523
- Note: Writing to OpenCode format is not fully implemented.
524
- """
525
- logger.warning("Writing to OpenCode format is not fully supported")
526
-
527
- async def log_conversation(
528
- self,
529
- *,
530
- conversation_id: str,
531
- node_name: str,
532
- start_time: datetime | None = None,
533
- ) -> None:
534
- """Log a conversation start."""
535
- # No-op for read-only provider
536
-
537
- async def get_conversations(
538
- self,
539
- filters: QueryFilters,
540
- ) -> list[tuple[ConversationData, Sequence[ChatMessage[str]]]]:
541
- """Get filtered conversations with their messages."""
542
- from agentpool_storage.models import ConversationData as ConvData
543
-
544
- result: list[tuple[ConvData, Sequence[ChatMessage[str]]]] = []
545
- sessions = self._list_sessions()
546
- for session_id, session_path in sessions:
547
- session = self._read_session(session_path)
548
- if not session:
549
- continue
550
-
551
- oc_messages = self._read_messages(session_id)
552
- if not oc_messages:
553
- continue
554
-
555
- # Build tool mapping
556
- all_parts: list[OpenCodePart] = []
557
- msg_parts_map: dict[str, list[OpenCodePart]] = {}
558
- for oc_msg in oc_messages:
559
- parts = self._read_parts(oc_msg.id)
560
- msg_parts_map[oc_msg.id] = parts
561
- all_parts.extend(parts)
562
- tool_mapping = self._build_tool_id_mapping(all_parts)
563
-
564
- # Convert messages
565
- chat_messages: list[ChatMessage[str]] = []
566
- total_tokens = 0
567
- total_cost = 0.0
568
-
569
- for oc_msg in oc_messages:
570
- parts = msg_parts_map.get(oc_msg.id, [])
571
- chat_msg = self._message_to_chat_message(oc_msg, parts, session_id, tool_mapping)
572
- chat_messages.append(chat_msg)
573
-
574
- if oc_msg.tokens:
575
- total_tokens += oc_msg.tokens.input + oc_msg.tokens.output
576
- if oc_msg.cost:
577
- total_cost += oc_msg.cost
578
-
579
- if not chat_messages:
580
- continue
581
-
582
- first_timestamp = self._ms_to_datetime(session.time.created)
583
-
584
- # Apply filters
585
- if filters.agent_name and not any(m.name == filters.agent_name for m in chat_messages):
586
- continue
587
-
588
- if filters.since and first_timestamp < filters.since:
589
- continue
590
-
591
- if filters.query and not any(filters.query in m.content for m in chat_messages):
592
- continue
593
-
594
- # Build MessageData list
595
- msg_data_list: list[MessageData] = []
596
- for chat_msg in chat_messages:
597
- msg_data: MessageData = {
598
- "role": chat_msg.role,
599
- "content": chat_msg.content,
600
- "timestamp": (chat_msg.timestamp or get_now()).isoformat(),
601
- "parent_id": chat_msg.parent_id,
602
- "model": chat_msg.model_name,
603
- "name": chat_msg.name,
604
- "token_usage": TokenUsage(
605
- total=chat_msg.cost_info.token_usage.total_tokens
606
- if chat_msg.cost_info
607
- else 0,
608
- prompt=chat_msg.cost_info.token_usage.input_tokens
609
- if chat_msg.cost_info
610
- else 0,
611
- completion=chat_msg.cost_info.token_usage.output_tokens
612
- if chat_msg.cost_info
613
- else 0,
614
- )
615
- if chat_msg.cost_info
616
- else None,
617
- "cost": float(chat_msg.cost_info.total_cost) if chat_msg.cost_info else None,
618
- "response_time": chat_msg.response_time,
619
- }
620
- msg_data_list.append(msg_data)
621
-
622
- token_usage_data: TokenUsage | None = (
623
- {"total": total_tokens, "prompt": 0, "completion": 0} if total_tokens else None
624
- )
625
- conv_data = ConvData(
626
- id=session_id,
627
- agent=chat_messages[0].name or "opencode",
628
- title=session.title,
629
- start_time=first_timestamp.isoformat(),
630
- messages=msg_data_list,
631
- token_usage=token_usage_data,
632
- )
633
-
634
- result.append((conv_data, chat_messages))
635
-
636
- if filters.limit and len(result) >= filters.limit:
637
- break
638
-
639
- return result
640
-
641
- async def get_conversation_stats(
642
- self,
643
- filters: StatsFilters,
644
- ) -> dict[str, dict[str, Any]]:
645
- """Get conversation statistics."""
646
- stats: dict[str, dict[str, Any]] = defaultdict(
647
- lambda: {"total_tokens": 0, "messages": 0, "models": set(), "total_cost": 0.0}
648
- )
649
-
650
- sessions = self._list_sessions()
651
-
652
- for _session_id, session_path in sessions:
653
- session = self._read_session(session_path)
654
- if not session:
655
- continue
656
-
657
- timestamp = self._ms_to_datetime(session.time.created)
658
- if timestamp < filters.cutoff:
659
- continue
660
-
661
- oc_messages = self._read_messages(session.id)
662
-
663
- for oc_msg in oc_messages:
664
- if oc_msg.role != "assistant":
665
- continue
666
-
667
- model = oc_msg.model_id or (oc_msg.model.model_id if oc_msg.model else "unknown")
668
- tokens = 0
669
- if oc_msg.tokens:
670
- tokens = oc_msg.tokens.input + oc_msg.tokens.output
671
-
672
- msg_timestamp = self._ms_to_datetime(oc_msg.time.created)
673
-
674
- # Group by specified criterion
675
- match filters.group_by:
676
- case "model":
677
- key = model
678
- case "hour":
679
- key = msg_timestamp.strftime("%Y-%m-%d %H:00")
680
- case "day":
681
- key = msg_timestamp.strftime("%Y-%m-%d")
682
- case _:
683
- key = oc_msg.agent or "opencode"
684
-
685
- stats[key]["messages"] += 1
686
- stats[key]["total_tokens"] += tokens
687
- stats[key]["models"].add(model)
688
- stats[key]["total_cost"] += oc_msg.cost or 0.0
689
-
690
- # Convert sets to lists
691
- for value in stats.values():
692
- value["models"] = list(value["models"])
693
-
694
- return dict(stats)
695
-
696
- async def reset(
697
- self,
698
- *,
699
- agent_name: str | None = None,
700
- hard: bool = False,
701
- ) -> tuple[int, int]:
702
- """Reset storage.
703
-
704
- Warning: This would delete OpenCode data!
705
- """
706
- logger.warning("Reset not implemented for OpenCode storage (read-only)")
707
- return 0, 0
708
-
709
- async def get_conversation_counts(
710
- self,
711
- *,
712
- agent_name: str | None = None,
713
- ) -> tuple[int, int]:
714
- """Get counts of conversations and messages."""
715
- conv_count = 0
716
- msg_count = 0
717
-
718
- sessions = self._list_sessions()
719
-
720
- for session_id, session_path in sessions:
721
- session = self._read_session(session_path)
722
- if not session:
723
- continue
724
-
725
- oc_messages = self._read_messages(session_id)
726
- if oc_messages:
727
- conv_count += 1
728
- msg_count += len(oc_messages)
729
-
730
- return conv_count, msg_count