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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (311) hide show
  1. acp/__init__.py +13 -4
  2. acp/acp_requests.py +20 -77
  3. acp/agent/connection.py +8 -0
  4. acp/agent/implementations/debug_server/debug_server.py +6 -2
  5. acp/agent/protocol.py +6 -0
  6. acp/bridge/README.md +15 -2
  7. acp/bridge/__init__.py +3 -2
  8. acp/bridge/__main__.py +60 -19
  9. acp/bridge/ws_server.py +173 -0
  10. acp/bridge/ws_server_cli.py +89 -0
  11. acp/client/connection.py +38 -29
  12. acp/client/implementations/default_client.py +3 -2
  13. acp/client/implementations/headless_client.py +2 -2
  14. acp/connection.py +2 -2
  15. acp/notifications.py +20 -50
  16. acp/schema/__init__.py +2 -0
  17. acp/schema/agent_responses.py +21 -0
  18. acp/schema/client_requests.py +3 -3
  19. acp/schema/session_state.py +63 -29
  20. acp/stdio.py +39 -9
  21. acp/task/supervisor.py +2 -2
  22. acp/transports.py +362 -2
  23. acp/utils.py +17 -4
  24. agentpool/__init__.py +6 -1
  25. agentpool/agents/__init__.py +2 -0
  26. agentpool/agents/acp_agent/acp_agent.py +407 -277
  27. agentpool/agents/acp_agent/acp_converters.py +196 -38
  28. agentpool/agents/acp_agent/client_handler.py +191 -26
  29. agentpool/agents/acp_agent/session_state.py +17 -6
  30. agentpool/agents/agent.py +607 -572
  31. agentpool/agents/agui_agent/__init__.py +0 -2
  32. agentpool/agents/agui_agent/agui_agent.py +176 -110
  33. agentpool/agents/agui_agent/agui_converters.py +0 -131
  34. agentpool/agents/agui_agent/helpers.py +3 -4
  35. agentpool/agents/base_agent.py +632 -17
  36. agentpool/agents/claude_code_agent/FORKING.md +191 -0
  37. agentpool/agents/claude_code_agent/__init__.py +13 -1
  38. agentpool/agents/claude_code_agent/claude_code_agent.py +1058 -291
  39. agentpool/agents/claude_code_agent/converters.py +74 -143
  40. agentpool/agents/claude_code_agent/history.py +474 -0
  41. agentpool/agents/claude_code_agent/models.py +77 -0
  42. agentpool/agents/claude_code_agent/static_info.py +100 -0
  43. agentpool/agents/claude_code_agent/usage.py +242 -0
  44. agentpool/agents/context.py +40 -0
  45. agentpool/agents/events/__init__.py +24 -0
  46. agentpool/agents/events/builtin_handlers.py +67 -1
  47. agentpool/agents/events/event_emitter.py +32 -2
  48. agentpool/agents/events/events.py +104 -3
  49. agentpool/agents/events/infer_info.py +145 -0
  50. agentpool/agents/events/processors.py +254 -0
  51. agentpool/agents/interactions.py +41 -6
  52. agentpool/agents/modes.py +67 -0
  53. agentpool/agents/slashed_agent.py +5 -4
  54. agentpool/agents/tool_call_accumulator.py +213 -0
  55. agentpool/agents/tool_wrapping.py +18 -6
  56. agentpool/common_types.py +56 -21
  57. agentpool/config_resources/__init__.py +38 -1
  58. agentpool/config_resources/acp_assistant.yml +2 -2
  59. agentpool/config_resources/agents.yml +3 -0
  60. agentpool/config_resources/agents_template.yml +1 -0
  61. agentpool/config_resources/claude_code_agent.yml +10 -6
  62. agentpool/config_resources/external_acp_agents.yml +2 -1
  63. agentpool/delegation/base_team.py +4 -30
  64. agentpool/delegation/pool.py +136 -289
  65. agentpool/delegation/team.py +58 -57
  66. agentpool/delegation/teamrun.py +51 -55
  67. agentpool/diagnostics/__init__.py +53 -0
  68. agentpool/diagnostics/lsp_manager.py +1593 -0
  69. agentpool/diagnostics/lsp_proxy.py +41 -0
  70. agentpool/diagnostics/lsp_proxy_script.py +229 -0
  71. agentpool/diagnostics/models.py +398 -0
  72. agentpool/functional/run.py +10 -4
  73. agentpool/mcp_server/__init__.py +0 -2
  74. agentpool/mcp_server/client.py +76 -32
  75. agentpool/mcp_server/conversions.py +54 -13
  76. agentpool/mcp_server/manager.py +34 -54
  77. agentpool/mcp_server/registries/official_registry_client.py +35 -1
  78. agentpool/mcp_server/tool_bridge.py +186 -139
  79. agentpool/messaging/__init__.py +0 -2
  80. agentpool/messaging/compaction.py +72 -197
  81. agentpool/messaging/connection_manager.py +11 -10
  82. agentpool/messaging/event_manager.py +5 -5
  83. agentpool/messaging/message_container.py +6 -30
  84. agentpool/messaging/message_history.py +99 -8
  85. agentpool/messaging/messagenode.py +52 -14
  86. agentpool/messaging/messages.py +54 -35
  87. agentpool/messaging/processing.py +12 -22
  88. agentpool/models/__init__.py +1 -1
  89. agentpool/models/acp_agents/base.py +6 -24
  90. agentpool/models/acp_agents/mcp_capable.py +126 -157
  91. agentpool/models/acp_agents/non_mcp.py +129 -95
  92. agentpool/models/agents.py +98 -76
  93. agentpool/models/agui_agents.py +1 -1
  94. agentpool/models/claude_code_agents.py +144 -19
  95. agentpool/models/file_parsing.py +0 -1
  96. agentpool/models/manifest.py +113 -50
  97. agentpool/prompts/conversion_manager.py +1 -1
  98. agentpool/prompts/prompts.py +5 -2
  99. agentpool/repomap.py +1 -1
  100. agentpool/resource_providers/__init__.py +11 -1
  101. agentpool/resource_providers/aggregating.py +56 -5
  102. agentpool/resource_providers/base.py +70 -4
  103. agentpool/resource_providers/codemode/code_executor.py +72 -5
  104. agentpool/resource_providers/codemode/helpers.py +2 -2
  105. agentpool/resource_providers/codemode/provider.py +64 -12
  106. agentpool/resource_providers/codemode/remote_mcp_execution.py +2 -2
  107. agentpool/resource_providers/codemode/remote_provider.py +9 -12
  108. agentpool/resource_providers/filtering.py +3 -1
  109. agentpool/resource_providers/mcp_provider.py +89 -12
  110. agentpool/resource_providers/plan_provider.py +228 -46
  111. agentpool/resource_providers/pool.py +7 -3
  112. agentpool/resource_providers/resource_info.py +111 -0
  113. agentpool/resource_providers/static.py +4 -2
  114. agentpool/sessions/__init__.py +4 -1
  115. agentpool/sessions/manager.py +33 -5
  116. agentpool/sessions/models.py +59 -6
  117. agentpool/sessions/protocol.py +28 -0
  118. agentpool/sessions/session.py +11 -55
  119. agentpool/skills/registry.py +13 -8
  120. agentpool/storage/manager.py +572 -49
  121. agentpool/talk/registry.py +4 -4
  122. agentpool/talk/talk.py +9 -10
  123. agentpool/testing.py +538 -20
  124. agentpool/tool_impls/__init__.py +6 -0
  125. agentpool/tool_impls/agent_cli/__init__.py +42 -0
  126. agentpool/tool_impls/agent_cli/tool.py +95 -0
  127. agentpool/tool_impls/bash/__init__.py +64 -0
  128. agentpool/tool_impls/bash/helpers.py +35 -0
  129. agentpool/tool_impls/bash/tool.py +171 -0
  130. agentpool/tool_impls/delete_path/__init__.py +70 -0
  131. agentpool/tool_impls/delete_path/tool.py +142 -0
  132. agentpool/tool_impls/download_file/__init__.py +80 -0
  133. agentpool/tool_impls/download_file/tool.py +183 -0
  134. agentpool/tool_impls/execute_code/__init__.py +55 -0
  135. agentpool/tool_impls/execute_code/tool.py +163 -0
  136. agentpool/tool_impls/grep/__init__.py +80 -0
  137. agentpool/tool_impls/grep/tool.py +200 -0
  138. agentpool/tool_impls/list_directory/__init__.py +73 -0
  139. agentpool/tool_impls/list_directory/tool.py +197 -0
  140. agentpool/tool_impls/question/__init__.py +42 -0
  141. agentpool/tool_impls/question/tool.py +127 -0
  142. agentpool/tool_impls/read/__init__.py +104 -0
  143. agentpool/tool_impls/read/tool.py +305 -0
  144. agentpool/tools/__init__.py +2 -1
  145. agentpool/tools/base.py +114 -34
  146. agentpool/tools/manager.py +57 -1
  147. agentpool/ui/base.py +2 -2
  148. agentpool/ui/mock_provider.py +2 -2
  149. agentpool/ui/stdlib_provider.py +2 -2
  150. agentpool/utils/file_watcher.py +269 -0
  151. agentpool/utils/identifiers.py +121 -0
  152. agentpool/utils/pydantic_ai_helpers.py +46 -0
  153. agentpool/utils/streams.py +616 -2
  154. agentpool/utils/subprocess_utils.py +155 -0
  155. agentpool/utils/token_breakdown.py +461 -0
  156. agentpool/vfs_registry.py +7 -2
  157. {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/METADATA +41 -27
  158. agentpool-2.5.0.dist-info/RECORD +579 -0
  159. {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/WHEEL +1 -1
  160. agentpool_cli/__main__.py +24 -0
  161. agentpool_cli/create.py +1 -1
  162. agentpool_cli/serve_acp.py +100 -21
  163. agentpool_cli/serve_agui.py +87 -0
  164. agentpool_cli/serve_opencode.py +119 -0
  165. agentpool_cli/ui.py +557 -0
  166. agentpool_commands/__init__.py +42 -5
  167. agentpool_commands/agents.py +75 -2
  168. agentpool_commands/history.py +62 -0
  169. agentpool_commands/mcp.py +176 -0
  170. agentpool_commands/models.py +56 -3
  171. agentpool_commands/pool.py +260 -0
  172. agentpool_commands/session.py +1 -1
  173. agentpool_commands/text_sharing/__init__.py +119 -0
  174. agentpool_commands/text_sharing/base.py +123 -0
  175. agentpool_commands/text_sharing/github_gist.py +80 -0
  176. agentpool_commands/text_sharing/opencode.py +462 -0
  177. agentpool_commands/text_sharing/paste_rs.py +59 -0
  178. agentpool_commands/text_sharing/pastebin.py +116 -0
  179. agentpool_commands/text_sharing/shittycodingagent.py +112 -0
  180. agentpool_commands/tools.py +57 -0
  181. agentpool_commands/utils.py +80 -30
  182. agentpool_config/__init__.py +30 -2
  183. agentpool_config/agentpool_tools.py +498 -0
  184. agentpool_config/builtin_tools.py +77 -22
  185. agentpool_config/commands.py +24 -1
  186. agentpool_config/compaction.py +258 -0
  187. agentpool_config/converters.py +1 -1
  188. agentpool_config/event_handlers.py +42 -0
  189. agentpool_config/events.py +1 -1
  190. agentpool_config/forward_targets.py +1 -4
  191. agentpool_config/jinja.py +3 -3
  192. agentpool_config/mcp_server.py +132 -6
  193. agentpool_config/nodes.py +1 -1
  194. agentpool_config/observability.py +44 -0
  195. agentpool_config/session.py +0 -3
  196. agentpool_config/storage.py +82 -38
  197. agentpool_config/task.py +3 -3
  198. agentpool_config/tools.py +11 -22
  199. agentpool_config/toolsets.py +109 -233
  200. agentpool_server/a2a_server/agent_worker.py +307 -0
  201. agentpool_server/a2a_server/server.py +23 -18
  202. agentpool_server/acp_server/acp_agent.py +234 -181
  203. agentpool_server/acp_server/commands/acp_commands.py +151 -156
  204. agentpool_server/acp_server/commands/docs_commands/fetch_repo.py +18 -17
  205. agentpool_server/acp_server/event_converter.py +651 -0
  206. agentpool_server/acp_server/input_provider.py +53 -10
  207. agentpool_server/acp_server/server.py +24 -90
  208. agentpool_server/acp_server/session.py +173 -331
  209. agentpool_server/acp_server/session_manager.py +8 -34
  210. agentpool_server/agui_server/server.py +3 -1
  211. agentpool_server/mcp_server/server.py +5 -2
  212. agentpool_server/opencode_server/.rules +95 -0
  213. agentpool_server/opencode_server/ENDPOINTS.md +401 -0
  214. agentpool_server/opencode_server/OPENCODE_UI_TOOLS_COMPLETE.md +202 -0
  215. agentpool_server/opencode_server/__init__.py +19 -0
  216. agentpool_server/opencode_server/command_validation.py +172 -0
  217. agentpool_server/opencode_server/converters.py +975 -0
  218. agentpool_server/opencode_server/dependencies.py +24 -0
  219. agentpool_server/opencode_server/input_provider.py +421 -0
  220. agentpool_server/opencode_server/models/__init__.py +250 -0
  221. agentpool_server/opencode_server/models/agent.py +53 -0
  222. agentpool_server/opencode_server/models/app.py +72 -0
  223. agentpool_server/opencode_server/models/base.py +26 -0
  224. agentpool_server/opencode_server/models/common.py +23 -0
  225. agentpool_server/opencode_server/models/config.py +37 -0
  226. agentpool_server/opencode_server/models/events.py +821 -0
  227. agentpool_server/opencode_server/models/file.py +88 -0
  228. agentpool_server/opencode_server/models/mcp.py +44 -0
  229. agentpool_server/opencode_server/models/message.py +179 -0
  230. agentpool_server/opencode_server/models/parts.py +323 -0
  231. agentpool_server/opencode_server/models/provider.py +81 -0
  232. agentpool_server/opencode_server/models/pty.py +43 -0
  233. agentpool_server/opencode_server/models/question.py +56 -0
  234. agentpool_server/opencode_server/models/session.py +111 -0
  235. agentpool_server/opencode_server/routes/__init__.py +29 -0
  236. agentpool_server/opencode_server/routes/agent_routes.py +473 -0
  237. agentpool_server/opencode_server/routes/app_routes.py +202 -0
  238. agentpool_server/opencode_server/routes/config_routes.py +302 -0
  239. agentpool_server/opencode_server/routes/file_routes.py +571 -0
  240. agentpool_server/opencode_server/routes/global_routes.py +94 -0
  241. agentpool_server/opencode_server/routes/lsp_routes.py +319 -0
  242. agentpool_server/opencode_server/routes/message_routes.py +761 -0
  243. agentpool_server/opencode_server/routes/permission_routes.py +63 -0
  244. agentpool_server/opencode_server/routes/pty_routes.py +300 -0
  245. agentpool_server/opencode_server/routes/question_routes.py +128 -0
  246. agentpool_server/opencode_server/routes/session_routes.py +1276 -0
  247. agentpool_server/opencode_server/routes/tui_routes.py +139 -0
  248. agentpool_server/opencode_server/server.py +475 -0
  249. agentpool_server/opencode_server/state.py +151 -0
  250. agentpool_server/opencode_server/time_utils.py +8 -0
  251. agentpool_storage/__init__.py +12 -0
  252. agentpool_storage/base.py +184 -2
  253. agentpool_storage/claude_provider/ARCHITECTURE.md +433 -0
  254. agentpool_storage/claude_provider/__init__.py +42 -0
  255. agentpool_storage/claude_provider/provider.py +1089 -0
  256. agentpool_storage/file_provider.py +278 -15
  257. agentpool_storage/memory_provider.py +193 -12
  258. agentpool_storage/models.py +3 -0
  259. agentpool_storage/opencode_provider/ARCHITECTURE.md +386 -0
  260. agentpool_storage/opencode_provider/__init__.py +16 -0
  261. agentpool_storage/opencode_provider/helpers.py +414 -0
  262. agentpool_storage/opencode_provider/provider.py +895 -0
  263. agentpool_storage/project_store.py +325 -0
  264. agentpool_storage/session_store.py +26 -6
  265. agentpool_storage/sql_provider/__init__.py +4 -2
  266. agentpool_storage/sql_provider/models.py +48 -0
  267. agentpool_storage/sql_provider/sql_provider.py +269 -3
  268. agentpool_storage/sql_provider/utils.py +12 -13
  269. agentpool_storage/zed_provider/__init__.py +16 -0
  270. agentpool_storage/zed_provider/helpers.py +281 -0
  271. agentpool_storage/zed_provider/models.py +130 -0
  272. agentpool_storage/zed_provider/provider.py +442 -0
  273. agentpool_storage/zed_provider.py +803 -0
  274. agentpool_toolsets/__init__.py +0 -2
  275. agentpool_toolsets/builtin/__init__.py +2 -12
  276. agentpool_toolsets/builtin/code.py +96 -57
  277. agentpool_toolsets/builtin/debug.py +118 -48
  278. agentpool_toolsets/builtin/execution_environment.py +115 -230
  279. agentpool_toolsets/builtin/file_edit/file_edit.py +115 -7
  280. agentpool_toolsets/builtin/skills.py +9 -4
  281. agentpool_toolsets/builtin/subagent_tools.py +64 -51
  282. agentpool_toolsets/builtin/workers.py +4 -2
  283. agentpool_toolsets/composio_toolset.py +2 -2
  284. agentpool_toolsets/entry_points.py +3 -1
  285. agentpool_toolsets/fsspec_toolset/__init__.py +13 -1
  286. agentpool_toolsets/fsspec_toolset/diagnostics.py +860 -73
  287. agentpool_toolsets/fsspec_toolset/grep.py +99 -7
  288. agentpool_toolsets/fsspec_toolset/helpers.py +3 -2
  289. agentpool_toolsets/fsspec_toolset/image_utils.py +161 -0
  290. agentpool_toolsets/fsspec_toolset/toolset.py +500 -95
  291. agentpool_toolsets/mcp_discovery/__init__.py +5 -0
  292. agentpool_toolsets/mcp_discovery/data/mcp_servers.parquet +0 -0
  293. agentpool_toolsets/mcp_discovery/toolset.py +511 -0
  294. agentpool_toolsets/mcp_run_toolset.py +87 -12
  295. agentpool_toolsets/notifications.py +33 -33
  296. agentpool_toolsets/openapi.py +3 -1
  297. agentpool_toolsets/search_toolset.py +3 -1
  298. agentpool-2.1.9.dist-info/RECORD +0 -474
  299. agentpool_config/resources.py +0 -33
  300. agentpool_server/acp_server/acp_tools.py +0 -43
  301. agentpool_server/acp_server/commands/spawn.py +0 -210
  302. agentpool_storage/text_log_provider.py +0 -275
  303. agentpool_toolsets/builtin/agent_management.py +0 -239
  304. agentpool_toolsets/builtin/chain.py +0 -288
  305. agentpool_toolsets/builtin/history.py +0 -36
  306. agentpool_toolsets/builtin/integration.py +0 -85
  307. agentpool_toolsets/builtin/tool_management.py +0 -90
  308. agentpool_toolsets/builtin/user_interaction.py +0 -52
  309. agentpool_toolsets/semantic_memory_toolset.py +0 -536
  310. {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/entry_points.txt +0 -0
  311. {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -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,9 +19,9 @@ 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
 
24
+ from agentpool.sessions.models import ProjectData
25
25
  from agentpool_config.session import SessionQuery
26
26
  from agentpool_config.storage import FileStorageConfig
27
27
 
@@ -41,11 +41,11 @@ class MessageData(TypedDict):
41
41
  cost: Decimal | None
42
42
  token_usage: TokenUsage | None
43
43
  response_time: float | None
44
- forwarded_from: list[str] | None
45
44
  provider_name: str | None
46
45
  provider_response_id: str | None
47
46
  messages: str | None
48
47
  finish_reason: FinishReason | None
48
+ parent_id: str | None
49
49
 
50
50
 
51
51
  class ConversationData(TypedDict):
@@ -68,12 +68,26 @@ class CommandData(TypedDict):
68
68
  metadata: dict[str, JsonValue]
69
69
 
70
70
 
71
+ class ProjectDataDict(TypedDict):
72
+ """Data structure for storing project information."""
73
+
74
+ project_id: str
75
+ worktree: str
76
+ name: str | None
77
+ vcs: str | None
78
+ config_path: str | None
79
+ created_at: str
80
+ last_active: str
81
+ settings: dict[str, JsonValue]
82
+
83
+
71
84
  class StorageData(TypedDict):
72
85
  """Data structure for storing storage information."""
73
86
 
74
87
  messages: list[MessageData]
75
88
  conversations: list[ConversationData]
76
89
  commands: list[CommandData]
90
+ projects: list[ProjectDataDict]
77
91
 
78
92
 
79
93
  class FileProvider(StorageProvider):
@@ -99,6 +113,7 @@ class FileProvider(StorageProvider):
99
113
  "messages": [],
100
114
  "conversations": [],
101
115
  "commands": [],
116
+ "projects": [],
102
117
  }
103
118
  self._load()
104
119
 
@@ -132,14 +147,7 @@ class FileProvider(StorageProvider):
132
147
  # Apply filters
133
148
  if query.name and msg["conversation_id"] != query.name:
134
149
  continue
135
- if query.agents and not (
136
- msg["name"] in query.agents
137
- or (
138
- query.include_forwarded
139
- and msg["forwarded_from"]
140
- and any(a in query.agents for a in msg["forwarded_from"])
141
- )
142
- ):
150
+ if query.agents and msg["name"] not in query.agents:
143
151
  continue
144
152
  cutoff = query.get_time_cutoff()
145
153
  timestamp = datetime.fromisoformat(msg["timestamp"])
@@ -175,7 +183,6 @@ class FileProvider(StorageProvider):
175
183
  model_name=msg["model"],
176
184
  cost_info=cost_info,
177
185
  response_time=msg["response_time"],
178
- forwarded_from=msg["forwarded_from"] or [],
179
186
  timestamp=datetime.fromisoformat(msg["timestamp"]),
180
187
  provider_name=msg["provider_name"],
181
188
  provider_response_id=msg["provider_response_id"],
@@ -200,11 +207,11 @@ class FileProvider(StorageProvider):
200
207
  cost_info: TokenCost | None = None,
201
208
  model: str | None = None,
202
209
  response_time: float | None = None,
203
- forwarded_from: list[str] | None = None,
204
210
  provider_name: str | None = None,
205
211
  provider_response_id: str | None = None,
206
212
  messages: str | None = None,
207
213
  finish_reason: FinishReason | None = None,
214
+ parent_id: str | None = None,
208
215
  ) -> None:
209
216
  """Log a new message."""
210
217
  self._data["messages"].append({
@@ -222,11 +229,11 @@ class FileProvider(StorageProvider):
222
229
  total=cost_info.token_usage.total_tokens if cost_info else 0,
223
230
  ),
224
231
  "response_time": response_time,
225
- "forwarded_from": forwarded_from,
226
232
  "provider_name": provider_name,
227
233
  "provider_response_id": provider_response_id,
228
234
  "messages": messages,
229
235
  "finish_reason": finish_reason,
236
+ "parent_id": parent_id,
230
237
  })
231
238
  self._save()
232
239
 
@@ -269,6 +276,152 @@ class FileProvider(StorageProvider):
269
276
  return conv.get("title")
270
277
  return None
271
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
+
272
425
  async def log_command(
273
426
  self,
274
427
  *,
@@ -329,6 +482,7 @@ class FileProvider(StorageProvider):
329
482
  "messages": [],
330
483
  "conversations": [],
331
484
  "commands": [],
485
+ "projects": [],
332
486
  }
333
487
  self._save()
334
488
  return conv_count, msg_count
@@ -376,3 +530,112 @@ class FileProvider(StorageProvider):
376
530
  msg_count = len(self._data["messages"])
377
531
 
378
532
  return conv_count, msg_count
533
+
534
+ async def delete_conversation_messages(
535
+ self,
536
+ conversation_id: str,
537
+ ) -> int:
538
+ """Delete all messages for a conversation."""
539
+ original_count = len(self._data["messages"])
540
+ self._data["messages"] = [
541
+ m for m in self._data["messages"] if m["conversation_id"] != conversation_id
542
+ ]
543
+ deleted = original_count - len(self._data["messages"])
544
+ if deleted > 0:
545
+ self._save()
546
+ return deleted
547
+
548
+ # Project methods
549
+
550
+ def _to_project_data(self, data: ProjectDataDict) -> ProjectData:
551
+ """Convert dict to ProjectData."""
552
+ from datetime import datetime
553
+
554
+ from agentpool.sessions.models import ProjectData
555
+
556
+ return ProjectData(
557
+ project_id=data["project_id"],
558
+ worktree=data["worktree"],
559
+ name=data["name"],
560
+ vcs=data["vcs"],
561
+ config_path=data["config_path"],
562
+ created_at=datetime.fromisoformat(data["created_at"]),
563
+ last_active=datetime.fromisoformat(data["last_active"]),
564
+ settings=data["settings"],
565
+ )
566
+
567
+ def _to_project_dict(self, project: ProjectData) -> ProjectDataDict:
568
+ """Convert ProjectData to dict."""
569
+ return ProjectDataDict(
570
+ project_id=project.project_id,
571
+ worktree=project.worktree,
572
+ name=project.name,
573
+ vcs=project.vcs,
574
+ config_path=project.config_path,
575
+ created_at=project.created_at.isoformat(),
576
+ last_active=project.last_active.isoformat(),
577
+ settings=project.settings,
578
+ )
579
+
580
+ async def save_project(self, project: ProjectData) -> None:
581
+ """Save or update a project."""
582
+ # Remove existing if present
583
+ self._data["projects"] = [
584
+ p for p in self._data.get("projects", []) if p["project_id"] != project.project_id
585
+ ]
586
+ # Add new/updated
587
+ self._data["projects"].append(self._to_project_dict(project))
588
+ self._save()
589
+ logger.debug("Saved project", project_id=project.project_id)
590
+
591
+ async def get_project(self, project_id: str) -> ProjectData | None:
592
+ """Get a project by ID."""
593
+ for p in self._data.get("projects", []):
594
+ if p["project_id"] == project_id:
595
+ return self._to_project_data(p)
596
+ return None
597
+
598
+ async def get_project_by_worktree(self, worktree: str) -> ProjectData | None:
599
+ """Get a project by worktree path."""
600
+ for p in self._data.get("projects", []):
601
+ if p["worktree"] == worktree:
602
+ return self._to_project_data(p)
603
+ return None
604
+
605
+ async def get_project_by_name(self, name: str) -> ProjectData | None:
606
+ """Get a project by friendly name."""
607
+ for p in self._data.get("projects", []):
608
+ if p["name"] == name:
609
+ return self._to_project_data(p)
610
+ return None
611
+
612
+ async def list_projects(self, limit: int | None = None) -> list[ProjectData]:
613
+ """List all projects, ordered by last_active descending."""
614
+ projects = sorted(
615
+ self._data.get("projects", []),
616
+ key=lambda p: p["last_active"],
617
+ reverse=True,
618
+ )
619
+ if limit is not None:
620
+ projects = projects[:limit]
621
+ return [self._to_project_data(p) for p in projects]
622
+
623
+ async def delete_project(self, project_id: str) -> bool:
624
+ """Delete a project."""
625
+ original_count = len(self._data.get("projects", []))
626
+ self._data["projects"] = [
627
+ p for p in self._data.get("projects", []) if p["project_id"] != project_id
628
+ ]
629
+ deleted = original_count > len(self._data["projects"])
630
+ if deleted:
631
+ self._save()
632
+ logger.debug("Deleted project", project_id=project_id)
633
+ return deleted
634
+
635
+ async def touch_project(self, project_id: str) -> None:
636
+ """Update project's last_active timestamp."""
637
+ for p in self._data.get("projects", []):
638
+ if p["project_id"] == project_id:
639
+ p["last_active"] = get_now().isoformat()
640
+ self._save()
641
+ return
@@ -16,11 +16,37 @@ if TYPE_CHECKING:
16
16
  from collections.abc import Sequence
17
17
 
18
18
  from agentpool.common_types import JsonValue
19
+ from agentpool.sessions.models import ProjectData
19
20
  from agentpool_config.session import SessionQuery
20
21
  from agentpool_config.storage import MemoryStorageConfig
21
22
  from agentpool_storage.models import MessageData, QueryFilters, StatsFilters, TokenUsage
22
23
 
23
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
+
24
50
  class MemoryStorageProvider(StorageProvider):
25
51
  """In-memory storage provider for testing."""
26
52
 
@@ -31,12 +57,14 @@ class MemoryStorageProvider(StorageProvider):
31
57
  self.messages: list[dict[str, Any]] = []
32
58
  self.conversations: list[dict[str, Any]] = []
33
59
  self.commands: list[dict[str, Any]] = []
60
+ self.projects: dict[str, ProjectData] = {}
34
61
 
35
62
  def cleanup(self) -> None:
36
63
  """Clear all stored data."""
37
64
  self.messages.clear()
38
65
  self.conversations.clear()
39
66
  self.commands.clear()
67
+ self.projects.clear()
40
68
 
41
69
  async def filter_messages(self, query: SessionQuery) -> list[ChatMessage[str]]:
42
70
  """Filter messages from memory."""
@@ -49,14 +77,7 @@ class MemoryStorageProvider(StorageProvider):
49
77
  continue
50
78
 
51
79
  # Skip if agent name doesn't match
52
- if query.agents and not (
53
- msg["name"] in query.agents
54
- or (
55
- query.include_forwarded
56
- and msg["forwarded_from"]
57
- and any(a in query.agents for a in msg["forwarded_from"])
58
- )
59
- ):
80
+ if query.agents and msg["name"] not in query.agents:
60
81
  continue
61
82
 
62
83
  # Skip if before cutoff time
@@ -90,7 +111,6 @@ class MemoryStorageProvider(StorageProvider):
90
111
  model_name=msg["model"],
91
112
  cost_info=cost_info,
92
113
  response_time=msg["response_time"],
93
- forwarded_from=msg["forwarded_from"] or [],
94
114
  timestamp=msg["timestamp"],
95
115
  provider_name=msg["provider_name"],
96
116
  provider_response_id=msg["provider_response_id"],
@@ -116,11 +136,11 @@ class MemoryStorageProvider(StorageProvider):
116
136
  cost_info: TokenCost | None = None,
117
137
  model: str | None = None,
118
138
  response_time: float | None = None,
119
- forwarded_from: list[str] | None = None,
120
139
  provider_name: str | None = None,
121
140
  provider_response_id: str | None = None,
122
141
  messages: str | None = None,
123
142
  finish_reason: str | None = None,
143
+ parent_id: str | None = None,
124
144
  ) -> None:
125
145
  """Store message in memory."""
126
146
  if next((i for i in self.messages if i["message_id"] == message_id), None):
@@ -130,13 +150,13 @@ class MemoryStorageProvider(StorageProvider):
130
150
  self.messages.append({
131
151
  "conversation_id": conversation_id,
132
152
  "message_id": message_id,
153
+ "parent_id": parent_id,
133
154
  "content": content,
134
155
  "role": role,
135
156
  "name": name,
136
157
  "cost_info": cost_info.token_usage if cost_info else None,
137
158
  "model": model,
138
159
  "response_time": response_time,
139
- "forwarded_from": forwarded_from,
140
160
  "provider_name": provider_name,
141
161
  "provider_response_id": provider_response_id,
142
162
  "messages": messages,
@@ -183,6 +203,111 @@ class MemoryStorageProvider(StorageProvider):
183
203
  return conv.get("title")
184
204
  return None
185
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
+
186
311
  async def log_command(
187
312
  self,
188
313
  *,
@@ -261,7 +386,6 @@ class MemoryStorageProvider(StorageProvider):
261
386
  model_name=msg["model"],
262
387
  cost_info=cost_info,
263
388
  response_time=msg["response_time"],
264
- forwarded_from=msg["forwarded_from"],
265
389
  timestamp=msg["timestamp"],
266
390
  )
267
391
  conv_messages.append(chat_msg)
@@ -394,3 +518,60 @@ class MemoryStorageProvider(StorageProvider):
394
518
  msg_count = len(self.messages)
395
519
 
396
520
  return conv_count, msg_count
521
+
522
+ async def delete_conversation_messages(
523
+ self,
524
+ conversation_id: str,
525
+ ) -> int:
526
+ """Delete all messages for a conversation."""
527
+ original_count = len(self.messages)
528
+ self.messages = [m for m in self.messages if m["conversation_id"] != conversation_id]
529
+ return original_count - len(self.messages)
530
+
531
+ # Project methods
532
+
533
+ async def save_project(self, project: ProjectData) -> None:
534
+ """Save or update a project."""
535
+ self.projects[project.project_id] = project
536
+
537
+ async def get_project(self, project_id: str) -> ProjectData | None:
538
+ """Get a project by ID."""
539
+ return self.projects.get(project_id)
540
+
541
+ async def get_project_by_worktree(self, worktree: str) -> ProjectData | None:
542
+ """Get a project by worktree path."""
543
+ for project in self.projects.values():
544
+ if project.worktree == worktree:
545
+ return project
546
+ return None
547
+
548
+ async def get_project_by_name(self, name: str) -> ProjectData | None:
549
+ """Get a project by friendly name."""
550
+ for project in self.projects.values():
551
+ if project.name == name:
552
+ return project
553
+ return None
554
+
555
+ async def list_projects(self, limit: int | None = None) -> list[ProjectData]:
556
+ """List all projects, ordered by last_active descending."""
557
+ projects = sorted(
558
+ self.projects.values(),
559
+ key=lambda p: p.last_active,
560
+ reverse=True,
561
+ )
562
+ if limit is not None:
563
+ projects = projects[:limit]
564
+ return list(projects)
565
+
566
+ async def delete_project(self, project_id: str) -> bool:
567
+ """Delete a project."""
568
+ if project_id in self.projects:
569
+ del self.projects[project_id]
570
+ return True
571
+ return False
572
+
573
+ async def touch_project(self, project_id: str) -> None:
574
+ """Update project's last_active timestamp."""
575
+ if project_id in self.projects:
576
+ project = self.projects[project_id]
577
+ self.projects[project_id] = project.touch()
@@ -36,6 +36,9 @@ class MessageData(TypedDict):
36
36
  timestamp: str
37
37
  """When the message was sent (ISO format)"""
38
38
 
39
+ parent_id: str | None
40
+ """ID of the parent message for tree-structured conversations."""
41
+
39
42
  model: str | None
40
43
  """Name of the model that generated this message"""
41
44