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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (311) hide show
  1. acp/__init__.py +13 -4
  2. acp/acp_requests.py +20 -77
  3. acp/agent/connection.py +8 -0
  4. acp/agent/implementations/debug_server/debug_server.py +6 -2
  5. acp/agent/protocol.py +6 -0
  6. acp/bridge/README.md +15 -2
  7. acp/bridge/__init__.py +3 -2
  8. acp/bridge/__main__.py +60 -19
  9. acp/bridge/ws_server.py +173 -0
  10. acp/bridge/ws_server_cli.py +89 -0
  11. acp/client/connection.py +38 -29
  12. acp/client/implementations/default_client.py +3 -2
  13. acp/client/implementations/headless_client.py +2 -2
  14. acp/connection.py +2 -2
  15. acp/notifications.py +20 -50
  16. acp/schema/__init__.py +2 -0
  17. acp/schema/agent_responses.py +21 -0
  18. acp/schema/client_requests.py +3 -3
  19. acp/schema/session_state.py +63 -29
  20. acp/stdio.py +39 -9
  21. acp/task/supervisor.py +2 -2
  22. acp/transports.py +362 -2
  23. acp/utils.py +17 -4
  24. agentpool/__init__.py +6 -1
  25. agentpool/agents/__init__.py +2 -0
  26. agentpool/agents/acp_agent/acp_agent.py +407 -277
  27. agentpool/agents/acp_agent/acp_converters.py +196 -38
  28. agentpool/agents/acp_agent/client_handler.py +191 -26
  29. agentpool/agents/acp_agent/session_state.py +17 -6
  30. agentpool/agents/agent.py +607 -572
  31. agentpool/agents/agui_agent/__init__.py +0 -2
  32. agentpool/agents/agui_agent/agui_agent.py +176 -110
  33. agentpool/agents/agui_agent/agui_converters.py +0 -131
  34. agentpool/agents/agui_agent/helpers.py +3 -4
  35. agentpool/agents/base_agent.py +632 -17
  36. agentpool/agents/claude_code_agent/FORKING.md +191 -0
  37. agentpool/agents/claude_code_agent/__init__.py +13 -1
  38. agentpool/agents/claude_code_agent/claude_code_agent.py +1058 -291
  39. agentpool/agents/claude_code_agent/converters.py +74 -143
  40. agentpool/agents/claude_code_agent/history.py +474 -0
  41. agentpool/agents/claude_code_agent/models.py +77 -0
  42. agentpool/agents/claude_code_agent/static_info.py +100 -0
  43. agentpool/agents/claude_code_agent/usage.py +242 -0
  44. agentpool/agents/context.py +40 -0
  45. agentpool/agents/events/__init__.py +24 -0
  46. agentpool/agents/events/builtin_handlers.py +67 -1
  47. agentpool/agents/events/event_emitter.py +32 -2
  48. agentpool/agents/events/events.py +104 -3
  49. agentpool/agents/events/infer_info.py +145 -0
  50. agentpool/agents/events/processors.py +254 -0
  51. agentpool/agents/interactions.py +41 -6
  52. agentpool/agents/modes.py +67 -0
  53. agentpool/agents/slashed_agent.py +5 -4
  54. agentpool/agents/tool_call_accumulator.py +213 -0
  55. agentpool/agents/tool_wrapping.py +18 -6
  56. agentpool/common_types.py +56 -21
  57. agentpool/config_resources/__init__.py +38 -1
  58. agentpool/config_resources/acp_assistant.yml +2 -2
  59. agentpool/config_resources/agents.yml +3 -0
  60. agentpool/config_resources/agents_template.yml +1 -0
  61. agentpool/config_resources/claude_code_agent.yml +10 -6
  62. agentpool/config_resources/external_acp_agents.yml +2 -1
  63. agentpool/delegation/base_team.py +4 -30
  64. agentpool/delegation/pool.py +136 -289
  65. agentpool/delegation/team.py +58 -57
  66. agentpool/delegation/teamrun.py +51 -55
  67. agentpool/diagnostics/__init__.py +53 -0
  68. agentpool/diagnostics/lsp_manager.py +1593 -0
  69. agentpool/diagnostics/lsp_proxy.py +41 -0
  70. agentpool/diagnostics/lsp_proxy_script.py +229 -0
  71. agentpool/diagnostics/models.py +398 -0
  72. agentpool/functional/run.py +10 -4
  73. agentpool/mcp_server/__init__.py +0 -2
  74. agentpool/mcp_server/client.py +76 -32
  75. agentpool/mcp_server/conversions.py +54 -13
  76. agentpool/mcp_server/manager.py +34 -54
  77. agentpool/mcp_server/registries/official_registry_client.py +35 -1
  78. agentpool/mcp_server/tool_bridge.py +186 -139
  79. agentpool/messaging/__init__.py +0 -2
  80. agentpool/messaging/compaction.py +72 -197
  81. agentpool/messaging/connection_manager.py +11 -10
  82. agentpool/messaging/event_manager.py +5 -5
  83. agentpool/messaging/message_container.py +6 -30
  84. agentpool/messaging/message_history.py +99 -8
  85. agentpool/messaging/messagenode.py +52 -14
  86. agentpool/messaging/messages.py +54 -35
  87. agentpool/messaging/processing.py +12 -22
  88. agentpool/models/__init__.py +1 -1
  89. agentpool/models/acp_agents/base.py +6 -24
  90. agentpool/models/acp_agents/mcp_capable.py +126 -157
  91. agentpool/models/acp_agents/non_mcp.py +129 -95
  92. agentpool/models/agents.py +98 -76
  93. agentpool/models/agui_agents.py +1 -1
  94. agentpool/models/claude_code_agents.py +144 -19
  95. agentpool/models/file_parsing.py +0 -1
  96. agentpool/models/manifest.py +113 -50
  97. agentpool/prompts/conversion_manager.py +1 -1
  98. agentpool/prompts/prompts.py +5 -2
  99. agentpool/repomap.py +1 -1
  100. agentpool/resource_providers/__init__.py +11 -1
  101. agentpool/resource_providers/aggregating.py +56 -5
  102. agentpool/resource_providers/base.py +70 -4
  103. agentpool/resource_providers/codemode/code_executor.py +72 -5
  104. agentpool/resource_providers/codemode/helpers.py +2 -2
  105. agentpool/resource_providers/codemode/provider.py +64 -12
  106. agentpool/resource_providers/codemode/remote_mcp_execution.py +2 -2
  107. agentpool/resource_providers/codemode/remote_provider.py +9 -12
  108. agentpool/resource_providers/filtering.py +3 -1
  109. agentpool/resource_providers/mcp_provider.py +89 -12
  110. agentpool/resource_providers/plan_provider.py +228 -46
  111. agentpool/resource_providers/pool.py +7 -3
  112. agentpool/resource_providers/resource_info.py +111 -0
  113. agentpool/resource_providers/static.py +4 -2
  114. agentpool/sessions/__init__.py +4 -1
  115. agentpool/sessions/manager.py +33 -5
  116. agentpool/sessions/models.py +59 -6
  117. agentpool/sessions/protocol.py +28 -0
  118. agentpool/sessions/session.py +11 -55
  119. agentpool/skills/registry.py +13 -8
  120. agentpool/storage/manager.py +572 -49
  121. agentpool/talk/registry.py +4 -4
  122. agentpool/talk/talk.py +9 -10
  123. agentpool/testing.py +538 -20
  124. agentpool/tool_impls/__init__.py +6 -0
  125. agentpool/tool_impls/agent_cli/__init__.py +42 -0
  126. agentpool/tool_impls/agent_cli/tool.py +95 -0
  127. agentpool/tool_impls/bash/__init__.py +64 -0
  128. agentpool/tool_impls/bash/helpers.py +35 -0
  129. agentpool/tool_impls/bash/tool.py +171 -0
  130. agentpool/tool_impls/delete_path/__init__.py +70 -0
  131. agentpool/tool_impls/delete_path/tool.py +142 -0
  132. agentpool/tool_impls/download_file/__init__.py +80 -0
  133. agentpool/tool_impls/download_file/tool.py +183 -0
  134. agentpool/tool_impls/execute_code/__init__.py +55 -0
  135. agentpool/tool_impls/execute_code/tool.py +163 -0
  136. agentpool/tool_impls/grep/__init__.py +80 -0
  137. agentpool/tool_impls/grep/tool.py +200 -0
  138. agentpool/tool_impls/list_directory/__init__.py +73 -0
  139. agentpool/tool_impls/list_directory/tool.py +197 -0
  140. agentpool/tool_impls/question/__init__.py +42 -0
  141. agentpool/tool_impls/question/tool.py +127 -0
  142. agentpool/tool_impls/read/__init__.py +104 -0
  143. agentpool/tool_impls/read/tool.py +305 -0
  144. agentpool/tools/__init__.py +2 -1
  145. agentpool/tools/base.py +114 -34
  146. agentpool/tools/manager.py +57 -1
  147. agentpool/ui/base.py +2 -2
  148. agentpool/ui/mock_provider.py +2 -2
  149. agentpool/ui/stdlib_provider.py +2 -2
  150. agentpool/utils/file_watcher.py +269 -0
  151. agentpool/utils/identifiers.py +121 -0
  152. agentpool/utils/pydantic_ai_helpers.py +46 -0
  153. agentpool/utils/streams.py +616 -2
  154. agentpool/utils/subprocess_utils.py +155 -0
  155. agentpool/utils/token_breakdown.py +461 -0
  156. agentpool/vfs_registry.py +7 -2
  157. {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/METADATA +41 -27
  158. agentpool-2.5.0.dist-info/RECORD +579 -0
  159. {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/WHEEL +1 -1
  160. agentpool_cli/__main__.py +24 -0
  161. agentpool_cli/create.py +1 -1
  162. agentpool_cli/serve_acp.py +100 -21
  163. agentpool_cli/serve_agui.py +87 -0
  164. agentpool_cli/serve_opencode.py +119 -0
  165. agentpool_cli/ui.py +557 -0
  166. agentpool_commands/__init__.py +42 -5
  167. agentpool_commands/agents.py +75 -2
  168. agentpool_commands/history.py +62 -0
  169. agentpool_commands/mcp.py +176 -0
  170. agentpool_commands/models.py +56 -3
  171. agentpool_commands/pool.py +260 -0
  172. agentpool_commands/session.py +1 -1
  173. agentpool_commands/text_sharing/__init__.py +119 -0
  174. agentpool_commands/text_sharing/base.py +123 -0
  175. agentpool_commands/text_sharing/github_gist.py +80 -0
  176. agentpool_commands/text_sharing/opencode.py +462 -0
  177. agentpool_commands/text_sharing/paste_rs.py +59 -0
  178. agentpool_commands/text_sharing/pastebin.py +116 -0
  179. agentpool_commands/text_sharing/shittycodingagent.py +112 -0
  180. agentpool_commands/tools.py +57 -0
  181. agentpool_commands/utils.py +80 -30
  182. agentpool_config/__init__.py +30 -2
  183. agentpool_config/agentpool_tools.py +498 -0
  184. agentpool_config/builtin_tools.py +77 -22
  185. agentpool_config/commands.py +24 -1
  186. agentpool_config/compaction.py +258 -0
  187. agentpool_config/converters.py +1 -1
  188. agentpool_config/event_handlers.py +42 -0
  189. agentpool_config/events.py +1 -1
  190. agentpool_config/forward_targets.py +1 -4
  191. agentpool_config/jinja.py +3 -3
  192. agentpool_config/mcp_server.py +132 -6
  193. agentpool_config/nodes.py +1 -1
  194. agentpool_config/observability.py +44 -0
  195. agentpool_config/session.py +0 -3
  196. agentpool_config/storage.py +82 -38
  197. agentpool_config/task.py +3 -3
  198. agentpool_config/tools.py +11 -22
  199. agentpool_config/toolsets.py +109 -233
  200. agentpool_server/a2a_server/agent_worker.py +307 -0
  201. agentpool_server/a2a_server/server.py +23 -18
  202. agentpool_server/acp_server/acp_agent.py +234 -181
  203. agentpool_server/acp_server/commands/acp_commands.py +151 -156
  204. agentpool_server/acp_server/commands/docs_commands/fetch_repo.py +18 -17
  205. agentpool_server/acp_server/event_converter.py +651 -0
  206. agentpool_server/acp_server/input_provider.py +53 -10
  207. agentpool_server/acp_server/server.py +24 -90
  208. agentpool_server/acp_server/session.py +173 -331
  209. agentpool_server/acp_server/session_manager.py +8 -34
  210. agentpool_server/agui_server/server.py +3 -1
  211. agentpool_server/mcp_server/server.py +5 -2
  212. agentpool_server/opencode_server/.rules +95 -0
  213. agentpool_server/opencode_server/ENDPOINTS.md +401 -0
  214. agentpool_server/opencode_server/OPENCODE_UI_TOOLS_COMPLETE.md +202 -0
  215. agentpool_server/opencode_server/__init__.py +19 -0
  216. agentpool_server/opencode_server/command_validation.py +172 -0
  217. agentpool_server/opencode_server/converters.py +975 -0
  218. agentpool_server/opencode_server/dependencies.py +24 -0
  219. agentpool_server/opencode_server/input_provider.py +421 -0
  220. agentpool_server/opencode_server/models/__init__.py +250 -0
  221. agentpool_server/opencode_server/models/agent.py +53 -0
  222. agentpool_server/opencode_server/models/app.py +72 -0
  223. agentpool_server/opencode_server/models/base.py +26 -0
  224. agentpool_server/opencode_server/models/common.py +23 -0
  225. agentpool_server/opencode_server/models/config.py +37 -0
  226. agentpool_server/opencode_server/models/events.py +821 -0
  227. agentpool_server/opencode_server/models/file.py +88 -0
  228. agentpool_server/opencode_server/models/mcp.py +44 -0
  229. agentpool_server/opencode_server/models/message.py +179 -0
  230. agentpool_server/opencode_server/models/parts.py +323 -0
  231. agentpool_server/opencode_server/models/provider.py +81 -0
  232. agentpool_server/opencode_server/models/pty.py +43 -0
  233. agentpool_server/opencode_server/models/question.py +56 -0
  234. agentpool_server/opencode_server/models/session.py +111 -0
  235. agentpool_server/opencode_server/routes/__init__.py +29 -0
  236. agentpool_server/opencode_server/routes/agent_routes.py +473 -0
  237. agentpool_server/opencode_server/routes/app_routes.py +202 -0
  238. agentpool_server/opencode_server/routes/config_routes.py +302 -0
  239. agentpool_server/opencode_server/routes/file_routes.py +571 -0
  240. agentpool_server/opencode_server/routes/global_routes.py +94 -0
  241. agentpool_server/opencode_server/routes/lsp_routes.py +319 -0
  242. agentpool_server/opencode_server/routes/message_routes.py +761 -0
  243. agentpool_server/opencode_server/routes/permission_routes.py +63 -0
  244. agentpool_server/opencode_server/routes/pty_routes.py +300 -0
  245. agentpool_server/opencode_server/routes/question_routes.py +128 -0
  246. agentpool_server/opencode_server/routes/session_routes.py +1276 -0
  247. agentpool_server/opencode_server/routes/tui_routes.py +139 -0
  248. agentpool_server/opencode_server/server.py +475 -0
  249. agentpool_server/opencode_server/state.py +151 -0
  250. agentpool_server/opencode_server/time_utils.py +8 -0
  251. agentpool_storage/__init__.py +12 -0
  252. agentpool_storage/base.py +184 -2
  253. agentpool_storage/claude_provider/ARCHITECTURE.md +433 -0
  254. agentpool_storage/claude_provider/__init__.py +42 -0
  255. agentpool_storage/claude_provider/provider.py +1089 -0
  256. agentpool_storage/file_provider.py +278 -15
  257. agentpool_storage/memory_provider.py +193 -12
  258. agentpool_storage/models.py +3 -0
  259. agentpool_storage/opencode_provider/ARCHITECTURE.md +386 -0
  260. agentpool_storage/opencode_provider/__init__.py +16 -0
  261. agentpool_storage/opencode_provider/helpers.py +414 -0
  262. agentpool_storage/opencode_provider/provider.py +895 -0
  263. agentpool_storage/project_store.py +325 -0
  264. agentpool_storage/session_store.py +26 -6
  265. agentpool_storage/sql_provider/__init__.py +4 -2
  266. agentpool_storage/sql_provider/models.py +48 -0
  267. agentpool_storage/sql_provider/sql_provider.py +269 -3
  268. agentpool_storage/sql_provider/utils.py +12 -13
  269. agentpool_storage/zed_provider/__init__.py +16 -0
  270. agentpool_storage/zed_provider/helpers.py +281 -0
  271. agentpool_storage/zed_provider/models.py +130 -0
  272. agentpool_storage/zed_provider/provider.py +442 -0
  273. agentpool_storage/zed_provider.py +803 -0
  274. agentpool_toolsets/__init__.py +0 -2
  275. agentpool_toolsets/builtin/__init__.py +2 -12
  276. agentpool_toolsets/builtin/code.py +96 -57
  277. agentpool_toolsets/builtin/debug.py +118 -48
  278. agentpool_toolsets/builtin/execution_environment.py +115 -230
  279. agentpool_toolsets/builtin/file_edit/file_edit.py +115 -7
  280. agentpool_toolsets/builtin/skills.py +9 -4
  281. agentpool_toolsets/builtin/subagent_tools.py +64 -51
  282. agentpool_toolsets/builtin/workers.py +4 -2
  283. agentpool_toolsets/composio_toolset.py +2 -2
  284. agentpool_toolsets/entry_points.py +3 -1
  285. agentpool_toolsets/fsspec_toolset/__init__.py +13 -1
  286. agentpool_toolsets/fsspec_toolset/diagnostics.py +860 -73
  287. agentpool_toolsets/fsspec_toolset/grep.py +99 -7
  288. agentpool_toolsets/fsspec_toolset/helpers.py +3 -2
  289. agentpool_toolsets/fsspec_toolset/image_utils.py +161 -0
  290. agentpool_toolsets/fsspec_toolset/toolset.py +500 -95
  291. agentpool_toolsets/mcp_discovery/__init__.py +5 -0
  292. agentpool_toolsets/mcp_discovery/data/mcp_servers.parquet +0 -0
  293. agentpool_toolsets/mcp_discovery/toolset.py +511 -0
  294. agentpool_toolsets/mcp_run_toolset.py +87 -12
  295. agentpool_toolsets/notifications.py +33 -33
  296. agentpool_toolsets/openapi.py +3 -1
  297. agentpool_toolsets/search_toolset.py +3 -1
  298. agentpool-2.1.9.dist-info/RECORD +0 -474
  299. agentpool_config/resources.py +0 -33
  300. agentpool_server/acp_server/acp_tools.py +0 -43
  301. agentpool_server/acp_server/commands/spawn.py +0 -210
  302. agentpool_storage/text_log_provider.py +0 -275
  303. agentpool_toolsets/builtin/agent_management.py +0 -239
  304. agentpool_toolsets/builtin/chain.py +0 -288
  305. agentpool_toolsets/builtin/history.py +0 -36
  306. agentpool_toolsets/builtin/integration.py +0 -85
  307. agentpool_toolsets/builtin/tool_management.py +0 -90
  308. agentpool_toolsets/builtin/user_interaction.py +0 -52
  309. agentpool_toolsets/semantic_memory_toolset.py +0 -536
  310. {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/entry_points.txt +0 -0
  311. {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,325 @@
1
+ """High-level project store with auto-detection."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ from pathlib import Path
7
+ from typing import TYPE_CHECKING
8
+
9
+ from agentpool.log import get_logger
10
+ from agentpool.sessions.models import ProjectData
11
+ from agentpool.utils.now import get_now
12
+
13
+
14
+ if TYPE_CHECKING:
15
+ from agentpool.storage.manager import StorageManager
16
+
17
+ logger = get_logger(__name__)
18
+
19
+ # Config file names to search for in .agentpool/ directory
20
+ CONFIG_FILENAMES = ["config.yml", "config.yaml", "config.json", "config.toml"]
21
+
22
+
23
+ def resolve_config(project: ProjectData | None = None, cwd: str | None = None) -> str | None:
24
+ """Resolve config path using project settings and global fallback.
25
+
26
+ Resolution order:
27
+ 1. Project's explicit config_path (if set)
28
+ 2. .agentpool/config.yml in project worktree (auto-discovered)
29
+ 3. Global default from ConfigStore (CLI fallback)
30
+ 4. None (use built-in defaults)
31
+
32
+ Args:
33
+ project: Project data (optional)
34
+ cwd: Current working directory for discovery if no project
35
+
36
+ Returns:
37
+ Path to config file, or None if no config found
38
+ """
39
+ # 1. Project-specific explicit config
40
+ if project and project.config_path:
41
+ config_path = Path(project.config_path)
42
+ if config_path.is_file():
43
+ return str(config_path)
44
+ logger.warning("Project config not found", path=project.config_path)
45
+
46
+ # 2. Auto-discover in project worktree
47
+ worktree = project.worktree if project else cwd
48
+ if worktree:
49
+ local_config = discover_config_path(worktree)
50
+ if local_config:
51
+ return local_config
52
+
53
+ # 3. Global default from ConfigStore
54
+ try:
55
+ from agentpool_cli.store import ConfigStore
56
+
57
+ config_store = ConfigStore()
58
+ if active := config_store.get_active():
59
+ config_path = Path(active.path)
60
+ if config_path.is_file():
61
+ return str(config_path)
62
+ logger.warning("Active config not found", path=active.path)
63
+ except ImportError:
64
+ # CLI not installed, skip fallback
65
+ pass
66
+ except Exception:
67
+ logger.exception("Error loading ConfigStore")
68
+
69
+ # 4. No config found
70
+ return None
71
+
72
+
73
+ def detect_project_root(cwd: str) -> tuple[str, str | None]:
74
+ """Walk up directory tree to find VCS root or use cwd.
75
+
76
+ Args:
77
+ cwd: Current working directory
78
+
79
+ Returns:
80
+ Tuple of (worktree_path, vcs_type).
81
+ vcs_type is "git", "hg", or None if no VCS found.
82
+ """
83
+ path = Path(cwd).resolve()
84
+ for parent in [path, *path.parents]:
85
+ if (parent / ".git").exists():
86
+ return str(parent), "git"
87
+ if (parent / ".hg").exists():
88
+ return str(parent), "hg"
89
+ # No VCS found, use the original directory
90
+ return str(path), None
91
+
92
+
93
+ def generate_project_id(worktree: str) -> str:
94
+ """Generate stable hash of canonical worktree path.
95
+
96
+ Args:
97
+ worktree: Absolute path to project root
98
+
99
+ Returns:
100
+ 40-character hex string (SHA1 hash)
101
+ """
102
+ canonical = str(Path(worktree).resolve())
103
+ return hashlib.sha1(canonical.encode()).hexdigest()
104
+
105
+
106
+ def discover_config_path(worktree: str) -> str | None:
107
+ """Search for config file in .agentpool/ directory.
108
+
109
+ Args:
110
+ worktree: Project root path
111
+
112
+ Returns:
113
+ Path to config file if found, None otherwise
114
+ """
115
+ config_dir = Path(worktree) / ".agentpool"
116
+ if not config_dir.is_dir():
117
+ return None
118
+
119
+ for filename in CONFIG_FILENAMES:
120
+ config_path = config_dir / filename
121
+ if config_path.is_file():
122
+ return str(config_path)
123
+ return None
124
+
125
+
126
+ class ProjectStore:
127
+ """High-level API for project management.
128
+
129
+ Provides:
130
+ - Auto-detection of project root from cwd
131
+ - Auto-creation of projects
132
+ - Config file discovery
133
+ - Convenient lookup methods
134
+ """
135
+
136
+ def __init__(self, storage: StorageManager) -> None:
137
+ """Initialize project store.
138
+
139
+ Args:
140
+ storage: Storage manager for persistence
141
+ """
142
+ self.storage = storage
143
+
144
+ async def get_or_create(self, cwd: str) -> ProjectData:
145
+ """Get existing project or create one for the given directory.
146
+
147
+ Detects VCS root, generates project ID, discovers config,
148
+ and creates/updates the project.
149
+
150
+ Args:
151
+ cwd: Current working directory
152
+
153
+ Returns:
154
+ ProjectData for the detected/created project
155
+ """
156
+ # Detect project root and VCS
157
+ worktree, vcs = detect_project_root(cwd)
158
+
159
+ # Check if project already exists
160
+ existing = await self.storage.get_project_by_worktree(worktree)
161
+ if existing:
162
+ # Update last_active and return
163
+ await self.storage.touch_project(existing.project_id)
164
+ return existing.touch()
165
+
166
+ # Create new project
167
+ project_id = generate_project_id(worktree)
168
+ config_path = discover_config_path(worktree)
169
+
170
+ project = ProjectData(
171
+ project_id=project_id,
172
+ worktree=worktree,
173
+ name=None, # Can be set later
174
+ vcs=vcs,
175
+ config_path=config_path,
176
+ created_at=get_now(),
177
+ last_active=get_now(),
178
+ settings={},
179
+ )
180
+
181
+ await self.storage.save_project(project)
182
+ logger.info(
183
+ "Created project",
184
+ project_id=project_id,
185
+ worktree=worktree,
186
+ vcs=vcs,
187
+ config_path=config_path,
188
+ )
189
+ return project
190
+
191
+ async def get_by_cwd(self, cwd: str) -> ProjectData | None:
192
+ """Get project for the given directory without creating.
193
+
194
+ Args:
195
+ cwd: Current working directory
196
+
197
+ Returns:
198
+ ProjectData if found, None otherwise
199
+ """
200
+ worktree, _ = detect_project_root(cwd)
201
+ return await self.storage.get_project_by_worktree(worktree)
202
+
203
+ async def get_by_name(self, name: str) -> ProjectData | None:
204
+ """Get project by friendly name.
205
+
206
+ Args:
207
+ name: Project name
208
+
209
+ Returns:
210
+ ProjectData if found, None otherwise
211
+ """
212
+ return await self.storage.get_project_by_name(name)
213
+
214
+ async def get_by_id(self, project_id: str) -> ProjectData | None:
215
+ """Get project by ID.
216
+
217
+ Args:
218
+ project_id: Project identifier
219
+
220
+ Returns:
221
+ ProjectData if found, None otherwise
222
+ """
223
+ return await self.storage.get_project(project_id)
224
+
225
+ async def list_recent(self, limit: int = 20) -> list[ProjectData]:
226
+ """List recent projects ordered by last_active.
227
+
228
+ Args:
229
+ limit: Maximum number of projects to return
230
+
231
+ Returns:
232
+ List of projects
233
+ """
234
+ projects = await self.storage.list_projects(limit=limit)
235
+ return projects if projects is not None else []
236
+
237
+ async def set_name(self, project_id: str, name: str) -> ProjectData | None:
238
+ """Set friendly name for a project.
239
+
240
+ Args:
241
+ project_id: Project identifier
242
+ name: New name for the project
243
+
244
+ Returns:
245
+ Updated ProjectData, or None if project not found
246
+ """
247
+ project = await self.storage.get_project(project_id)
248
+ if not project:
249
+ return None
250
+
251
+ updated = project.model_copy(update={"name": name, "last_active": get_now()})
252
+ await self.storage.save_project(updated)
253
+ return updated
254
+
255
+ async def set_config_path(self, project_id: str, config_path: str | None) -> ProjectData | None:
256
+ """Set or clear config path for a project.
257
+
258
+ Args:
259
+ project_id: Project identifier
260
+ config_path: Path to config file, or None to use auto-discovery
261
+
262
+ Returns:
263
+ Updated ProjectData, or None if project not found
264
+ """
265
+ project = await self.storage.get_project(project_id)
266
+ if not project:
267
+ return None
268
+
269
+ updated = project.model_copy(update={"config_path": config_path, "last_active": get_now()})
270
+ await self.storage.save_project(updated)
271
+ return updated
272
+
273
+ async def update_settings(self, project_id: str, **settings: object) -> ProjectData | None:
274
+ """Update project-specific settings.
275
+
276
+ Args:
277
+ project_id: Project identifier
278
+ **settings: Settings to update
279
+
280
+ Returns:
281
+ Updated ProjectData, or None if project not found
282
+ """
283
+ project = await self.storage.get_project(project_id)
284
+ if not project:
285
+ return None
286
+
287
+ updated = project.with_settings(**settings)
288
+ await self.storage.save_project(updated)
289
+ return updated
290
+
291
+ async def delete(self, project_id: str) -> bool:
292
+ """Delete a project.
293
+
294
+ Args:
295
+ project_id: Project identifier
296
+
297
+ Returns:
298
+ True if deleted, False if not found
299
+ """
300
+ result = await self.storage.delete_project(project_id)
301
+ return bool(result)
302
+
303
+ async def refresh_config(self, project_id: str) -> ProjectData | None:
304
+ """Re-discover config file for a project.
305
+
306
+ Useful after user adds .agentpool/config.yml to their project.
307
+
308
+ Args:
309
+ project_id: Project identifier
310
+
311
+ Returns:
312
+ Updated ProjectData, or None if project not found
313
+ """
314
+ project = await self.storage.get_project(project_id)
315
+ if not project:
316
+ return None
317
+
318
+ config_path = discover_config_path(project.worktree)
319
+ if config_path != project.config_path:
320
+ updated = project.model_copy(
321
+ update={"config_path": config_path, "last_active": get_now()}
322
+ )
323
+ await self.storage.save_project(updated)
324
+ return updated
325
+ return project
@@ -5,10 +5,6 @@ from __future__ import annotations
5
5
  from datetime import timedelta
6
6
  from typing import TYPE_CHECKING, Any, Self
7
7
 
8
- from sqlalchemy import delete, select
9
- from sqlalchemy.ext.asyncio import AsyncSession
10
- from sqlmodel import SQLModel
11
-
12
8
  from agentpool.log import get_logger
13
9
  from agentpool.sessions.models import SessionData
14
10
  from agentpool.utils.now import get_now
@@ -45,6 +41,8 @@ class SQLSessionStore:
45
41
 
46
42
  async def __aenter__(self) -> Self:
47
43
  """Initialize database connection and create tables."""
44
+ from sqlmodel import SQLModel
45
+
48
46
  self._engine = self._config.get_engine()
49
47
 
50
48
  async with self._engine.begin() as conn:
@@ -95,7 +93,9 @@ class SQLSessionStore:
95
93
  agent_name=data.agent_name,
96
94
  conversation_id=data.conversation_id,
97
95
  pool_id=data.pool_id,
98
- title=data.title,
96
+ project_id=data.project_id,
97
+ parent_id=data.parent_id,
98
+ version=data.version,
99
99
  cwd=data.cwd,
100
100
  created_at=data.created_at,
101
101
  last_active=data.last_active,
@@ -109,7 +109,9 @@ class SQLSessionStore:
109
109
  agent_name=row.agent_name,
110
110
  conversation_id=row.conversation_id,
111
111
  pool_id=row.pool_id,
112
- title=row.title,
112
+ project_id=row.project_id,
113
+ parent_id=row.parent_id,
114
+ version=row.version,
113
115
  cwd=row.cwd,
114
116
  created_at=row.created_at,
115
117
  last_active=row.last_active,
@@ -124,6 +126,9 @@ class SQLSessionStore:
124
126
  Args:
125
127
  data: Session data to persist
126
128
  """
129
+ from sqlalchemy import delete
130
+ from sqlalchemy.ext.asyncio import AsyncSession
131
+
127
132
  engine = self._get_engine()
128
133
 
129
134
  async with AsyncSession(engine) as session:
@@ -146,6 +151,9 @@ class SQLSessionStore:
146
151
  Returns:
147
152
  Session data if found, None otherwise
148
153
  """
154
+ from sqlalchemy import select
155
+ from sqlalchemy.ext.asyncio import AsyncSession
156
+
149
157
  engine = self._get_engine()
150
158
 
151
159
  async with AsyncSession(engine) as session:
@@ -167,6 +175,9 @@ class SQLSessionStore:
167
175
  Returns:
168
176
  True if session was deleted, False if not found
169
177
  """
178
+ from sqlalchemy import delete
179
+ from sqlalchemy.ext.asyncio import AsyncSession
180
+
170
181
  engine = self._get_engine()
171
182
 
172
183
  async with AsyncSession(engine) as session:
@@ -193,6 +204,9 @@ class SQLSessionStore:
193
204
  Returns:
194
205
  List of session IDs
195
206
  """
207
+ from sqlalchemy import select
208
+ from sqlalchemy.ext.asyncio import AsyncSession
209
+
196
210
  engine = self._get_engine()
197
211
 
198
212
  async with AsyncSession(engine) as session:
@@ -217,6 +231,9 @@ class SQLSessionStore:
217
231
  Returns:
218
232
  Number of sessions removed
219
233
  """
234
+ from sqlalchemy import delete
235
+ from sqlalchemy.ext.asyncio import AsyncSession
236
+
220
237
  engine = self._get_engine()
221
238
  cutoff = get_now() - timedelta(hours=max_age_hours)
222
239
 
@@ -244,6 +261,9 @@ class SQLSessionStore:
244
261
  Returns:
245
262
  List of session data objects
246
263
  """
264
+ from sqlalchemy import select
265
+ from sqlalchemy.ext.asyncio import AsyncSession
266
+
247
267
  engine = self._get_engine()
248
268
 
249
269
  async with AsyncSession(engine) as session:
@@ -4,11 +4,12 @@ from __future__ import annotations
4
4
 
5
5
  from agentpool_storage.sql_provider.sql_provider import SQLModelProvider
6
6
  from agentpool_storage.sql_provider.models import (
7
+ CommandHistory,
7
8
  Conversation,
9
+ ConversationLog,
8
10
  Message,
9
- CommandHistory,
10
11
  MessageLog,
11
- ConversationLog,
12
+ Project,
12
13
  )
13
14
 
14
15
  __all__ = [
@@ -17,5 +18,6 @@ __all__ = [
17
18
  "ConversationLog",
18
19
  "Message",
19
20
  "MessageLog",
21
+ "Project",
20
22
  "SQLModelProvider",
21
23
  ]
@@ -127,6 +127,9 @@ class Message(AsyncAttrs, SQLModel, table=True):
127
127
  conversation_id: str = Field(index=True)
128
128
  """ID of the conversation this message belongs to"""
129
129
 
130
+ parent_id: str | None = Field(default=None, index=True)
131
+ """ID of the parent message for tree-structured conversations."""
132
+
130
133
  timestamp: datetime = Field(
131
134
  sa_column=Column(UTCDateTime, default=get_now), default_factory=get_now
132
135
  )
@@ -201,6 +204,15 @@ class Session(AsyncAttrs, SQLModel, table=True):
201
204
  pool_id: str | None = Field(default=None, index=True)
202
205
  """Optional pool/manifest identifier for multi-pool setups."""
203
206
 
207
+ project_id: str | None = Field(default=None, index=True)
208
+ """Project identifier (e.g., for OpenCode compatibility)."""
209
+
210
+ parent_id: str | None = Field(default=None, index=True)
211
+ """Parent session ID for forked sessions."""
212
+
213
+ version: str = Field(default="1")
214
+ """Session version string."""
215
+
204
216
  title: str | None = Field(default=None, index=True)
205
217
  """AI-generated or user-provided title for the conversation."""
206
218
 
@@ -225,6 +237,42 @@ class Session(AsyncAttrs, SQLModel, table=True):
225
237
  model_config = SQLModelConfig(use_attribute_docstrings=True) # pyright: ignore[reportCallIssue]
226
238
 
227
239
 
240
+ class Project(AsyncAttrs, SQLModel, table=True):
241
+ """Database model for project/worktree tracking."""
242
+
243
+ project_id: str = Field(primary_key=True)
244
+ """Unique identifier (hash of canonical worktree path)."""
245
+
246
+ worktree: str = Field(sa_column=Column(Text, index=True, unique=True))
247
+ """Absolute path to the project root/worktree."""
248
+
249
+ name: str | None = Field(default=None, index=True)
250
+ """Optional friendly name for the project."""
251
+
252
+ vcs: str | None = Field(default=None)
253
+ """Version control system type (git, hg, or None)."""
254
+
255
+ config_path: str | None = Field(default=None, sa_column=Column(Text))
256
+ """Path to the project's config file, or None for auto-discovery."""
257
+
258
+ created_at: datetime = Field(
259
+ sa_column=Column(UTCDateTime, index=True),
260
+ default_factory=get_now,
261
+ )
262
+ """When the project was first registered."""
263
+
264
+ last_active: datetime = Field(
265
+ sa_column=Column(UTCDateTime, index=True),
266
+ default_factory=get_now,
267
+ )
268
+ """Last activity timestamp."""
269
+
270
+ settings_json: dict[str, Any] = Field(default_factory=dict, sa_column=Column(JSON))
271
+ """Project-specific settings overrides."""
272
+
273
+ model_config = SQLModelConfig(use_attribute_docstrings=True) # pyright: ignore[reportCallIssue]
274
+
275
+
228
276
  class Conversation(AsyncAttrs, SQLModel, table=True):
229
277
  """Database model for conversations."""
230
278