agentpool 2.1.9__py3-none-any.whl → 2.2.3__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 (174) hide show
  1. acp/__init__.py +13 -0
  2. acp/bridge/README.md +15 -2
  3. acp/bridge/__init__.py +3 -2
  4. acp/bridge/__main__.py +60 -19
  5. acp/bridge/ws_server.py +173 -0
  6. acp/bridge/ws_server_cli.py +89 -0
  7. acp/notifications.py +2 -1
  8. acp/stdio.py +39 -9
  9. acp/transports.py +362 -2
  10. acp/utils.py +15 -2
  11. agentpool/__init__.py +4 -1
  12. agentpool/agents/__init__.py +2 -0
  13. agentpool/agents/acp_agent/acp_agent.py +203 -88
  14. agentpool/agents/acp_agent/acp_converters.py +46 -21
  15. agentpool/agents/acp_agent/client_handler.py +157 -3
  16. agentpool/agents/acp_agent/session_state.py +4 -1
  17. agentpool/agents/agent.py +314 -107
  18. agentpool/agents/agui_agent/__init__.py +0 -2
  19. agentpool/agents/agui_agent/agui_agent.py +90 -21
  20. agentpool/agents/agui_agent/agui_converters.py +0 -131
  21. agentpool/agents/base_agent.py +163 -1
  22. agentpool/agents/claude_code_agent/claude_code_agent.py +626 -179
  23. agentpool/agents/claude_code_agent/converters.py +71 -3
  24. agentpool/agents/claude_code_agent/history.py +474 -0
  25. agentpool/agents/context.py +40 -0
  26. agentpool/agents/events/__init__.py +2 -0
  27. agentpool/agents/events/builtin_handlers.py +2 -1
  28. agentpool/agents/events/event_emitter.py +29 -2
  29. agentpool/agents/events/events.py +20 -0
  30. agentpool/agents/modes.py +54 -0
  31. agentpool/agents/tool_call_accumulator.py +213 -0
  32. agentpool/common_types.py +21 -0
  33. agentpool/config_resources/__init__.py +38 -1
  34. agentpool/config_resources/claude_code_agent.yml +3 -0
  35. agentpool/delegation/pool.py +37 -29
  36. agentpool/delegation/team.py +1 -0
  37. agentpool/delegation/teamrun.py +1 -0
  38. agentpool/diagnostics/__init__.py +53 -0
  39. agentpool/diagnostics/lsp_manager.py +1593 -0
  40. agentpool/diagnostics/lsp_proxy.py +41 -0
  41. agentpool/diagnostics/lsp_proxy_script.py +229 -0
  42. agentpool/diagnostics/models.py +398 -0
  43. agentpool/mcp_server/__init__.py +0 -2
  44. agentpool/mcp_server/client.py +12 -3
  45. agentpool/mcp_server/manager.py +25 -31
  46. agentpool/mcp_server/registries/official_registry_client.py +25 -0
  47. agentpool/mcp_server/tool_bridge.py +78 -66
  48. agentpool/messaging/__init__.py +0 -2
  49. agentpool/messaging/compaction.py +72 -197
  50. agentpool/messaging/message_history.py +12 -0
  51. agentpool/messaging/messages.py +52 -9
  52. agentpool/messaging/processing.py +3 -1
  53. agentpool/models/acp_agents/base.py +0 -22
  54. agentpool/models/acp_agents/mcp_capable.py +8 -148
  55. agentpool/models/acp_agents/non_mcp.py +129 -72
  56. agentpool/models/agents.py +35 -13
  57. agentpool/models/claude_code_agents.py +33 -2
  58. agentpool/models/manifest.py +43 -0
  59. agentpool/repomap.py +1 -1
  60. agentpool/resource_providers/__init__.py +9 -1
  61. agentpool/resource_providers/aggregating.py +52 -3
  62. agentpool/resource_providers/base.py +57 -1
  63. agentpool/resource_providers/mcp_provider.py +23 -0
  64. agentpool/resource_providers/plan_provider.py +130 -41
  65. agentpool/resource_providers/pool.py +2 -0
  66. agentpool/resource_providers/static.py +2 -0
  67. agentpool/sessions/__init__.py +2 -1
  68. agentpool/sessions/manager.py +31 -2
  69. agentpool/sessions/models.py +50 -0
  70. agentpool/skills/registry.py +13 -8
  71. agentpool/storage/manager.py +217 -1
  72. agentpool/testing.py +537 -19
  73. agentpool/utils/file_watcher.py +269 -0
  74. agentpool/utils/identifiers.py +121 -0
  75. agentpool/utils/pydantic_ai_helpers.py +46 -0
  76. agentpool/utils/streams.py +690 -1
  77. agentpool/utils/subprocess_utils.py +155 -0
  78. agentpool/utils/token_breakdown.py +461 -0
  79. {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/METADATA +27 -7
  80. {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/RECORD +170 -112
  81. {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/WHEEL +1 -1
  82. agentpool_cli/__main__.py +4 -0
  83. agentpool_cli/serve_acp.py +41 -20
  84. agentpool_cli/serve_agui.py +87 -0
  85. agentpool_cli/serve_opencode.py +119 -0
  86. agentpool_commands/__init__.py +30 -0
  87. agentpool_commands/agents.py +74 -1
  88. agentpool_commands/history.py +62 -0
  89. agentpool_commands/mcp.py +176 -0
  90. agentpool_commands/models.py +56 -3
  91. agentpool_commands/tools.py +57 -0
  92. agentpool_commands/utils.py +51 -0
  93. agentpool_config/builtin_tools.py +77 -22
  94. agentpool_config/commands.py +24 -1
  95. agentpool_config/compaction.py +258 -0
  96. agentpool_config/mcp_server.py +131 -1
  97. agentpool_config/storage.py +46 -1
  98. agentpool_config/tools.py +7 -1
  99. agentpool_config/toolsets.py +92 -148
  100. agentpool_server/acp_server/acp_agent.py +134 -150
  101. agentpool_server/acp_server/commands/acp_commands.py +216 -51
  102. agentpool_server/acp_server/commands/docs_commands/fetch_repo.py +10 -10
  103. agentpool_server/acp_server/server.py +23 -79
  104. agentpool_server/acp_server/session.py +181 -19
  105. agentpool_server/opencode_server/.rules +95 -0
  106. agentpool_server/opencode_server/ENDPOINTS.md +362 -0
  107. agentpool_server/opencode_server/__init__.py +27 -0
  108. agentpool_server/opencode_server/command_validation.py +172 -0
  109. agentpool_server/opencode_server/converters.py +869 -0
  110. agentpool_server/opencode_server/dependencies.py +24 -0
  111. agentpool_server/opencode_server/input_provider.py +269 -0
  112. agentpool_server/opencode_server/models/__init__.py +228 -0
  113. agentpool_server/opencode_server/models/agent.py +53 -0
  114. agentpool_server/opencode_server/models/app.py +60 -0
  115. agentpool_server/opencode_server/models/base.py +26 -0
  116. agentpool_server/opencode_server/models/common.py +23 -0
  117. agentpool_server/opencode_server/models/config.py +37 -0
  118. agentpool_server/opencode_server/models/events.py +647 -0
  119. agentpool_server/opencode_server/models/file.py +88 -0
  120. agentpool_server/opencode_server/models/mcp.py +25 -0
  121. agentpool_server/opencode_server/models/message.py +162 -0
  122. agentpool_server/opencode_server/models/parts.py +190 -0
  123. agentpool_server/opencode_server/models/provider.py +81 -0
  124. agentpool_server/opencode_server/models/pty.py +43 -0
  125. agentpool_server/opencode_server/models/session.py +99 -0
  126. agentpool_server/opencode_server/routes/__init__.py +25 -0
  127. agentpool_server/opencode_server/routes/agent_routes.py +442 -0
  128. agentpool_server/opencode_server/routes/app_routes.py +139 -0
  129. agentpool_server/opencode_server/routes/config_routes.py +241 -0
  130. agentpool_server/opencode_server/routes/file_routes.py +392 -0
  131. agentpool_server/opencode_server/routes/global_routes.py +94 -0
  132. agentpool_server/opencode_server/routes/lsp_routes.py +319 -0
  133. agentpool_server/opencode_server/routes/message_routes.py +705 -0
  134. agentpool_server/opencode_server/routes/pty_routes.py +299 -0
  135. agentpool_server/opencode_server/routes/session_routes.py +1205 -0
  136. agentpool_server/opencode_server/routes/tui_routes.py +139 -0
  137. agentpool_server/opencode_server/server.py +430 -0
  138. agentpool_server/opencode_server/state.py +121 -0
  139. agentpool_server/opencode_server/time_utils.py +8 -0
  140. agentpool_storage/__init__.py +16 -0
  141. agentpool_storage/base.py +103 -0
  142. agentpool_storage/claude_provider.py +907 -0
  143. agentpool_storage/file_provider.py +129 -0
  144. agentpool_storage/memory_provider.py +61 -0
  145. agentpool_storage/models.py +3 -0
  146. agentpool_storage/opencode_provider.py +730 -0
  147. agentpool_storage/project_store.py +325 -0
  148. agentpool_storage/session_store.py +6 -0
  149. agentpool_storage/sql_provider/__init__.py +4 -2
  150. agentpool_storage/sql_provider/models.py +48 -0
  151. agentpool_storage/sql_provider/sql_provider.py +134 -1
  152. agentpool_storage/sql_provider/utils.py +10 -1
  153. agentpool_storage/text_log_provider.py +1 -0
  154. agentpool_toolsets/builtin/__init__.py +0 -8
  155. agentpool_toolsets/builtin/code.py +95 -56
  156. agentpool_toolsets/builtin/debug.py +16 -21
  157. agentpool_toolsets/builtin/execution_environment.py +99 -103
  158. agentpool_toolsets/builtin/file_edit/file_edit.py +115 -7
  159. agentpool_toolsets/builtin/skills.py +86 -4
  160. agentpool_toolsets/fsspec_toolset/__init__.py +13 -1
  161. agentpool_toolsets/fsspec_toolset/diagnostics.py +860 -73
  162. agentpool_toolsets/fsspec_toolset/grep.py +74 -2
  163. agentpool_toolsets/fsspec_toolset/image_utils.py +161 -0
  164. agentpool_toolsets/fsspec_toolset/toolset.py +159 -38
  165. agentpool_toolsets/mcp_discovery/__init__.py +5 -0
  166. agentpool_toolsets/mcp_discovery/data/mcp_servers.parquet +0 -0
  167. agentpool_toolsets/mcp_discovery/toolset.py +454 -0
  168. agentpool_toolsets/mcp_run_toolset.py +84 -6
  169. agentpool_toolsets/builtin/agent_management.py +0 -239
  170. agentpool_toolsets/builtin/history.py +0 -36
  171. agentpool_toolsets/builtin/integration.py +0 -85
  172. agentpool_toolsets/builtin/tool_management.py +0 -90
  173. {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/entry_points.txt +0 -0
  174. {agentpool-2.1.9.dist-info → agentpool-2.2.3.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
@@ -95,6 +95,9 @@ class SQLSessionStore:
95
95
  agent_name=data.agent_name,
96
96
  conversation_id=data.conversation_id,
97
97
  pool_id=data.pool_id,
98
+ project_id=data.project_id,
99
+ parent_id=data.parent_id,
100
+ version=data.version,
98
101
  title=data.title,
99
102
  cwd=data.cwd,
100
103
  created_at=data.created_at,
@@ -109,6 +112,9 @@ class SQLSessionStore:
109
112
  agent_name=row.agent_name,
110
113
  conversation_id=row.conversation_id,
111
114
  pool_id=row.pool_id,
115
+ project_id=row.project_id,
116
+ parent_id=row.parent_id,
117
+ version=row.version,
112
118
  title=row.title,
113
119
  cwd=row.cwd,
114
120
  created_at=row.created_at,
@@ -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
 
@@ -15,7 +15,12 @@ from agentpool.utils.now import get_now
15
15
  from agentpool.utils.parse_time import parse_time_period
16
16
  from agentpool_storage.base import StorageProvider
17
17
  from agentpool_storage.models import QueryFilters
18
- from agentpool_storage.sql_provider.models import CommandHistory, Conversation, Message
18
+ from agentpool_storage.sql_provider.models import (
19
+ CommandHistory,
20
+ Conversation,
21
+ Message,
22
+ Project,
23
+ )
19
24
  from agentpool_storage.sql_provider.utils import (
20
25
  build_message_query,
21
26
  format_conversation,
@@ -33,6 +38,7 @@ if TYPE_CHECKING:
33
38
 
34
39
  from agentpool.common_types import JsonValue
35
40
  from agentpool.messaging import ChatMessage
41
+ from agentpool.sessions.models import ProjectData
36
42
  from agentpool_config.session import SessionQuery
37
43
  from agentpool_config.storage import SQLStorageConfig
38
44
  from agentpool_storage.models import ConversationData, StatsFilters
@@ -120,6 +126,7 @@ class SQLModelProvider(StorageProvider):
120
126
  content: str,
121
127
  role: str,
122
128
  name: str | None = None,
129
+ parent_id: str | None = None,
123
130
  cost_info: TokenCost | None = None,
124
131
  model: str | None = None,
125
132
  response_time: float | None = None,
@@ -138,6 +145,7 @@ class SQLModelProvider(StorageProvider):
138
145
  msg = Message(
139
146
  conversation_id=conversation_id,
140
147
  id=message_id,
148
+ parent_id=parent_id,
141
149
  content=content,
142
150
  role=role,
143
151
  name=name,
@@ -442,3 +450,128 @@ class SQLModelProvider(StorageProvider):
442
450
  msg_count = len(msg_result.scalars().all())
443
451
 
444
452
  return conv_count, msg_count
453
+
454
+ async def delete_conversation_messages(
455
+ self,
456
+ conversation_id: str,
457
+ ) -> int:
458
+ """Delete all messages for a conversation."""
459
+ from sqlalchemy import delete, func
460
+
461
+ async with AsyncSession(self.engine) as session:
462
+ # First count messages to return
463
+ count_result = await session.execute(
464
+ select(func.count()).where(Message.conversation_id == conversation_id)
465
+ )
466
+ count = count_result.scalar() or 0
467
+
468
+ # Then delete
469
+ await session.execute(
470
+ delete(Message).where(Message.conversation_id == conversation_id) # type: ignore[arg-type]
471
+ )
472
+ await session.commit()
473
+ return count
474
+
475
+ # Project methods
476
+
477
+ def _to_project_data(self, row: Project) -> ProjectData:
478
+ """Convert database model to ProjectData."""
479
+ from agentpool.sessions.models import ProjectData
480
+
481
+ return ProjectData(
482
+ project_id=row.project_id,
483
+ worktree=row.worktree,
484
+ name=row.name,
485
+ vcs=row.vcs,
486
+ config_path=row.config_path,
487
+ created_at=row.created_at,
488
+ last_active=row.last_active,
489
+ settings=row.settings_json or {},
490
+ )
491
+
492
+ def _to_project_model(self, data: ProjectData) -> Project:
493
+ """Convert ProjectData to database model."""
494
+ return Project(
495
+ project_id=data.project_id,
496
+ worktree=data.worktree,
497
+ name=data.name,
498
+ vcs=data.vcs,
499
+ config_path=data.config_path,
500
+ created_at=data.created_at,
501
+ last_active=data.last_active,
502
+ settings_json=data.settings,
503
+ )
504
+
505
+ async def save_project(self, project: ProjectData) -> None:
506
+ """Save or update a project."""
507
+ from sqlalchemy import delete
508
+
509
+ async with AsyncSession(self.engine) as session:
510
+ # Delete existing if present (upsert via delete+insert)
511
+ stmt = delete(Project).where(Project.project_id == project.project_id) # type: ignore[arg-type]
512
+ await session.execute(stmt)
513
+
514
+ # Insert new/updated
515
+ db_project = self._to_project_model(project)
516
+ session.add(db_project)
517
+ await session.commit()
518
+ logger.debug("Saved project", project_id=project.project_id)
519
+
520
+ async def get_project(self, project_id: str) -> ProjectData | None:
521
+ """Get a project by ID."""
522
+ async with AsyncSession(self.engine) as session:
523
+ stmt = select(Project).where(Project.project_id == project_id)
524
+ result = await session.execute(stmt)
525
+ row = result.scalars().first()
526
+ return self._to_project_data(row) if row else None
527
+
528
+ async def get_project_by_worktree(self, worktree: str) -> ProjectData | None:
529
+ """Get a project by worktree path."""
530
+ async with AsyncSession(self.engine) as session:
531
+ stmt = select(Project).where(Project.worktree == worktree)
532
+ result = await session.execute(stmt)
533
+ row = result.scalars().first()
534
+ return self._to_project_data(row) if row else None
535
+
536
+ async def get_project_by_name(self, name: str) -> ProjectData | None:
537
+ """Get a project by friendly name."""
538
+ async with AsyncSession(self.engine) as session:
539
+ stmt = select(Project).where(Project.name == name)
540
+ result = await session.execute(stmt)
541
+ row = result.scalars().first()
542
+ return self._to_project_data(row) if row else None
543
+
544
+ async def list_projects(self, limit: int | None = None) -> list[ProjectData]:
545
+ """List all projects, ordered by last_active descending."""
546
+ async with AsyncSession(self.engine) as session:
547
+ stmt = select(Project).order_by(desc(Project.last_active))
548
+ if limit is not None:
549
+ stmt = stmt.limit(limit)
550
+ result = await session.execute(stmt)
551
+ return [self._to_project_data(row) for row in result.scalars().all()]
552
+
553
+ async def delete_project(self, project_id: str) -> bool:
554
+ """Delete a project."""
555
+ from sqlalchemy import delete
556
+
557
+ async with AsyncSession(self.engine) as session:
558
+ stmt = delete(Project).where(Project.project_id == project_id) # type: ignore[arg-type]
559
+ result = await session.execute(stmt)
560
+ await session.commit()
561
+ deleted: bool = result.rowcount > 0 # type: ignore[attr-defined]
562
+ if deleted:
563
+ logger.debug("Deleted project", project_id=project_id)
564
+ return deleted
565
+
566
+ async def touch_project(self, project_id: str) -> None:
567
+ """Update project's last_active timestamp."""
568
+ from sqlalchemy import update
569
+
570
+ async with AsyncSession(self.engine) as session:
571
+ stmt = (
572
+ update(Project)
573
+ .where(Project.project_id == project_id) # type: ignore[arg-type]
574
+ .values(last_active=get_now())
575
+ )
576
+ await session.execute(stmt)
577
+ await session.commit()
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import contextlib
5
6
  from datetime import datetime
6
7
  from decimal import Decimal
7
8
  from typing import TYPE_CHECKING, Any
@@ -101,9 +102,14 @@ def auto_migrate_columns(sync_conn: Any, dialect: Any) -> None:
101
102
  from sqlmodel import SQLModel
102
103
 
103
104
  inspector = inspect(sync_conn)
105
+ existing_tables = set(inspector.get_table_names())
104
106
 
105
107
  # For each table in our models
106
108
  for table_name, table in SQLModel.metadata.tables.items():
109
+ # Skip tables that don't exist yet (they'll be created fresh)
110
+ if table_name not in existing_tables:
111
+ continue
112
+
107
113
  existing = {col["name"] for col in inspector.get_columns(table_name)}
108
114
 
109
115
  # For each column in model that doesn't exist in DB
@@ -116,7 +122,9 @@ def auto_migrate_columns(sync_conn: Any, dialect: Any) -> None:
116
122
  sql = (
117
123
  f"ALTER TABLE {table_name} ADD COLUMN {col.name} {type_sql}{nullable}{default}"
118
124
  )
119
- sync_conn.execute(text(sql))
125
+ # Column may already exist (race condition or stale inspector cache)
126
+ with contextlib.suppress(Exception):
127
+ sync_conn.execute(text(sql))
120
128
 
121
129
 
122
130
  def parse_model_info(model: str | None) -> tuple[str | None, str | None]:
@@ -222,6 +230,7 @@ def format_conversation(
222
230
  },
223
231
  cost=float(msg.cost_info.total_cost) if msg.cost_info else None,
224
232
  response_time=msg.response_time,
233
+ parent_id=msg.parent_id,
225
234
  )
226
235
  for msg in chat_messages
227
236
  ],
@@ -196,6 +196,7 @@ class TextLogProvider(StorageProvider):
196
196
  provider_response_id: str | None = None,
197
197
  messages: str | None = None,
198
198
  finish_reason: str | None = None,
199
+ parent_id: str | None = None,
199
200
  ) -> None:
200
201
  """Store message and update log."""
201
202
  entry = {
@@ -4,30 +4,22 @@ from __future__ import annotations
4
4
 
5
5
 
6
6
  # Import provider classes
7
- from agentpool_toolsets.builtin.agent_management import AgentManagementTools
8
7
  from agentpool_toolsets.builtin.code import CodeTools
9
8
  from agentpool_toolsets.builtin.debug import DebugTools
10
9
  from agentpool_toolsets.builtin.execution_environment import ExecutionEnvironmentTools
11
- from agentpool_toolsets.builtin.history import HistoryTools
12
- from agentpool_toolsets.builtin.integration import IntegrationTools
13
10
  from agentpool_toolsets.builtin.skills import SkillsTools
14
11
  from agentpool_toolsets.builtin.subagent_tools import SubagentTools
15
- from agentpool_toolsets.builtin.tool_management import ToolManagementTools
16
12
  from agentpool_toolsets.builtin.user_interaction import UserInteractionTools
17
13
  from agentpool_toolsets.builtin.workers import WorkersTools
18
14
 
19
15
 
20
16
  __all__ = [
21
17
  # Provider classes
22
- "AgentManagementTools",
23
18
  "CodeTools",
24
19
  "DebugTools",
25
20
  "ExecutionEnvironmentTools",
26
- "HistoryTools",
27
- "IntegrationTools",
28
21
  "SkillsTools",
29
22
  "SubagentTools",
30
- "ToolManagementTools",
31
23
  "UserInteractionTools",
32
24
  "WorkersTools",
33
25
  ]