agentpool 2.2.3__py3-none-any.whl → 2.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- acp/__init__.py +0 -4
- acp/acp_requests.py +20 -77
- acp/agent/connection.py +8 -0
- acp/agent/implementations/debug_server/debug_server.py +6 -2
- acp/agent/protocol.py +6 -0
- acp/client/connection.py +38 -29
- acp/client/implementations/default_client.py +3 -2
- acp/client/implementations/headless_client.py +2 -2
- acp/connection.py +2 -2
- acp/notifications.py +18 -49
- acp/schema/__init__.py +2 -0
- acp/schema/agent_responses.py +21 -0
- acp/schema/client_requests.py +3 -3
- acp/schema/session_state.py +63 -29
- acp/task/supervisor.py +2 -2
- acp/utils.py +2 -2
- agentpool/__init__.py +2 -0
- agentpool/agents/acp_agent/acp_agent.py +278 -263
- agentpool/agents/acp_agent/acp_converters.py +150 -17
- agentpool/agents/acp_agent/client_handler.py +35 -24
- agentpool/agents/acp_agent/session_state.py +14 -6
- agentpool/agents/agent.py +471 -643
- agentpool/agents/agui_agent/agui_agent.py +104 -107
- agentpool/agents/agui_agent/helpers.py +3 -4
- agentpool/agents/base_agent.py +485 -32
- agentpool/agents/claude_code_agent/FORKING.md +191 -0
- agentpool/agents/claude_code_agent/__init__.py +13 -1
- agentpool/agents/claude_code_agent/claude_code_agent.py +654 -334
- agentpool/agents/claude_code_agent/converters.py +4 -141
- agentpool/agents/claude_code_agent/models.py +77 -0
- agentpool/agents/claude_code_agent/static_info.py +100 -0
- agentpool/agents/claude_code_agent/usage.py +242 -0
- agentpool/agents/events/__init__.py +22 -0
- agentpool/agents/events/builtin_handlers.py +65 -0
- agentpool/agents/events/event_emitter.py +3 -0
- agentpool/agents/events/events.py +84 -3
- agentpool/agents/events/infer_info.py +145 -0
- agentpool/agents/events/processors.py +254 -0
- agentpool/agents/interactions.py +41 -6
- agentpool/agents/modes.py +13 -0
- agentpool/agents/slashed_agent.py +5 -4
- agentpool/agents/tool_wrapping.py +18 -6
- agentpool/common_types.py +35 -21
- agentpool/config_resources/acp_assistant.yml +2 -2
- agentpool/config_resources/agents.yml +3 -0
- agentpool/config_resources/agents_template.yml +1 -0
- agentpool/config_resources/claude_code_agent.yml +9 -8
- agentpool/config_resources/external_acp_agents.yml +2 -1
- agentpool/delegation/base_team.py +4 -30
- agentpool/delegation/pool.py +104 -265
- agentpool/delegation/team.py +57 -57
- agentpool/delegation/teamrun.py +50 -55
- agentpool/functional/run.py +10 -4
- agentpool/mcp_server/client.py +73 -38
- agentpool/mcp_server/conversions.py +54 -13
- agentpool/mcp_server/manager.py +9 -23
- agentpool/mcp_server/registries/official_registry_client.py +10 -1
- agentpool/mcp_server/tool_bridge.py +114 -79
- agentpool/messaging/connection_manager.py +11 -10
- agentpool/messaging/event_manager.py +5 -5
- agentpool/messaging/message_container.py +6 -30
- agentpool/messaging/message_history.py +87 -8
- agentpool/messaging/messagenode.py +52 -14
- agentpool/messaging/messages.py +2 -26
- agentpool/messaging/processing.py +10 -22
- agentpool/models/__init__.py +1 -1
- agentpool/models/acp_agents/base.py +6 -2
- agentpool/models/acp_agents/mcp_capable.py +124 -15
- agentpool/models/acp_agents/non_mcp.py +0 -23
- agentpool/models/agents.py +66 -66
- agentpool/models/agui_agents.py +1 -1
- agentpool/models/claude_code_agents.py +111 -17
- agentpool/models/file_parsing.py +0 -1
- agentpool/models/manifest.py +70 -50
- agentpool/prompts/conversion_manager.py +1 -1
- agentpool/prompts/prompts.py +5 -2
- agentpool/resource_providers/__init__.py +2 -0
- agentpool/resource_providers/aggregating.py +4 -2
- agentpool/resource_providers/base.py +13 -3
- agentpool/resource_providers/codemode/code_executor.py +72 -5
- agentpool/resource_providers/codemode/helpers.py +2 -2
- agentpool/resource_providers/codemode/provider.py +64 -12
- agentpool/resource_providers/codemode/remote_mcp_execution.py +2 -2
- agentpool/resource_providers/codemode/remote_provider.py +9 -12
- agentpool/resource_providers/filtering.py +3 -1
- agentpool/resource_providers/mcp_provider.py +66 -12
- agentpool/resource_providers/plan_provider.py +111 -18
- agentpool/resource_providers/pool.py +5 -3
- agentpool/resource_providers/resource_info.py +111 -0
- agentpool/resource_providers/static.py +2 -2
- agentpool/sessions/__init__.py +2 -0
- agentpool/sessions/manager.py +2 -3
- agentpool/sessions/models.py +9 -6
- agentpool/sessions/protocol.py +28 -0
- agentpool/sessions/session.py +11 -55
- agentpool/storage/manager.py +361 -54
- agentpool/talk/registry.py +4 -4
- agentpool/talk/talk.py +9 -10
- agentpool/testing.py +1 -1
- agentpool/tool_impls/__init__.py +6 -0
- agentpool/tool_impls/agent_cli/__init__.py +42 -0
- agentpool/tool_impls/agent_cli/tool.py +95 -0
- agentpool/tool_impls/bash/__init__.py +64 -0
- agentpool/tool_impls/bash/helpers.py +35 -0
- agentpool/tool_impls/bash/tool.py +171 -0
- agentpool/tool_impls/delete_path/__init__.py +70 -0
- agentpool/tool_impls/delete_path/tool.py +142 -0
- agentpool/tool_impls/download_file/__init__.py +80 -0
- agentpool/tool_impls/download_file/tool.py +183 -0
- agentpool/tool_impls/execute_code/__init__.py +55 -0
- agentpool/tool_impls/execute_code/tool.py +163 -0
- agentpool/tool_impls/grep/__init__.py +80 -0
- agentpool/tool_impls/grep/tool.py +200 -0
- agentpool/tool_impls/list_directory/__init__.py +73 -0
- agentpool/tool_impls/list_directory/tool.py +197 -0
- agentpool/tool_impls/question/__init__.py +42 -0
- agentpool/tool_impls/question/tool.py +127 -0
- agentpool/tool_impls/read/__init__.py +104 -0
- agentpool/tool_impls/read/tool.py +305 -0
- agentpool/tools/__init__.py +2 -1
- agentpool/tools/base.py +114 -34
- agentpool/tools/manager.py +57 -1
- agentpool/ui/base.py +2 -2
- agentpool/ui/mock_provider.py +2 -2
- agentpool/ui/stdlib_provider.py +2 -2
- agentpool/utils/streams.py +21 -96
- agentpool/vfs_registry.py +7 -2
- {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/METADATA +16 -22
- {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/RECORD +242 -195
- {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/WHEEL +1 -1
- agentpool_cli/__main__.py +20 -0
- agentpool_cli/create.py +1 -1
- agentpool_cli/serve_acp.py +59 -1
- agentpool_cli/serve_opencode.py +1 -1
- agentpool_cli/ui.py +557 -0
- agentpool_commands/__init__.py +12 -5
- agentpool_commands/agents.py +1 -1
- agentpool_commands/pool.py +260 -0
- agentpool_commands/session.py +1 -1
- agentpool_commands/text_sharing/__init__.py +119 -0
- agentpool_commands/text_sharing/base.py +123 -0
- agentpool_commands/text_sharing/github_gist.py +80 -0
- agentpool_commands/text_sharing/opencode.py +462 -0
- agentpool_commands/text_sharing/paste_rs.py +59 -0
- agentpool_commands/text_sharing/pastebin.py +116 -0
- agentpool_commands/text_sharing/shittycodingagent.py +112 -0
- agentpool_commands/utils.py +31 -32
- agentpool_config/__init__.py +30 -2
- agentpool_config/agentpool_tools.py +498 -0
- agentpool_config/converters.py +1 -1
- agentpool_config/event_handlers.py +42 -0
- agentpool_config/events.py +1 -1
- agentpool_config/forward_targets.py +1 -4
- agentpool_config/jinja.py +3 -3
- agentpool_config/mcp_server.py +1 -5
- agentpool_config/nodes.py +1 -1
- agentpool_config/observability.py +44 -0
- agentpool_config/session.py +0 -3
- agentpool_config/storage.py +38 -39
- agentpool_config/task.py +3 -3
- agentpool_config/tools.py +11 -28
- agentpool_config/toolsets.py +22 -90
- agentpool_server/a2a_server/agent_worker.py +307 -0
- agentpool_server/a2a_server/server.py +23 -18
- agentpool_server/acp_server/acp_agent.py +125 -56
- agentpool_server/acp_server/commands/acp_commands.py +46 -216
- agentpool_server/acp_server/commands/docs_commands/fetch_repo.py +8 -7
- agentpool_server/acp_server/event_converter.py +651 -0
- agentpool_server/acp_server/input_provider.py +53 -10
- agentpool_server/acp_server/server.py +1 -11
- agentpool_server/acp_server/session.py +90 -410
- agentpool_server/acp_server/session_manager.py +8 -34
- agentpool_server/agui_server/server.py +3 -1
- agentpool_server/mcp_server/server.py +5 -2
- agentpool_server/opencode_server/ENDPOINTS.md +53 -14
- agentpool_server/opencode_server/OPENCODE_UI_TOOLS_COMPLETE.md +202 -0
- agentpool_server/opencode_server/__init__.py +0 -8
- agentpool_server/opencode_server/converters.py +132 -26
- agentpool_server/opencode_server/input_provider.py +160 -8
- agentpool_server/opencode_server/models/__init__.py +42 -20
- agentpool_server/opencode_server/models/app.py +12 -0
- agentpool_server/opencode_server/models/events.py +203 -29
- agentpool_server/opencode_server/models/mcp.py +19 -0
- agentpool_server/opencode_server/models/message.py +18 -1
- agentpool_server/opencode_server/models/parts.py +134 -1
- agentpool_server/opencode_server/models/question.py +56 -0
- agentpool_server/opencode_server/models/session.py +13 -1
- agentpool_server/opencode_server/routes/__init__.py +4 -0
- agentpool_server/opencode_server/routes/agent_routes.py +33 -2
- agentpool_server/opencode_server/routes/app_routes.py +66 -3
- agentpool_server/opencode_server/routes/config_routes.py +66 -5
- agentpool_server/opencode_server/routes/file_routes.py +184 -5
- agentpool_server/opencode_server/routes/global_routes.py +1 -1
- agentpool_server/opencode_server/routes/lsp_routes.py +1 -1
- agentpool_server/opencode_server/routes/message_routes.py +122 -66
- agentpool_server/opencode_server/routes/permission_routes.py +63 -0
- agentpool_server/opencode_server/routes/pty_routes.py +23 -22
- agentpool_server/opencode_server/routes/question_routes.py +128 -0
- agentpool_server/opencode_server/routes/session_routes.py +139 -68
- agentpool_server/opencode_server/routes/tui_routes.py +1 -1
- agentpool_server/opencode_server/server.py +47 -2
- agentpool_server/opencode_server/state.py +30 -0
- agentpool_storage/__init__.py +0 -4
- agentpool_storage/base.py +81 -2
- agentpool_storage/claude_provider/ARCHITECTURE.md +433 -0
- agentpool_storage/claude_provider/__init__.py +42 -0
- agentpool_storage/{claude_provider.py → claude_provider/provider.py} +190 -8
- agentpool_storage/file_provider.py +149 -15
- agentpool_storage/memory_provider.py +132 -12
- agentpool_storage/opencode_provider/ARCHITECTURE.md +386 -0
- agentpool_storage/opencode_provider/__init__.py +16 -0
- agentpool_storage/opencode_provider/helpers.py +414 -0
- agentpool_storage/opencode_provider/provider.py +895 -0
- agentpool_storage/session_store.py +20 -6
- agentpool_storage/sql_provider/sql_provider.py +135 -2
- agentpool_storage/sql_provider/utils.py +2 -12
- agentpool_storage/zed_provider/__init__.py +16 -0
- agentpool_storage/zed_provider/helpers.py +281 -0
- agentpool_storage/zed_provider/models.py +130 -0
- agentpool_storage/zed_provider/provider.py +442 -0
- agentpool_storage/zed_provider.py +803 -0
- agentpool_toolsets/__init__.py +0 -2
- agentpool_toolsets/builtin/__init__.py +2 -4
- agentpool_toolsets/builtin/code.py +4 -4
- agentpool_toolsets/builtin/debug.py +115 -40
- agentpool_toolsets/builtin/execution_environment.py +54 -165
- agentpool_toolsets/builtin/skills.py +0 -77
- agentpool_toolsets/builtin/subagent_tools.py +64 -51
- agentpool_toolsets/builtin/workers.py +4 -2
- agentpool_toolsets/composio_toolset.py +2 -2
- agentpool_toolsets/entry_points.py +3 -1
- agentpool_toolsets/fsspec_toolset/grep.py +25 -5
- agentpool_toolsets/fsspec_toolset/helpers.py +3 -2
- agentpool_toolsets/fsspec_toolset/toolset.py +350 -66
- agentpool_toolsets/mcp_discovery/data/mcp_servers.parquet +0 -0
- agentpool_toolsets/mcp_discovery/toolset.py +74 -17
- agentpool_toolsets/mcp_run_toolset.py +8 -11
- agentpool_toolsets/notifications.py +33 -33
- agentpool_toolsets/openapi.py +3 -1
- agentpool_toolsets/search_toolset.py +3 -1
- agentpool_config/resources.py +0 -33
- agentpool_server/acp_server/acp_tools.py +0 -43
- agentpool_server/acp_server/commands/spawn.py +0 -210
- agentpool_storage/opencode_provider.py +0 -730
- agentpool_storage/text_log_provider.py +0 -276
- agentpool_toolsets/builtin/chain.py +0 -288
- agentpool_toolsets/builtin/user_interaction.py +0 -52
- agentpool_toolsets/semantic_memory_toolset.py +0 -536
- {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/entry_points.txt +0 -0
- {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -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:
|
|
@@ -98,7 +96,6 @@ class SQLSessionStore:
|
|
|
98
96
|
project_id=data.project_id,
|
|
99
97
|
parent_id=data.parent_id,
|
|
100
98
|
version=data.version,
|
|
101
|
-
title=data.title,
|
|
102
99
|
cwd=data.cwd,
|
|
103
100
|
created_at=data.created_at,
|
|
104
101
|
last_active=data.last_active,
|
|
@@ -115,7 +112,6 @@ class SQLSessionStore:
|
|
|
115
112
|
project_id=row.project_id,
|
|
116
113
|
parent_id=row.parent_id,
|
|
117
114
|
version=row.version,
|
|
118
|
-
title=row.title,
|
|
119
115
|
cwd=row.cwd,
|
|
120
116
|
created_at=row.created_at,
|
|
121
117
|
last_active=row.last_active,
|
|
@@ -130,6 +126,9 @@ class SQLSessionStore:
|
|
|
130
126
|
Args:
|
|
131
127
|
data: Session data to persist
|
|
132
128
|
"""
|
|
129
|
+
from sqlalchemy import delete
|
|
130
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
131
|
+
|
|
133
132
|
engine = self._get_engine()
|
|
134
133
|
|
|
135
134
|
async with AsyncSession(engine) as session:
|
|
@@ -152,6 +151,9 @@ class SQLSessionStore:
|
|
|
152
151
|
Returns:
|
|
153
152
|
Session data if found, None otherwise
|
|
154
153
|
"""
|
|
154
|
+
from sqlalchemy import select
|
|
155
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
156
|
+
|
|
155
157
|
engine = self._get_engine()
|
|
156
158
|
|
|
157
159
|
async with AsyncSession(engine) as session:
|
|
@@ -173,6 +175,9 @@ class SQLSessionStore:
|
|
|
173
175
|
Returns:
|
|
174
176
|
True if session was deleted, False if not found
|
|
175
177
|
"""
|
|
178
|
+
from sqlalchemy import delete
|
|
179
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
180
|
+
|
|
176
181
|
engine = self._get_engine()
|
|
177
182
|
|
|
178
183
|
async with AsyncSession(engine) as session:
|
|
@@ -199,6 +204,9 @@ class SQLSessionStore:
|
|
|
199
204
|
Returns:
|
|
200
205
|
List of session IDs
|
|
201
206
|
"""
|
|
207
|
+
from sqlalchemy import select
|
|
208
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
209
|
+
|
|
202
210
|
engine = self._get_engine()
|
|
203
211
|
|
|
204
212
|
async with AsyncSession(engine) as session:
|
|
@@ -223,6 +231,9 @@ class SQLSessionStore:
|
|
|
223
231
|
Returns:
|
|
224
232
|
Number of sessions removed
|
|
225
233
|
"""
|
|
234
|
+
from sqlalchemy import delete
|
|
235
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
236
|
+
|
|
226
237
|
engine = self._get_engine()
|
|
227
238
|
cutoff = get_now() - timedelta(hours=max_age_hours)
|
|
228
239
|
|
|
@@ -250,6 +261,9 @@ class SQLSessionStore:
|
|
|
250
261
|
Returns:
|
|
251
262
|
List of session data objects
|
|
252
263
|
"""
|
|
264
|
+
from sqlalchemy import select
|
|
265
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
266
|
+
|
|
253
267
|
engine = self._get_engine()
|
|
254
268
|
|
|
255
269
|
async with AsyncSession(engine) as session:
|
|
@@ -130,7 +130,6 @@ class SQLModelProvider(StorageProvider):
|
|
|
130
130
|
cost_info: TokenCost | None = None,
|
|
131
131
|
model: str | None = None,
|
|
132
132
|
response_time: float | None = None,
|
|
133
|
-
forwarded_from: list[str] | None = None,
|
|
134
133
|
provider_name: str | None = None,
|
|
135
134
|
provider_response_id: str | None = None,
|
|
136
135
|
messages: str | None = None,
|
|
@@ -157,7 +156,6 @@ class SQLModelProvider(StorageProvider):
|
|
|
157
156
|
input_tokens=cost_info.token_usage.input_tokens if cost_info else None,
|
|
158
157
|
output_tokens=cost_info.token_usage.output_tokens if cost_info else None,
|
|
159
158
|
cost=float(cost_info.total_cost) if cost_info else None,
|
|
160
|
-
forwarded_from=forwarded_from,
|
|
161
159
|
provider_name=provider_name,
|
|
162
160
|
provider_response_id=provider_response_id,
|
|
163
161
|
messages=messages,
|
|
@@ -210,6 +208,141 @@ class SQLModelProvider(StorageProvider):
|
|
|
210
208
|
)
|
|
211
209
|
return result.scalar_one_or_none()
|
|
212
210
|
|
|
211
|
+
async def get_conversation_messages(
|
|
212
|
+
self,
|
|
213
|
+
conversation_id: str,
|
|
214
|
+
*,
|
|
215
|
+
include_ancestors: bool = False,
|
|
216
|
+
) -> list[ChatMessage[str]]:
|
|
217
|
+
"""Get all messages for a conversation.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
conversation_id: ID of the conversation
|
|
221
|
+
include_ancestors: If True, traverse parent_id chain to include
|
|
222
|
+
messages from ancestor conversations (for forked convos).
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
List of messages ordered by timestamp.
|
|
226
|
+
"""
|
|
227
|
+
async with AsyncSession(self.engine) as session:
|
|
228
|
+
# Get messages for this conversation
|
|
229
|
+
result = await session.execute(
|
|
230
|
+
select(Message)
|
|
231
|
+
.where(Message.conversation_id == conversation_id)
|
|
232
|
+
.order_by(Message.timestamp.asc()) # type: ignore
|
|
233
|
+
)
|
|
234
|
+
messages = [to_chat_message(m) for m in result.scalars().all()]
|
|
235
|
+
|
|
236
|
+
if not include_ancestors or not messages:
|
|
237
|
+
return messages
|
|
238
|
+
|
|
239
|
+
# Find the first message's parent_id to get ancestor chain
|
|
240
|
+
first_msg = messages[0]
|
|
241
|
+
if first_msg.parent_id:
|
|
242
|
+
ancestors = await self.get_message_ancestry(first_msg.parent_id)
|
|
243
|
+
return ancestors + messages
|
|
244
|
+
|
|
245
|
+
return messages
|
|
246
|
+
|
|
247
|
+
async def get_message(
|
|
248
|
+
self,
|
|
249
|
+
message_id: str,
|
|
250
|
+
) -> ChatMessage[str] | None:
|
|
251
|
+
"""Get a single message by ID."""
|
|
252
|
+
async with AsyncSession(self.engine) as session:
|
|
253
|
+
result = await session.execute(select(Message).where(Message.id == message_id))
|
|
254
|
+
msg = result.scalar_one_or_none()
|
|
255
|
+
return to_chat_message(msg) if msg else None
|
|
256
|
+
|
|
257
|
+
async def get_message_ancestry(
|
|
258
|
+
self,
|
|
259
|
+
message_id: str,
|
|
260
|
+
) -> list[ChatMessage[str]]:
|
|
261
|
+
"""Get the ancestry chain of a message.
|
|
262
|
+
|
|
263
|
+
Traverses parent_id chain to build full history.
|
|
264
|
+
"""
|
|
265
|
+
ancestors: list[ChatMessage[str]] = []
|
|
266
|
+
current_id: str | None = message_id
|
|
267
|
+
|
|
268
|
+
async with AsyncSession(self.engine) as session:
|
|
269
|
+
while current_id:
|
|
270
|
+
result = await session.execute(select(Message).where(Message.id == current_id))
|
|
271
|
+
msg = result.scalar_one_or_none()
|
|
272
|
+
if not msg:
|
|
273
|
+
break
|
|
274
|
+
ancestors.append(to_chat_message(msg))
|
|
275
|
+
current_id = msg.parent_id
|
|
276
|
+
|
|
277
|
+
# Reverse to get oldest first
|
|
278
|
+
ancestors.reverse()
|
|
279
|
+
return ancestors
|
|
280
|
+
|
|
281
|
+
async def fork_conversation(
|
|
282
|
+
self,
|
|
283
|
+
*,
|
|
284
|
+
source_conversation_id: str,
|
|
285
|
+
new_conversation_id: str,
|
|
286
|
+
fork_from_message_id: str | None = None,
|
|
287
|
+
new_agent_name: str | None = None,
|
|
288
|
+
) -> str | None:
|
|
289
|
+
"""Fork a conversation at a specific point.
|
|
290
|
+
|
|
291
|
+
Creates a new conversation record. The fork point message_id is returned
|
|
292
|
+
so callers can set it as parent_id for new messages.
|
|
293
|
+
"""
|
|
294
|
+
async with AsyncSession(self.engine) as session:
|
|
295
|
+
# Get source conversation
|
|
296
|
+
result = await session.execute(
|
|
297
|
+
select(Conversation).where(Conversation.id == source_conversation_id)
|
|
298
|
+
)
|
|
299
|
+
source_conv = result.scalar_one_or_none()
|
|
300
|
+
if not source_conv:
|
|
301
|
+
msg = f"Source conversation not found: {source_conversation_id}"
|
|
302
|
+
raise ValueError(msg)
|
|
303
|
+
|
|
304
|
+
# Determine fork point
|
|
305
|
+
fork_point_id: str | None = None
|
|
306
|
+
if fork_from_message_id:
|
|
307
|
+
# Verify the message exists and belongs to the source conversation
|
|
308
|
+
msg_result = await session.execute(
|
|
309
|
+
select(Message).where(
|
|
310
|
+
Message.id == fork_from_message_id,
|
|
311
|
+
Message.conversation_id == source_conversation_id,
|
|
312
|
+
)
|
|
313
|
+
)
|
|
314
|
+
fork_msg = msg_result.scalar_one_or_none()
|
|
315
|
+
if not fork_msg:
|
|
316
|
+
err = f"Message {fork_from_message_id} not found in conversation"
|
|
317
|
+
raise ValueError(err)
|
|
318
|
+
fork_point_id = fork_from_message_id
|
|
319
|
+
else:
|
|
320
|
+
# Fork from the last message
|
|
321
|
+
msg_result = await session.execute(
|
|
322
|
+
select(Message)
|
|
323
|
+
.where(Message.conversation_id == source_conversation_id)
|
|
324
|
+
.order_by(desc(Message.timestamp))
|
|
325
|
+
.limit(1)
|
|
326
|
+
)
|
|
327
|
+
last_msg = msg_result.scalar_one_or_none()
|
|
328
|
+
if last_msg:
|
|
329
|
+
fork_point_id = last_msg.id
|
|
330
|
+
|
|
331
|
+
# Create new conversation
|
|
332
|
+
agent_name = new_agent_name or source_conv.agent_name
|
|
333
|
+
new_conv = Conversation(
|
|
334
|
+
id=new_conversation_id,
|
|
335
|
+
agent_name=agent_name,
|
|
336
|
+
title=f"{source_conv.title or 'Conversation'} (fork)"
|
|
337
|
+
if source_conv.title
|
|
338
|
+
else None,
|
|
339
|
+
start_time=get_now(),
|
|
340
|
+
)
|
|
341
|
+
session.add(new_conv)
|
|
342
|
+
await session.commit()
|
|
343
|
+
|
|
344
|
+
return fork_point_id
|
|
345
|
+
|
|
213
346
|
async def log_command(
|
|
214
347
|
self,
|
|
215
348
|
*,
|
|
@@ -8,8 +8,7 @@ from decimal import Decimal
|
|
|
8
8
|
from typing import TYPE_CHECKING, Any
|
|
9
9
|
|
|
10
10
|
from pydantic_ai import RunUsage
|
|
11
|
-
from sqlalchemy import
|
|
12
|
-
from sqlalchemy.sql import expression
|
|
11
|
+
from sqlalchemy import Column, and_
|
|
13
12
|
from sqlmodel import select
|
|
14
13
|
|
|
15
14
|
from agentpool.messaging import ChatMessage, TokenCost
|
|
@@ -68,7 +67,6 @@ def to_chat_message(db_message: Message) -> ChatMessage[str]:
|
|
|
68
67
|
model_name=db_message.model,
|
|
69
68
|
cost_info=cost_info,
|
|
70
69
|
response_time=db_message.response_time,
|
|
71
|
-
forwarded_from=db_message.forwarded_from or [],
|
|
72
70
|
timestamp=db_message.timestamp,
|
|
73
71
|
provider_name=db_message.provider_name,
|
|
74
72
|
provider_response_id=db_message.provider_response_id,
|
|
@@ -168,15 +166,7 @@ def build_message_query(query: SessionQuery) -> SelectOfScalar[Any]:
|
|
|
168
166
|
if query.name:
|
|
169
167
|
conditions.append(Message.conversation_id == query.name)
|
|
170
168
|
if query.agents:
|
|
171
|
-
|
|
172
|
-
if query.include_forwarded:
|
|
173
|
-
agent_conditions.append(
|
|
174
|
-
and_(
|
|
175
|
-
Column("forwarded_from").isnot(None),
|
|
176
|
-
expression.cast(Column("forwarded_from"), JSON).contains(list(query.agents)), # type: ignore
|
|
177
|
-
)
|
|
178
|
-
)
|
|
179
|
-
conditions.append(or_(*agent_conditions))
|
|
169
|
+
conditions.append(Column("name").in_(query.agents))
|
|
180
170
|
if query.since and (cutoff := query.get_time_cutoff()):
|
|
181
171
|
conditions.append(Message.timestamp >= cutoff)
|
|
182
172
|
if query.until:
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Zed IDE storage provider.
|
|
2
|
+
|
|
3
|
+
This package implements a read-only storage backend that reads Zed IDE's
|
|
4
|
+
native thread format from ~/.local/share/zed/threads/threads.db.
|
|
5
|
+
|
|
6
|
+
Zed stores conversations as zstd-compressed JSON in a SQLite database.
|
|
7
|
+
This provider enables importing and analyzing Zed conversations.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from agentpool_storage.zed_provider.provider import ZedStorageProvider
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"ZedStorageProvider",
|
|
16
|
+
]
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"""Helper functions for Zed storage provider.
|
|
2
|
+
|
|
3
|
+
Stateless conversion and utility functions for working with Zed format.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import base64
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
import io
|
|
11
|
+
import json
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from pydantic_ai.messages import (
|
|
15
|
+
BinaryContent,
|
|
16
|
+
ModelRequest,
|
|
17
|
+
ModelResponse,
|
|
18
|
+
TextPart,
|
|
19
|
+
ThinkingPart,
|
|
20
|
+
ToolCallPart,
|
|
21
|
+
ToolReturnPart,
|
|
22
|
+
UserPromptPart,
|
|
23
|
+
)
|
|
24
|
+
from pydantic_ai.usage import RequestUsage
|
|
25
|
+
import zstandard
|
|
26
|
+
|
|
27
|
+
from agentpool.log import get_logger
|
|
28
|
+
from agentpool.messaging import ChatMessage
|
|
29
|
+
from agentpool.utils.now import get_now
|
|
30
|
+
from agentpool_storage.zed_provider.models import ZedThread
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
logger = get_logger(__name__)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def decompress_thread(data: bytes, data_type: str) -> ZedThread:
|
|
37
|
+
"""Decompress and parse thread data.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
data: Compressed thread data
|
|
41
|
+
data_type: Type of compression ("zstd" or plain)
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Parsed ZedThread object
|
|
45
|
+
"""
|
|
46
|
+
if data_type == "zstd":
|
|
47
|
+
dctx = zstandard.ZstdDecompressor()
|
|
48
|
+
# Use stream_reader for data without content size in header
|
|
49
|
+
reader = dctx.stream_reader(io.BytesIO(data))
|
|
50
|
+
json_data = reader.read()
|
|
51
|
+
else:
|
|
52
|
+
json_data = data
|
|
53
|
+
|
|
54
|
+
thread_dict = json.loads(json_data)
|
|
55
|
+
return ZedThread.model_validate(thread_dict)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def parse_user_content( # noqa: PLR0915
|
|
59
|
+
content_list: list[dict[str, Any]],
|
|
60
|
+
) -> tuple[str, list[str | BinaryContent]]:
|
|
61
|
+
"""Parse user message content blocks.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
content_list: List of content blocks from Zed user message
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Tuple of (display_text, pydantic_ai_content_list)
|
|
68
|
+
"""
|
|
69
|
+
display_parts: list[str] = []
|
|
70
|
+
pydantic_content: list[str | BinaryContent] = []
|
|
71
|
+
|
|
72
|
+
for item in content_list:
|
|
73
|
+
if "Text" in item:
|
|
74
|
+
text = item["Text"]
|
|
75
|
+
display_parts.append(text)
|
|
76
|
+
pydantic_content.append(text)
|
|
77
|
+
|
|
78
|
+
elif "Image" in item:
|
|
79
|
+
image_data = item["Image"]
|
|
80
|
+
source = image_data.get("source", "")
|
|
81
|
+
try:
|
|
82
|
+
binary_data = base64.b64decode(source)
|
|
83
|
+
# Try to detect image type from magic bytes
|
|
84
|
+
media_type = "image/png" # default
|
|
85
|
+
if binary_data[:3] == b"\xff\xd8\xff":
|
|
86
|
+
media_type = "image/jpeg"
|
|
87
|
+
elif binary_data[:4] == b"\x89PNG":
|
|
88
|
+
media_type = "image/png"
|
|
89
|
+
elif binary_data[:6] in (b"GIF87a", b"GIF89a"):
|
|
90
|
+
media_type = "image/gif"
|
|
91
|
+
elif binary_data[:4] == b"RIFF" and binary_data[8:12] == b"WEBP":
|
|
92
|
+
media_type = "image/webp"
|
|
93
|
+
|
|
94
|
+
pydantic_content.append(BinaryContent(data=binary_data, media_type=media_type))
|
|
95
|
+
display_parts.append("[image]")
|
|
96
|
+
except (ValueError, TypeError, IndexError) as e:
|
|
97
|
+
logger.warning("Failed to decode image", error=str(e))
|
|
98
|
+
display_parts.append("[image decode error]")
|
|
99
|
+
|
|
100
|
+
elif "Mention" in item:
|
|
101
|
+
mention = item["Mention"]
|
|
102
|
+
uri = mention.get("uri", {})
|
|
103
|
+
content = mention.get("content", "")
|
|
104
|
+
|
|
105
|
+
# Format mention based on type
|
|
106
|
+
if "File" in uri:
|
|
107
|
+
file_info = uri["File"]
|
|
108
|
+
path = file_info.get("abs_path", "unknown")
|
|
109
|
+
formatted = f"[File: {path}]\n{content}"
|
|
110
|
+
elif "Directory" in uri:
|
|
111
|
+
dir_info = uri["Directory"]
|
|
112
|
+
path = dir_info.get("abs_path", "unknown")
|
|
113
|
+
formatted = f"[Directory: {path}]\n{content}"
|
|
114
|
+
elif "Symbol" in uri:
|
|
115
|
+
symbol_info = uri["Symbol"]
|
|
116
|
+
path = symbol_info.get("abs_path", "unknown")
|
|
117
|
+
name = symbol_info.get("name", "")
|
|
118
|
+
formatted = f"[Symbol: {name} in {path}]\n{content}"
|
|
119
|
+
elif "Selection" in uri:
|
|
120
|
+
sel_info = uri["Selection"]
|
|
121
|
+
path = sel_info.get("abs_path", "unknown")
|
|
122
|
+
formatted = f"[Selection: {path}]\n{content}"
|
|
123
|
+
elif "Fetch" in uri:
|
|
124
|
+
fetch_info = uri["Fetch"]
|
|
125
|
+
url = fetch_info.get("url", "unknown")
|
|
126
|
+
formatted = f"[Fetched: {url}]\n{content}"
|
|
127
|
+
else:
|
|
128
|
+
formatted = content
|
|
129
|
+
|
|
130
|
+
display_parts.append(formatted)
|
|
131
|
+
pydantic_content.append(formatted)
|
|
132
|
+
|
|
133
|
+
display_text = "\n".join(display_parts)
|
|
134
|
+
return display_text, pydantic_content
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def parse_agent_content(
|
|
138
|
+
content_list: list[dict[str, Any]],
|
|
139
|
+
) -> tuple[str, list[TextPart | ThinkingPart | ToolCallPart]]:
|
|
140
|
+
"""Parse agent message content blocks.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
content_list: List of content blocks from Zed agent message
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
Tuple of (display_text, pydantic_ai_parts)
|
|
147
|
+
"""
|
|
148
|
+
display_parts: list[str] = []
|
|
149
|
+
pydantic_parts: list[TextPart | ThinkingPart | ToolCallPart] = []
|
|
150
|
+
|
|
151
|
+
for item in content_list:
|
|
152
|
+
if "Text" in item:
|
|
153
|
+
text = item["Text"]
|
|
154
|
+
display_parts.append(text)
|
|
155
|
+
pydantic_parts.append(TextPart(content=text))
|
|
156
|
+
|
|
157
|
+
elif "Thinking" in item:
|
|
158
|
+
thinking = item["Thinking"]
|
|
159
|
+
text = thinking.get("text", "")
|
|
160
|
+
signature = thinking.get("signature")
|
|
161
|
+
display_parts.append(f"<thinking>\n{text}\n</thinking>")
|
|
162
|
+
pydantic_parts.append(ThinkingPart(content=text, signature=signature))
|
|
163
|
+
|
|
164
|
+
elif "ToolUse" in item:
|
|
165
|
+
tool_use = item["ToolUse"]
|
|
166
|
+
tool_id = tool_use.get("id", "")
|
|
167
|
+
tool_name = tool_use.get("name", "")
|
|
168
|
+
tool_input = tool_use.get("input", {})
|
|
169
|
+
display_parts.append(f"[Tool: {tool_name}]")
|
|
170
|
+
pydantic_parts.append(
|
|
171
|
+
ToolCallPart(tool_name=tool_name, args=tool_input, tool_call_id=tool_id)
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
display_text = "\n".join(display_parts)
|
|
175
|
+
return display_text, pydantic_parts
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def parse_tool_results(tool_results: dict[str, Any]) -> list[ToolReturnPart]:
|
|
179
|
+
"""Parse tool results into ToolReturnParts.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
tool_results: Dictionary of tool results from Zed agent message
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
List of ToolReturnPart objects
|
|
186
|
+
"""
|
|
187
|
+
parts: list[ToolReturnPart] = []
|
|
188
|
+
|
|
189
|
+
for tool_id, result in tool_results.items():
|
|
190
|
+
if isinstance(result, dict):
|
|
191
|
+
tool_name = result.get("tool_name", "")
|
|
192
|
+
output = result.get("output", "")
|
|
193
|
+
content = result.get("content", {})
|
|
194
|
+
|
|
195
|
+
# Handle output being a dict like {"Text": "..."}
|
|
196
|
+
if isinstance(output, dict) and "Text" in output:
|
|
197
|
+
output = output["Text"]
|
|
198
|
+
# Extract text content if available
|
|
199
|
+
elif isinstance(content, dict) and "Text" in content:
|
|
200
|
+
output = content["Text"]
|
|
201
|
+
elif isinstance(content, str):
|
|
202
|
+
output = content
|
|
203
|
+
|
|
204
|
+
parts.append(
|
|
205
|
+
ToolReturnPart(tool_name=tool_name, content=output or "", tool_call_id=tool_id)
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
return parts
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def thread_to_chat_messages(thread: ZedThread, thread_id: str) -> list[ChatMessage[str]]:
|
|
212
|
+
"""Convert a Zed thread to ChatMessages.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
thread: Zed thread object
|
|
216
|
+
thread_id: Thread identifier
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
List of ChatMessage objects
|
|
220
|
+
"""
|
|
221
|
+
messages: list[ChatMessage[str]] = []
|
|
222
|
+
try:
|
|
223
|
+
updated_at = datetime.fromisoformat(thread.updated_at.replace("Z", "+00:00"))
|
|
224
|
+
except (ValueError, AttributeError):
|
|
225
|
+
updated_at = get_now()
|
|
226
|
+
# Get model info
|
|
227
|
+
model_name = None
|
|
228
|
+
if thread.model:
|
|
229
|
+
model_name = f"{thread.model.provider}:{thread.model.model}"
|
|
230
|
+
|
|
231
|
+
for idx, msg in enumerate(thread.messages):
|
|
232
|
+
if msg == "Resume":
|
|
233
|
+
continue # Skip control messages
|
|
234
|
+
msg_id = f"{thread_id}_{idx}"
|
|
235
|
+
|
|
236
|
+
if msg.User is not None:
|
|
237
|
+
user_msg = msg.User
|
|
238
|
+
display_text, pydantic_content = parse_user_content(user_msg.content)
|
|
239
|
+
part = UserPromptPart(content=pydantic_content)
|
|
240
|
+
model_request = ModelRequest(parts=[part])
|
|
241
|
+
|
|
242
|
+
messages.append(
|
|
243
|
+
ChatMessage[str](
|
|
244
|
+
content=display_text,
|
|
245
|
+
conversation_id=thread_id,
|
|
246
|
+
role="user",
|
|
247
|
+
message_id=user_msg.id or msg_id,
|
|
248
|
+
name=None,
|
|
249
|
+
model_name=None,
|
|
250
|
+
timestamp=updated_at, # Zed doesn't store per-message timestamps
|
|
251
|
+
messages=[model_request],
|
|
252
|
+
)
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
elif msg.Agent is not None:
|
|
256
|
+
agent_msg = msg.Agent
|
|
257
|
+
display_text, pydantic_parts = parse_agent_content(agent_msg.content)
|
|
258
|
+
# Build ModelResponse
|
|
259
|
+
usage = RequestUsage()
|
|
260
|
+
model_response = ModelResponse(parts=pydantic_parts, usage=usage, model_name=model_name)
|
|
261
|
+
# Build tool return parts for next request if there are tool results
|
|
262
|
+
tool_return_parts = parse_tool_results(agent_msg.tool_results)
|
|
263
|
+
# Create the messages list - response, then optionally tool returns
|
|
264
|
+
pydantic_messages: list[ModelResponse | ModelRequest] = [model_response]
|
|
265
|
+
if tool_return_parts:
|
|
266
|
+
pydantic_messages.append(ModelRequest(parts=tool_return_parts))
|
|
267
|
+
|
|
268
|
+
messages.append(
|
|
269
|
+
ChatMessage[str](
|
|
270
|
+
content=display_text,
|
|
271
|
+
conversation_id=thread_id,
|
|
272
|
+
role="assistant",
|
|
273
|
+
message_id=msg_id,
|
|
274
|
+
name="zed",
|
|
275
|
+
model_name=model_name,
|
|
276
|
+
timestamp=updated_at,
|
|
277
|
+
messages=pydantic_messages,
|
|
278
|
+
)
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
return messages
|