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
@@ -0,0 +1,414 @@
1
+ """Helper functions for OpenCode storage provider.
2
+
3
+ Stateless conversion and utility functions for working with OpenCode format.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import base64
9
+ from datetime import UTC, datetime
10
+ from decimal import Decimal
11
+ from typing import TYPE_CHECKING
12
+
13
+ import anyenv
14
+ from pydantic_ai import RunUsage
15
+ from pydantic_ai.messages import (
16
+ AudioUrl,
17
+ BinaryContent,
18
+ DocumentUrl,
19
+ ImageUrl,
20
+ ModelRequest,
21
+ ModelResponse,
22
+ TextPart,
23
+ ThinkingPart,
24
+ ToolCallPart,
25
+ ToolReturnPart,
26
+ UserPromptPart,
27
+ VideoUrl,
28
+ )
29
+ from pydantic_ai.usage import RequestUsage
30
+
31
+ from agentpool.log import get_logger
32
+ from agentpool.messaging import ChatMessage, TokenCost
33
+ from agentpool_server.opencode_server.models import (
34
+ AssistantMessage,
35
+ FilePart as OpenCodeFilePart,
36
+ ReasoningPart as OpenCodeReasoningPart,
37
+ Session,
38
+ TextPart as OpenCodeTextPart,
39
+ ToolPart as OpenCodeToolPart,
40
+ ToolStateCompleted,
41
+ UserMessage,
42
+ )
43
+
44
+
45
+ if TYPE_CHECKING:
46
+ from collections.abc import Sequence
47
+ from pathlib import Path
48
+
49
+ from pydantic_ai.messages import (
50
+ UserContent,
51
+ )
52
+
53
+ from agentpool_server.opencode_server.models import (
54
+ MessageInfo as OpenCodeMessage,
55
+ Part as OpenCodePart,
56
+ )
57
+
58
+
59
+ logger = get_logger(__name__)
60
+
61
+
62
+ def ms_to_datetime(ms: int) -> datetime:
63
+ """Convert milliseconds timestamp to datetime.
64
+
65
+ Args:
66
+ ms: Milliseconds since epoch
67
+
68
+ Returns:
69
+ Datetime object in UTC
70
+ """
71
+ return datetime.fromtimestamp(ms / 1000, tz=UTC)
72
+
73
+
74
+ def convert_user_content_to_parts(
75
+ content: str | Sequence[UserContent],
76
+ message_id: str,
77
+ session_id: str,
78
+ part_counter_start: int,
79
+ ) -> list[OpenCodeTextPart | OpenCodeFilePart]:
80
+ """Convert UserContent to OpenCode parts.
81
+
82
+ Args:
83
+ content: User content (str or Sequence[UserContent])
84
+ message_id: Message ID
85
+ session_id: Session ID
86
+ part_counter_start: Starting counter for part IDs
87
+
88
+ Returns:
89
+ List of OpenCode parts (TextPart for text/strings, FilePart for media)
90
+
91
+ Note:
92
+ CachePoint markers are lost in conversion (no OpenCode equivalent)
93
+ """
94
+ parts: list[OpenCodeTextPart | OpenCodeFilePart] = []
95
+ part_counter = part_counter_start
96
+
97
+ if isinstance(content, str):
98
+ # Simple text
99
+ part_id = f"{message_id}-{part_counter}"
100
+ parts.append(
101
+ OpenCodeTextPart(
102
+ id=part_id,
103
+ session_id=session_id,
104
+ message_id=message_id,
105
+ type="text",
106
+ text=content,
107
+ )
108
+ )
109
+ else:
110
+ # Sequence of UserContent
111
+ for content_item in content:
112
+ part_id = f"{message_id}-{part_counter}"
113
+ part_counter += 1
114
+
115
+ if isinstance(content_item, str):
116
+ # Text content
117
+ parts.append(
118
+ OpenCodeTextPart(
119
+ id=part_id,
120
+ session_id=session_id,
121
+ message_id=message_id,
122
+ type="text",
123
+ text=content_item,
124
+ )
125
+ )
126
+ elif isinstance(content_item, ImageUrl):
127
+ # Image URL
128
+ parts.append(
129
+ OpenCodeFilePart(
130
+ id=part_id,
131
+ session_id=session_id,
132
+ message_id=message_id,
133
+ type="file",
134
+ mime=content_item.media_type or "image/*",
135
+ url=content_item.url,
136
+ )
137
+ )
138
+ elif isinstance(content_item, AudioUrl):
139
+ # Audio URL
140
+ parts.append(
141
+ OpenCodeFilePart(
142
+ id=part_id,
143
+ session_id=session_id,
144
+ message_id=message_id,
145
+ type="file",
146
+ mime=content_item.media_type or "audio/*",
147
+ url=content_item.url,
148
+ )
149
+ )
150
+ elif isinstance(content_item, DocumentUrl):
151
+ # Document URL
152
+ parts.append(
153
+ OpenCodeFilePart(
154
+ id=part_id,
155
+ session_id=session_id,
156
+ message_id=message_id,
157
+ type="file",
158
+ mime=content_item.media_type or "application/pdf",
159
+ url=content_item.url,
160
+ )
161
+ )
162
+ elif isinstance(content_item, VideoUrl):
163
+ # Video URL
164
+ parts.append(
165
+ OpenCodeFilePart(
166
+ id=part_id,
167
+ session_id=session_id,
168
+ message_id=message_id,
169
+ type="file",
170
+ mime=content_item.media_type or "video/*",
171
+ url=content_item.url,
172
+ )
173
+ )
174
+ elif isinstance(content_item, BinaryContent):
175
+ # Binary content - convert to data URL
176
+ b64_data = base64.b64encode(content_item.data).decode("ascii")
177
+ data_url = f"data:{content_item.media_type};base64,{b64_data}"
178
+ parts.append(
179
+ OpenCodeFilePart(
180
+ id=part_id,
181
+ session_id=session_id,
182
+ message_id=message_id,
183
+ type="file",
184
+ mime=content_item.media_type,
185
+ url=data_url,
186
+ )
187
+ )
188
+ # CachePoint is just a marker for prompt caching - no data to store
189
+
190
+ return parts
191
+
192
+
193
+ def extract_text_content(parts: list[OpenCodePart]) -> str:
194
+ """Extract text content from parts for display.
195
+
196
+ Args:
197
+ parts: List of OpenCode parts
198
+
199
+ Returns:
200
+ Combined text content from all text and reasoning parts
201
+ """
202
+ text_parts: list[str] = []
203
+ for part in parts:
204
+ if isinstance(part, OpenCodeTextPart) and part.text:
205
+ text_parts.append(part.text)
206
+ elif isinstance(part, OpenCodeReasoningPart) and part.text:
207
+ text_parts.append(f"<thinking>\n{part.text}\n</thinking>")
208
+ return "\n".join(text_parts)
209
+
210
+
211
+ def build_pydantic_messages( # noqa: PLR0915
212
+ msg: OpenCodeMessage,
213
+ parts: list[OpenCodePart],
214
+ timestamp: datetime,
215
+ ) -> list[ModelRequest | ModelResponse]:
216
+ """Build pydantic-ai ModelRequest and/or ModelResponse from OpenCode data.
217
+
218
+ In OpenCode's model, assistant messages contain both tool calls AND their
219
+ results in the same message. We split these into:
220
+ - ModelResponse with ToolCallPart (the call)
221
+ - ModelRequest with ToolReturnPart (the result)
222
+
223
+ Args:
224
+ msg: OpenCode message metadata
225
+ parts: OpenCode message parts
226
+ timestamp: Message timestamp
227
+
228
+ Returns:
229
+ List of pydantic-ai messages (ModelRequest and/or ModelResponse)
230
+ """
231
+ result: list[ModelRequest | ModelResponse] = []
232
+
233
+ if isinstance(msg, UserMessage):
234
+ # Build UserPromptPart content from text and file parts
235
+ user_content: list[UserContent] = []
236
+
237
+ for part in parts:
238
+ if isinstance(part, OpenCodeTextPart) and part.text:
239
+ user_content.append(part.text)
240
+ elif isinstance(part, OpenCodeFilePart):
241
+ # Convert FilePart back to appropriate UserContent type
242
+ url = part.url
243
+ mime = part.mime
244
+
245
+ # Detect data URLs for BinaryContent
246
+ if url.startswith("data:"):
247
+ # Parse data URL: data:mime;base64,data
248
+ if ";base64," in url:
249
+ mime_part, b64_data = url.split(";base64,", 1)
250
+ media_type = mime_part.replace("data:", "")
251
+ data = base64.b64decode(b64_data)
252
+ user_content.append(BinaryContent(data=data, media_type=media_type))
253
+ continue
254
+
255
+ # Convert to appropriate URL type based on MIME
256
+ if mime.startswith("image/") or mime == "image/*":
257
+ user_content.append(
258
+ ImageUrl(url=url, media_type=mime if mime != "image/*" else None)
259
+ )
260
+ elif mime.startswith("audio/") or mime == "audio/*":
261
+ user_content.append(
262
+ AudioUrl(url=url, media_type=mime if mime != "audio/*" else None)
263
+ )
264
+ elif mime.startswith("video/") or mime == "video/*":
265
+ user_content.append(
266
+ VideoUrl(url=url, media_type=mime if mime != "video/*" else None)
267
+ )
268
+ elif mime == "application/pdf" or mime.startswith("application/"):
269
+ user_content.append(DocumentUrl(url=url, media_type=mime))
270
+ else:
271
+ # Unknown MIME type - treat as document
272
+ user_content.append(DocumentUrl(url=url, media_type=mime))
273
+
274
+ if user_content:
275
+ # Create UserPromptPart with content (str or Sequence)
276
+ content: str | Sequence[UserContent]
277
+ if len(user_content) == 1 and isinstance(user_content[0], str):
278
+ content = user_content[0]
279
+ else:
280
+ content = user_content
281
+
282
+ request_parts: list[UserPromptPart | ToolReturnPart] = [
283
+ UserPromptPart(content=content, timestamp=timestamp)
284
+ ]
285
+ result.append(ModelRequest(parts=request_parts, timestamp=timestamp))
286
+
287
+ return result
288
+
289
+ # Assistant message - may contain both tool calls and results
290
+ response_parts: list[TextPart | ToolCallPart | ThinkingPart] = []
291
+ tool_return_parts: list[ToolReturnPart] = []
292
+
293
+ # Build usage
294
+ usage = RequestUsage()
295
+ if isinstance(msg, AssistantMessage) and msg.tokens:
296
+ usage = RequestUsage(
297
+ input_tokens=msg.tokens.input,
298
+ output_tokens=msg.tokens.output,
299
+ cache_read_tokens=msg.tokens.cache.read,
300
+ cache_write_tokens=msg.tokens.cache.write,
301
+ )
302
+
303
+ for part in parts:
304
+ if isinstance(part, OpenCodeTextPart) and part.text:
305
+ response_parts.append(TextPart(content=part.text))
306
+ elif isinstance(part, OpenCodeReasoningPart) and part.text:
307
+ response_parts.append(ThinkingPart(content=part.text))
308
+ elif isinstance(part, OpenCodeToolPart):
309
+ # Add tool call to response
310
+ args = part.state.input or {}
311
+ tc_part = ToolCallPart(tool_name=part.tool, args=args, tool_call_id=part.call_id)
312
+ response_parts.append(tc_part)
313
+ # If completed, also create a tool return
314
+ if isinstance(part.state, ToolStateCompleted) and part.state.output:
315
+ return_part = ToolReturnPart(
316
+ tool_name=part.tool,
317
+ content=part.state.output,
318
+ tool_call_id=part.call_id,
319
+ timestamp=timestamp,
320
+ )
321
+ tool_return_parts.append(return_part)
322
+
323
+ # Add the response if we have parts
324
+ if response_parts:
325
+ # AssistantMessage only has model_id, not model
326
+ model_name = msg.model_id if isinstance(msg, AssistantMessage) else None
327
+ result.append(
328
+ ModelResponse(
329
+ parts=response_parts,
330
+ usage=usage,
331
+ model_name=model_name,
332
+ timestamp=timestamp,
333
+ )
334
+ )
335
+
336
+ # Add tool returns as a separate request (simulating user sending results back)
337
+ if tool_return_parts:
338
+ result.append(ModelRequest(parts=tool_return_parts, timestamp=timestamp))
339
+
340
+ return result
341
+
342
+
343
+ def read_session(session_path: Path) -> Session | None:
344
+ """Read session metadata from file.
345
+
346
+ Args:
347
+ session_path: Path to session JSON file
348
+
349
+ Returns:
350
+ Session object or None if read/parse fails
351
+ """
352
+ if not session_path.exists():
353
+ return None
354
+ try:
355
+ content = session_path.read_text(encoding="utf-8")
356
+ return anyenv.load_json(content, return_type=Session)
357
+ except anyenv.JsonLoadError as e:
358
+ logger.warning("Failed to parse session", path=str(session_path), error=str(e))
359
+ return None
360
+
361
+
362
+ def message_to_chat_message(
363
+ msg: OpenCodeMessage,
364
+ parts: list[OpenCodePart],
365
+ conversation_id: str,
366
+ ) -> ChatMessage[str]:
367
+ """Convert OpenCode message + parts to ChatMessage.
368
+
369
+ Args:
370
+ msg: OpenCode message metadata
371
+ parts: OpenCode message parts
372
+ conversation_id: Conversation/session ID
373
+
374
+ Returns:
375
+ ChatMessage suitable for agentpool storage
376
+ """
377
+ timestamp = ms_to_datetime(msg.time.created)
378
+ # Extract text content for display
379
+ content = extract_text_content(parts)
380
+ # Build pydantic-ai messages
381
+ pydantic_messages = build_pydantic_messages(msg, parts, timestamp)
382
+ # Extract cost info (only for assistant messages)
383
+ cost_info = None
384
+ model_name = None
385
+ parent_id = None
386
+ provider_details = {}
387
+ if isinstance(msg, AssistantMessage):
388
+ if msg.tokens:
389
+ input_tokens = msg.tokens.input + msg.tokens.cache.read
390
+ output_tokens = msg.tokens.output
391
+ if input_tokens or output_tokens:
392
+ usage = RunUsage(input_tokens=input_tokens, output_tokens=output_tokens)
393
+ cost = Decimal(str(msg.cost)) if msg.cost else Decimal(0)
394
+ cost_info = TokenCost(token_usage=usage, total_cost=cost)
395
+ model_name = msg.model_id
396
+ parent_id = msg.parent_id
397
+ if msg.finish:
398
+ provider_details["finish_reason"] = msg.finish
399
+ elif isinstance(msg, UserMessage) and msg.model:
400
+ model_name = msg.model.model_id
401
+
402
+ return ChatMessage[str](
403
+ content=content,
404
+ conversation_id=conversation_id,
405
+ role=msg.role,
406
+ message_id=msg.id,
407
+ name=msg.agent,
408
+ model_name=model_name,
409
+ cost_info=cost_info,
410
+ timestamp=timestamp,
411
+ parent_id=parent_id,
412
+ messages=pydantic_messages,
413
+ provider_details=provider_details,
414
+ )