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.
- acp/__init__.py +13 -0
- acp/bridge/README.md +15 -2
- acp/bridge/__init__.py +3 -2
- acp/bridge/__main__.py +60 -19
- acp/bridge/ws_server.py +173 -0
- acp/bridge/ws_server_cli.py +89 -0
- acp/notifications.py +2 -1
- acp/stdio.py +39 -9
- acp/transports.py +362 -2
- acp/utils.py +15 -2
- agentpool/__init__.py +4 -1
- agentpool/agents/__init__.py +2 -0
- agentpool/agents/acp_agent/acp_agent.py +203 -88
- agentpool/agents/acp_agent/acp_converters.py +46 -21
- agentpool/agents/acp_agent/client_handler.py +157 -3
- agentpool/agents/acp_agent/session_state.py +4 -1
- agentpool/agents/agent.py +314 -107
- agentpool/agents/agui_agent/__init__.py +0 -2
- agentpool/agents/agui_agent/agui_agent.py +90 -21
- agentpool/agents/agui_agent/agui_converters.py +0 -131
- agentpool/agents/base_agent.py +163 -1
- agentpool/agents/claude_code_agent/claude_code_agent.py +626 -179
- agentpool/agents/claude_code_agent/converters.py +71 -3
- agentpool/agents/claude_code_agent/history.py +474 -0
- agentpool/agents/context.py +40 -0
- agentpool/agents/events/__init__.py +2 -0
- agentpool/agents/events/builtin_handlers.py +2 -1
- agentpool/agents/events/event_emitter.py +29 -2
- agentpool/agents/events/events.py +20 -0
- agentpool/agents/modes.py +54 -0
- agentpool/agents/tool_call_accumulator.py +213 -0
- agentpool/common_types.py +21 -0
- agentpool/config_resources/__init__.py +38 -1
- agentpool/config_resources/claude_code_agent.yml +3 -0
- agentpool/delegation/pool.py +37 -29
- agentpool/delegation/team.py +1 -0
- agentpool/delegation/teamrun.py +1 -0
- agentpool/diagnostics/__init__.py +53 -0
- agentpool/diagnostics/lsp_manager.py +1593 -0
- agentpool/diagnostics/lsp_proxy.py +41 -0
- agentpool/diagnostics/lsp_proxy_script.py +229 -0
- agentpool/diagnostics/models.py +398 -0
- agentpool/mcp_server/__init__.py +0 -2
- agentpool/mcp_server/client.py +12 -3
- agentpool/mcp_server/manager.py +25 -31
- agentpool/mcp_server/registries/official_registry_client.py +25 -0
- agentpool/mcp_server/tool_bridge.py +78 -66
- agentpool/messaging/__init__.py +0 -2
- agentpool/messaging/compaction.py +72 -197
- agentpool/messaging/message_history.py +12 -0
- agentpool/messaging/messages.py +52 -9
- agentpool/messaging/processing.py +3 -1
- agentpool/models/acp_agents/base.py +0 -22
- agentpool/models/acp_agents/mcp_capable.py +8 -148
- agentpool/models/acp_agents/non_mcp.py +129 -72
- agentpool/models/agents.py +35 -13
- agentpool/models/claude_code_agents.py +33 -2
- agentpool/models/manifest.py +43 -0
- agentpool/repomap.py +1 -1
- agentpool/resource_providers/__init__.py +9 -1
- agentpool/resource_providers/aggregating.py +52 -3
- agentpool/resource_providers/base.py +57 -1
- agentpool/resource_providers/mcp_provider.py +23 -0
- agentpool/resource_providers/plan_provider.py +130 -41
- agentpool/resource_providers/pool.py +2 -0
- agentpool/resource_providers/static.py +2 -0
- agentpool/sessions/__init__.py +2 -1
- agentpool/sessions/manager.py +31 -2
- agentpool/sessions/models.py +50 -0
- agentpool/skills/registry.py +13 -8
- agentpool/storage/manager.py +217 -1
- agentpool/testing.py +537 -19
- agentpool/utils/file_watcher.py +269 -0
- agentpool/utils/identifiers.py +121 -0
- agentpool/utils/pydantic_ai_helpers.py +46 -0
- agentpool/utils/streams.py +690 -1
- agentpool/utils/subprocess_utils.py +155 -0
- agentpool/utils/token_breakdown.py +461 -0
- {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/METADATA +27 -7
- {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/RECORD +170 -112
- {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/WHEEL +1 -1
- agentpool_cli/__main__.py +4 -0
- agentpool_cli/serve_acp.py +41 -20
- agentpool_cli/serve_agui.py +87 -0
- agentpool_cli/serve_opencode.py +119 -0
- agentpool_commands/__init__.py +30 -0
- agentpool_commands/agents.py +74 -1
- agentpool_commands/history.py +62 -0
- agentpool_commands/mcp.py +176 -0
- agentpool_commands/models.py +56 -3
- agentpool_commands/tools.py +57 -0
- agentpool_commands/utils.py +51 -0
- agentpool_config/builtin_tools.py +77 -22
- agentpool_config/commands.py +24 -1
- agentpool_config/compaction.py +258 -0
- agentpool_config/mcp_server.py +131 -1
- agentpool_config/storage.py +46 -1
- agentpool_config/tools.py +7 -1
- agentpool_config/toolsets.py +92 -148
- agentpool_server/acp_server/acp_agent.py +134 -150
- agentpool_server/acp_server/commands/acp_commands.py +216 -51
- agentpool_server/acp_server/commands/docs_commands/fetch_repo.py +10 -10
- agentpool_server/acp_server/server.py +23 -79
- agentpool_server/acp_server/session.py +181 -19
- agentpool_server/opencode_server/.rules +95 -0
- agentpool_server/opencode_server/ENDPOINTS.md +362 -0
- agentpool_server/opencode_server/__init__.py +27 -0
- agentpool_server/opencode_server/command_validation.py +172 -0
- agentpool_server/opencode_server/converters.py +869 -0
- agentpool_server/opencode_server/dependencies.py +24 -0
- agentpool_server/opencode_server/input_provider.py +269 -0
- agentpool_server/opencode_server/models/__init__.py +228 -0
- agentpool_server/opencode_server/models/agent.py +53 -0
- agentpool_server/opencode_server/models/app.py +60 -0
- agentpool_server/opencode_server/models/base.py +26 -0
- agentpool_server/opencode_server/models/common.py +23 -0
- agentpool_server/opencode_server/models/config.py +37 -0
- agentpool_server/opencode_server/models/events.py +647 -0
- agentpool_server/opencode_server/models/file.py +88 -0
- agentpool_server/opencode_server/models/mcp.py +25 -0
- agentpool_server/opencode_server/models/message.py +162 -0
- agentpool_server/opencode_server/models/parts.py +190 -0
- agentpool_server/opencode_server/models/provider.py +81 -0
- agentpool_server/opencode_server/models/pty.py +43 -0
- agentpool_server/opencode_server/models/session.py +99 -0
- agentpool_server/opencode_server/routes/__init__.py +25 -0
- agentpool_server/opencode_server/routes/agent_routes.py +442 -0
- agentpool_server/opencode_server/routes/app_routes.py +139 -0
- agentpool_server/opencode_server/routes/config_routes.py +241 -0
- agentpool_server/opencode_server/routes/file_routes.py +392 -0
- agentpool_server/opencode_server/routes/global_routes.py +94 -0
- agentpool_server/opencode_server/routes/lsp_routes.py +319 -0
- agentpool_server/opencode_server/routes/message_routes.py +705 -0
- agentpool_server/opencode_server/routes/pty_routes.py +299 -0
- agentpool_server/opencode_server/routes/session_routes.py +1205 -0
- agentpool_server/opencode_server/routes/tui_routes.py +139 -0
- agentpool_server/opencode_server/server.py +430 -0
- agentpool_server/opencode_server/state.py +121 -0
- agentpool_server/opencode_server/time_utils.py +8 -0
- agentpool_storage/__init__.py +16 -0
- agentpool_storage/base.py +103 -0
- agentpool_storage/claude_provider.py +907 -0
- agentpool_storage/file_provider.py +129 -0
- agentpool_storage/memory_provider.py +61 -0
- agentpool_storage/models.py +3 -0
- agentpool_storage/opencode_provider.py +730 -0
- agentpool_storage/project_store.py +325 -0
- agentpool_storage/session_store.py +6 -0
- agentpool_storage/sql_provider/__init__.py +4 -2
- agentpool_storage/sql_provider/models.py +48 -0
- agentpool_storage/sql_provider/sql_provider.py +134 -1
- agentpool_storage/sql_provider/utils.py +10 -1
- agentpool_storage/text_log_provider.py +1 -0
- agentpool_toolsets/builtin/__init__.py +0 -8
- agentpool_toolsets/builtin/code.py +95 -56
- agentpool_toolsets/builtin/debug.py +16 -21
- agentpool_toolsets/builtin/execution_environment.py +99 -103
- agentpool_toolsets/builtin/file_edit/file_edit.py +115 -7
- agentpool_toolsets/builtin/skills.py +86 -4
- agentpool_toolsets/fsspec_toolset/__init__.py +13 -1
- agentpool_toolsets/fsspec_toolset/diagnostics.py +860 -73
- agentpool_toolsets/fsspec_toolset/grep.py +74 -2
- agentpool_toolsets/fsspec_toolset/image_utils.py +161 -0
- agentpool_toolsets/fsspec_toolset/toolset.py +159 -38
- agentpool_toolsets/mcp_discovery/__init__.py +5 -0
- agentpool_toolsets/mcp_discovery/data/mcp_servers.parquet +0 -0
- agentpool_toolsets/mcp_discovery/toolset.py +454 -0
- agentpool_toolsets/mcp_run_toolset.py +84 -6
- agentpool_toolsets/builtin/agent_management.py +0 -239
- agentpool_toolsets/builtin/history.py +0 -36
- agentpool_toolsets/builtin/integration.py +0 -85
- agentpool_toolsets/builtin/tool_management.py +0 -90
- {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
]
|