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
@@ -4,9 +4,9 @@ from __future__ import annotations
4
4
 
5
5
  from datetime import datetime
6
6
  from decimal import Decimal
7
- from typing import TYPE_CHECKING, TypedDict, cast
7
+ from typing import TYPE_CHECKING, Any, TypedDict, cast
8
8
 
9
- from pydantic_ai import RunUsage
9
+ from pydantic_ai import FinishReason, RunUsage # noqa: TC002
10
10
  from upathtools import to_upath
11
11
 
12
12
  from agentpool.common_types import JsonValue, MessageRole # noqa: TC001
@@ -19,7 +19,6 @@ from agentpool_storage.models import TokenUsage
19
19
 
20
20
 
21
21
  if TYPE_CHECKING:
22
- from pydantic_ai import FinishReason
23
22
  from yamling import FormatType
24
23
 
25
24
  from agentpool.sessions.models import ProjectData
@@ -42,7 +41,6 @@ class MessageData(TypedDict):
42
41
  cost: Decimal | None
43
42
  token_usage: TokenUsage | None
44
43
  response_time: float | None
45
- forwarded_from: list[str] | None
46
44
  provider_name: str | None
47
45
  provider_response_id: str | None
48
46
  messages: str | None
@@ -149,14 +147,7 @@ class FileProvider(StorageProvider):
149
147
  # Apply filters
150
148
  if query.name and msg["conversation_id"] != query.name:
151
149
  continue
152
- if query.agents and not (
153
- msg["name"] in query.agents
154
- or (
155
- query.include_forwarded
156
- and msg["forwarded_from"]
157
- and any(a in query.agents for a in msg["forwarded_from"])
158
- )
159
- ):
150
+ if query.agents and msg["name"] not in query.agents:
160
151
  continue
161
152
  cutoff = query.get_time_cutoff()
162
153
  timestamp = datetime.fromisoformat(msg["timestamp"])
@@ -192,7 +183,6 @@ class FileProvider(StorageProvider):
192
183
  model_name=msg["model"],
193
184
  cost_info=cost_info,
194
185
  response_time=msg["response_time"],
195
- forwarded_from=msg["forwarded_from"] or [],
196
186
  timestamp=datetime.fromisoformat(msg["timestamp"]),
197
187
  provider_name=msg["provider_name"],
198
188
  provider_response_id=msg["provider_response_id"],
@@ -217,7 +207,6 @@ class FileProvider(StorageProvider):
217
207
  cost_info: TokenCost | None = None,
218
208
  model: str | None = None,
219
209
  response_time: float | None = None,
220
- forwarded_from: list[str] | None = None,
221
210
  provider_name: str | None = None,
222
211
  provider_response_id: str | None = None,
223
212
  messages: str | None = None,
@@ -240,7 +229,6 @@ class FileProvider(StorageProvider):
240
229
  total=cost_info.token_usage.total_tokens if cost_info else 0,
241
230
  ),
242
231
  "response_time": response_time,
243
- "forwarded_from": forwarded_from,
244
232
  "provider_name": provider_name,
245
233
  "provider_response_id": provider_response_id,
246
234
  "messages": messages,
@@ -288,6 +276,152 @@ class FileProvider(StorageProvider):
288
276
  return conv.get("title")
289
277
  return None
290
278
 
279
+ async def get_conversation_messages(
280
+ self,
281
+ conversation_id: str,
282
+ *,
283
+ include_ancestors: bool = False,
284
+ ) -> list[ChatMessage[str]]:
285
+ """Get all messages for a conversation."""
286
+ messages: list[ChatMessage[str]] = []
287
+ for msg in self._data["messages"]:
288
+ if msg["conversation_id"] != conversation_id:
289
+ continue
290
+ chat_msg = self._to_chat_message(msg)
291
+ messages.append(chat_msg)
292
+
293
+ # Sort by timestamp
294
+ messages.sort(key=lambda m: m.timestamp or get_now())
295
+
296
+ if not include_ancestors or not messages:
297
+ return messages
298
+
299
+ # Get ancestor chain if first message has parent_id
300
+ first_msg = messages[0]
301
+ if first_msg.parent_id:
302
+ ancestors = await self.get_message_ancestry(first_msg.parent_id)
303
+ return ancestors + messages
304
+
305
+ return messages
306
+
307
+ def _to_chat_message(self, msg: MessageData) -> ChatMessage[str]:
308
+ """Convert stored message data to ChatMessage."""
309
+ cost_info = None
310
+ if msg.get("token_usage"):
311
+ usage = msg["token_usage"]
312
+ cost_info = TokenCost(
313
+ token_usage=RunUsage(
314
+ input_tokens=usage.get("prompt", 0) if usage else 0,
315
+ output_tokens=usage.get("completion", 0) if usage else 0,
316
+ ),
317
+ total_cost=Decimal(str(msg.get("cost") or 0)),
318
+ )
319
+
320
+ # Build kwargs, only including timestamp/message_id if they have values
321
+ kwargs: dict[str, Any] = {
322
+ "content": msg["content"],
323
+ "role": cast(MessageRole, msg["role"]),
324
+ "name": msg.get("name"),
325
+ "model_name": msg.get("model"),
326
+ "cost_info": cost_info,
327
+ "response_time": msg.get("response_time"),
328
+ "parent_id": msg.get("parent_id"),
329
+ "conversation_id": msg.get("conversation_id"),
330
+ "messages": deserialize_messages(msg.get("messages")),
331
+ "finish_reason": msg.get("finish_reason"),
332
+ }
333
+ if msg.get("timestamp"):
334
+ kwargs["timestamp"] = datetime.fromisoformat(msg["timestamp"])
335
+ if msg.get("message_id"):
336
+ kwargs["message_id"] = msg["message_id"]
337
+
338
+ return ChatMessage[str](**kwargs)
339
+
340
+ async def get_message(self, message_id: str) -> ChatMessage[str] | None:
341
+ """Get a single message by ID."""
342
+ for msg in self._data["messages"]:
343
+ if msg.get("message_id") == message_id:
344
+ return self._to_chat_message(msg)
345
+ return None
346
+
347
+ async def get_message_ancestry(self, message_id: str) -> list[ChatMessage[str]]:
348
+ """Get the ancestry chain of a message."""
349
+ ancestors: list[ChatMessage[str]] = []
350
+ current_id: str | None = message_id
351
+
352
+ while current_id:
353
+ msg = await self.get_message(current_id)
354
+ if not msg:
355
+ break
356
+ ancestors.append(msg)
357
+ current_id = msg.parent_id
358
+
359
+ # Reverse to get oldest first
360
+ ancestors.reverse()
361
+ return ancestors
362
+
363
+ async def fork_conversation(
364
+ self,
365
+ *,
366
+ source_conversation_id: str,
367
+ new_conversation_id: str,
368
+ fork_from_message_id: str | None = None,
369
+ new_agent_name: str | None = None,
370
+ ) -> str | None:
371
+ """Fork a conversation at a specific point."""
372
+ # Find source conversation
373
+ source_conv = next(
374
+ (c for c in self._data["conversations"] if c["id"] == source_conversation_id),
375
+ None,
376
+ )
377
+ if not source_conv:
378
+ msg = f"Source conversation not found: {source_conversation_id}"
379
+ raise ValueError(msg)
380
+
381
+ # Determine fork point
382
+ fork_point_id: str | None = None
383
+ if fork_from_message_id:
384
+ # Verify message exists in source conversation
385
+ msg_exists = any(
386
+ m.get("message_id") == fork_from_message_id
387
+ and m["conversation_id"] == source_conversation_id
388
+ for m in self._data["messages"]
389
+ )
390
+ if not msg_exists:
391
+ err = f"Message {fork_from_message_id} not found in conversation"
392
+ raise ValueError(err)
393
+ fork_point_id = fork_from_message_id
394
+ else:
395
+ # Find last message in source conversation
396
+ conv_messages = [
397
+ m for m in self._data["messages"] if m["conversation_id"] == source_conversation_id
398
+ ]
399
+ if conv_messages:
400
+ conv_messages.sort(
401
+ key=lambda m: (
402
+ datetime.fromisoformat(m["timestamp"]) if m.get("timestamp") else get_now()
403
+ )
404
+ )
405
+ fork_point_id = conv_messages[-1].get("message_id")
406
+
407
+ # Create new conversation
408
+ agent_name = new_agent_name or source_conv["agent_name"]
409
+ title = (
410
+ f"{source_conv.get('title') or 'Conversation'} (fork)"
411
+ if source_conv.get("title")
412
+ else None
413
+ )
414
+ new_conv: ConversationData = {
415
+ "id": new_conversation_id,
416
+ "agent_name": agent_name,
417
+ "title": title,
418
+ "start_time": get_now().isoformat(),
419
+ }
420
+ self._data["conversations"].append(new_conv)
421
+ self._save()
422
+
423
+ return fork_point_id
424
+
291
425
  async def log_command(
292
426
  self,
293
427
  *,
@@ -22,6 +22,31 @@ if TYPE_CHECKING:
22
22
  from agentpool_storage.models import MessageData, QueryFilters, StatsFilters, TokenUsage
23
23
 
24
24
 
25
+ def _dict_to_chat_message(msg: dict[str, Any]) -> ChatMessage[str]:
26
+ """Convert a stored message dict to ChatMessage."""
27
+ cost_info = None
28
+ if msg.get("cost_info"):
29
+ cost_info = TokenCost(token_usage=msg["cost_info"], total_cost=msg.get("cost", 0.0))
30
+
31
+ # Build kwargs, only including timestamp/message_id if they exist
32
+ kwargs: dict[str, Any] = {
33
+ "content": msg["content"],
34
+ "role": msg["role"],
35
+ "name": msg.get("name"),
36
+ "model_name": msg.get("model"),
37
+ "cost_info": cost_info,
38
+ "response_time": msg.get("response_time"),
39
+ "parent_id": msg.get("parent_id"),
40
+ "conversation_id": msg.get("conversation_id"),
41
+ }
42
+ if msg.get("timestamp"):
43
+ kwargs["timestamp"] = msg["timestamp"]
44
+ if msg.get("message_id"):
45
+ kwargs["message_id"] = msg["message_id"]
46
+
47
+ return ChatMessage[str](**kwargs)
48
+
49
+
25
50
  class MemoryStorageProvider(StorageProvider):
26
51
  """In-memory storage provider for testing."""
27
52
 
@@ -52,14 +77,7 @@ class MemoryStorageProvider(StorageProvider):
52
77
  continue
53
78
 
54
79
  # Skip if agent name doesn't match
55
- if query.agents and not (
56
- msg["name"] in query.agents
57
- or (
58
- query.include_forwarded
59
- and msg["forwarded_from"]
60
- and any(a in query.agents for a in msg["forwarded_from"])
61
- )
62
- ):
80
+ if query.agents and msg["name"] not in query.agents:
63
81
  continue
64
82
 
65
83
  # Skip if before cutoff time
@@ -93,7 +111,6 @@ class MemoryStorageProvider(StorageProvider):
93
111
  model_name=msg["model"],
94
112
  cost_info=cost_info,
95
113
  response_time=msg["response_time"],
96
- forwarded_from=msg["forwarded_from"] or [],
97
114
  timestamp=msg["timestamp"],
98
115
  provider_name=msg["provider_name"],
99
116
  provider_response_id=msg["provider_response_id"],
@@ -119,7 +136,6 @@ class MemoryStorageProvider(StorageProvider):
119
136
  cost_info: TokenCost | None = None,
120
137
  model: str | None = None,
121
138
  response_time: float | None = None,
122
- forwarded_from: list[str] | None = None,
123
139
  provider_name: str | None = None,
124
140
  provider_response_id: str | None = None,
125
141
  messages: str | None = None,
@@ -134,13 +150,13 @@ class MemoryStorageProvider(StorageProvider):
134
150
  self.messages.append({
135
151
  "conversation_id": conversation_id,
136
152
  "message_id": message_id,
153
+ "parent_id": parent_id,
137
154
  "content": content,
138
155
  "role": role,
139
156
  "name": name,
140
157
  "cost_info": cost_info.token_usage if cost_info else None,
141
158
  "model": model,
142
159
  "response_time": response_time,
143
- "forwarded_from": forwarded_from,
144
160
  "provider_name": provider_name,
145
161
  "provider_response_id": provider_response_id,
146
162
  "messages": messages,
@@ -187,6 +203,111 @@ class MemoryStorageProvider(StorageProvider):
187
203
  return conv.get("title")
188
204
  return None
189
205
 
206
+ async def get_conversation_messages(
207
+ self,
208
+ conversation_id: str,
209
+ *,
210
+ include_ancestors: bool = False,
211
+ ) -> list[ChatMessage[str]]:
212
+ """Get all messages for a conversation."""
213
+ messages: list[ChatMessage[str]] = []
214
+ for msg in self.messages:
215
+ if msg.get("conversation_id") != conversation_id:
216
+ continue
217
+ messages.append(_dict_to_chat_message(msg))
218
+
219
+ # Sort by timestamp
220
+ messages.sort(key=lambda m: m.timestamp or get_now())
221
+
222
+ if not include_ancestors or not messages:
223
+ return messages
224
+
225
+ # Get ancestor chain if first message has parent_id
226
+ first_msg = messages[0]
227
+ if first_msg.parent_id:
228
+ ancestors = await self.get_message_ancestry(first_msg.parent_id)
229
+ return ancestors + messages
230
+
231
+ return messages
232
+
233
+ async def get_message(self, message_id: str) -> ChatMessage[str] | None:
234
+ """Get a single message by ID."""
235
+ for msg in self.messages:
236
+ if msg.get("message_id") == message_id:
237
+ return _dict_to_chat_message(msg)
238
+ return None
239
+
240
+ async def get_message_ancestry(self, message_id: str) -> list[ChatMessage[str]]:
241
+ """Get the ancestry chain of a message."""
242
+ ancestors: list[ChatMessage[str]] = []
243
+ current_id: str | None = message_id
244
+
245
+ while current_id:
246
+ msg = await self.get_message(current_id)
247
+ if not msg:
248
+ break
249
+ ancestors.append(msg)
250
+ current_id = msg.parent_id
251
+
252
+ # Reverse to get oldest first
253
+ ancestors.reverse()
254
+ return ancestors
255
+
256
+ async def fork_conversation(
257
+ self,
258
+ *,
259
+ source_conversation_id: str,
260
+ new_conversation_id: str,
261
+ fork_from_message_id: str | None = None,
262
+ new_agent_name: str | None = None,
263
+ ) -> str | None:
264
+ """Fork a conversation at a specific point."""
265
+ # Find source conversation
266
+ source_conv = next(
267
+ (c for c in self.conversations if c["id"] == source_conversation_id), None
268
+ )
269
+ if not source_conv:
270
+ msg = f"Source conversation not found: {source_conversation_id}"
271
+ raise ValueError(msg)
272
+
273
+ # Determine fork point
274
+ fork_point_id: str | None = None
275
+ if fork_from_message_id:
276
+ # Verify message exists in source conversation
277
+ msg_exists = any(
278
+ m.get("message_id") == fork_from_message_id
279
+ and m["conversation_id"] == source_conversation_id
280
+ for m in self.messages
281
+ )
282
+ if not msg_exists:
283
+ err = f"Message {fork_from_message_id} not found in conversation"
284
+ raise ValueError(err)
285
+ fork_point_id = fork_from_message_id
286
+ else:
287
+ # Find last message in source conversation
288
+ conv_messages = [
289
+ m for m in self.messages if m["conversation_id"] == source_conversation_id
290
+ ]
291
+ if conv_messages:
292
+ conv_messages.sort(key=lambda m: m.get("timestamp") or get_now())
293
+ fork_point_id = conv_messages[-1].get("message_id")
294
+
295
+ # Create new conversation
296
+ agent_name = new_agent_name or source_conv["agent_name"]
297
+ title = (
298
+ f"{source_conv.get('title') or 'Conversation'} (fork)"
299
+ if source_conv.get("title")
300
+ else None
301
+ )
302
+ self.conversations.append({
303
+ "id": new_conversation_id,
304
+ "agent_name": agent_name,
305
+ "title": title,
306
+ "start_time": get_now(),
307
+ })
308
+
309
+ return fork_point_id
310
+
190
311
  async def log_command(
191
312
  self,
192
313
  *,
@@ -265,7 +386,6 @@ class MemoryStorageProvider(StorageProvider):
265
386
  model_name=msg["model"],
266
387
  cost_info=cost_info,
267
388
  response_time=msg["response_time"],
268
- forwarded_from=msg["forwarded_from"],
269
389
  timestamp=msg["timestamp"],
270
390
  )
271
391
  conv_messages.append(chat_msg)