agentpool 2.1.9__py3-none-any.whl → 2.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- acp/__init__.py +13 -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/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/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 +20 -50
- 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/stdio.py +39 -9
- acp/task/supervisor.py +2 -2
- acp/transports.py +362 -2
- acp/utils.py +17 -4
- agentpool/__init__.py +6 -1
- agentpool/agents/__init__.py +2 -0
- agentpool/agents/acp_agent/acp_agent.py +407 -277
- agentpool/agents/acp_agent/acp_converters.py +196 -38
- agentpool/agents/acp_agent/client_handler.py +191 -26
- agentpool/agents/acp_agent/session_state.py +17 -6
- agentpool/agents/agent.py +607 -572
- agentpool/agents/agui_agent/__init__.py +0 -2
- agentpool/agents/agui_agent/agui_agent.py +176 -110
- agentpool/agents/agui_agent/agui_converters.py +0 -131
- agentpool/agents/agui_agent/helpers.py +3 -4
- agentpool/agents/base_agent.py +632 -17
- 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 +1058 -291
- agentpool/agents/claude_code_agent/converters.py +74 -143
- agentpool/agents/claude_code_agent/history.py +474 -0
- 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/context.py +40 -0
- agentpool/agents/events/__init__.py +24 -0
- agentpool/agents/events/builtin_handlers.py +67 -1
- agentpool/agents/events/event_emitter.py +32 -2
- agentpool/agents/events/events.py +104 -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 +67 -0
- agentpool/agents/slashed_agent.py +5 -4
- agentpool/agents/tool_call_accumulator.py +213 -0
- agentpool/agents/tool_wrapping.py +18 -6
- agentpool/common_types.py +56 -21
- agentpool/config_resources/__init__.py +38 -1
- 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 +10 -6
- agentpool/config_resources/external_acp_agents.yml +2 -1
- agentpool/delegation/base_team.py +4 -30
- agentpool/delegation/pool.py +136 -289
- agentpool/delegation/team.py +58 -57
- agentpool/delegation/teamrun.py +51 -55
- 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/functional/run.py +10 -4
- agentpool/mcp_server/__init__.py +0 -2
- agentpool/mcp_server/client.py +76 -32
- agentpool/mcp_server/conversions.py +54 -13
- agentpool/mcp_server/manager.py +34 -54
- agentpool/mcp_server/registries/official_registry_client.py +35 -1
- agentpool/mcp_server/tool_bridge.py +186 -139
- agentpool/messaging/__init__.py +0 -2
- agentpool/messaging/compaction.py +72 -197
- 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 +99 -8
- agentpool/messaging/messagenode.py +52 -14
- agentpool/messaging/messages.py +54 -35
- agentpool/messaging/processing.py +12 -22
- agentpool/models/__init__.py +1 -1
- agentpool/models/acp_agents/base.py +6 -24
- agentpool/models/acp_agents/mcp_capable.py +126 -157
- agentpool/models/acp_agents/non_mcp.py +129 -95
- agentpool/models/agents.py +98 -76
- agentpool/models/agui_agents.py +1 -1
- agentpool/models/claude_code_agents.py +144 -19
- agentpool/models/file_parsing.py +0 -1
- agentpool/models/manifest.py +113 -50
- agentpool/prompts/conversion_manager.py +1 -1
- agentpool/prompts/prompts.py +5 -2
- agentpool/repomap.py +1 -1
- agentpool/resource_providers/__init__.py +11 -1
- agentpool/resource_providers/aggregating.py +56 -5
- agentpool/resource_providers/base.py +70 -4
- 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 +89 -12
- agentpool/resource_providers/plan_provider.py +228 -46
- agentpool/resource_providers/pool.py +7 -3
- agentpool/resource_providers/resource_info.py +111 -0
- agentpool/resource_providers/static.py +4 -2
- agentpool/sessions/__init__.py +4 -1
- agentpool/sessions/manager.py +33 -5
- agentpool/sessions/models.py +59 -6
- agentpool/sessions/protocol.py +28 -0
- agentpool/sessions/session.py +11 -55
- agentpool/skills/registry.py +13 -8
- agentpool/storage/manager.py +572 -49
- agentpool/talk/registry.py +4 -4
- agentpool/talk/talk.py +9 -10
- agentpool/testing.py +538 -20
- 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/file_watcher.py +269 -0
- agentpool/utils/identifiers.py +121 -0
- agentpool/utils/pydantic_ai_helpers.py +46 -0
- agentpool/utils/streams.py +616 -2
- agentpool/utils/subprocess_utils.py +155 -0
- agentpool/utils/token_breakdown.py +461 -0
- agentpool/vfs_registry.py +7 -2
- {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/METADATA +41 -27
- agentpool-2.5.0.dist-info/RECORD +579 -0
- {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/WHEEL +1 -1
- agentpool_cli/__main__.py +24 -0
- agentpool_cli/create.py +1 -1
- agentpool_cli/serve_acp.py +100 -21
- agentpool_cli/serve_agui.py +87 -0
- agentpool_cli/serve_opencode.py +119 -0
- agentpool_cli/ui.py +557 -0
- agentpool_commands/__init__.py +42 -5
- agentpool_commands/agents.py +75 -2
- agentpool_commands/history.py +62 -0
- agentpool_commands/mcp.py +176 -0
- agentpool_commands/models.py +56 -3
- 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/tools.py +57 -0
- agentpool_commands/utils.py +80 -30
- agentpool_config/__init__.py +30 -2
- agentpool_config/agentpool_tools.py +498 -0
- agentpool_config/builtin_tools.py +77 -22
- agentpool_config/commands.py +24 -1
- agentpool_config/compaction.py +258 -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 +132 -6
- agentpool_config/nodes.py +1 -1
- agentpool_config/observability.py +44 -0
- agentpool_config/session.py +0 -3
- agentpool_config/storage.py +82 -38
- agentpool_config/task.py +3 -3
- agentpool_config/tools.py +11 -22
- agentpool_config/toolsets.py +109 -233
- agentpool_server/a2a_server/agent_worker.py +307 -0
- agentpool_server/a2a_server/server.py +23 -18
- agentpool_server/acp_server/acp_agent.py +234 -181
- agentpool_server/acp_server/commands/acp_commands.py +151 -156
- agentpool_server/acp_server/commands/docs_commands/fetch_repo.py +18 -17
- agentpool_server/acp_server/event_converter.py +651 -0
- agentpool_server/acp_server/input_provider.py +53 -10
- agentpool_server/acp_server/server.py +24 -90
- agentpool_server/acp_server/session.py +173 -331
- 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/.rules +95 -0
- agentpool_server/opencode_server/ENDPOINTS.md +401 -0
- agentpool_server/opencode_server/OPENCODE_UI_TOOLS_COMPLETE.md +202 -0
- agentpool_server/opencode_server/__init__.py +19 -0
- agentpool_server/opencode_server/command_validation.py +172 -0
- agentpool_server/opencode_server/converters.py +975 -0
- agentpool_server/opencode_server/dependencies.py +24 -0
- agentpool_server/opencode_server/input_provider.py +421 -0
- agentpool_server/opencode_server/models/__init__.py +250 -0
- agentpool_server/opencode_server/models/agent.py +53 -0
- agentpool_server/opencode_server/models/app.py +72 -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 +821 -0
- agentpool_server/opencode_server/models/file.py +88 -0
- agentpool_server/opencode_server/models/mcp.py +44 -0
- agentpool_server/opencode_server/models/message.py +179 -0
- agentpool_server/opencode_server/models/parts.py +323 -0
- agentpool_server/opencode_server/models/provider.py +81 -0
- agentpool_server/opencode_server/models/pty.py +43 -0
- agentpool_server/opencode_server/models/question.py +56 -0
- agentpool_server/opencode_server/models/session.py +111 -0
- agentpool_server/opencode_server/routes/__init__.py +29 -0
- agentpool_server/opencode_server/routes/agent_routes.py +473 -0
- agentpool_server/opencode_server/routes/app_routes.py +202 -0
- agentpool_server/opencode_server/routes/config_routes.py +302 -0
- agentpool_server/opencode_server/routes/file_routes.py +571 -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 +761 -0
- agentpool_server/opencode_server/routes/permission_routes.py +63 -0
- agentpool_server/opencode_server/routes/pty_routes.py +300 -0
- agentpool_server/opencode_server/routes/question_routes.py +128 -0
- agentpool_server/opencode_server/routes/session_routes.py +1276 -0
- agentpool_server/opencode_server/routes/tui_routes.py +139 -0
- agentpool_server/opencode_server/server.py +475 -0
- agentpool_server/opencode_server/state.py +151 -0
- agentpool_server/opencode_server/time_utils.py +8 -0
- agentpool_storage/__init__.py +12 -0
- agentpool_storage/base.py +184 -2
- agentpool_storage/claude_provider/ARCHITECTURE.md +433 -0
- agentpool_storage/claude_provider/__init__.py +42 -0
- agentpool_storage/claude_provider/provider.py +1089 -0
- agentpool_storage/file_provider.py +278 -15
- agentpool_storage/memory_provider.py +193 -12
- agentpool_storage/models.py +3 -0
- 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/project_store.py +325 -0
- agentpool_storage/session_store.py +26 -6
- agentpool_storage/sql_provider/__init__.py +4 -2
- agentpool_storage/sql_provider/models.py +48 -0
- agentpool_storage/sql_provider/sql_provider.py +269 -3
- agentpool_storage/sql_provider/utils.py +12 -13
- 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 -12
- agentpool_toolsets/builtin/code.py +96 -57
- agentpool_toolsets/builtin/debug.py +118 -48
- agentpool_toolsets/builtin/execution_environment.py +115 -230
- agentpool_toolsets/builtin/file_edit/file_edit.py +115 -7
- agentpool_toolsets/builtin/skills.py +9 -4
- 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/__init__.py +13 -1
- agentpool_toolsets/fsspec_toolset/diagnostics.py +860 -73
- agentpool_toolsets/fsspec_toolset/grep.py +99 -7
- agentpool_toolsets/fsspec_toolset/helpers.py +3 -2
- agentpool_toolsets/fsspec_toolset/image_utils.py +161 -0
- agentpool_toolsets/fsspec_toolset/toolset.py +500 -95
- agentpool_toolsets/mcp_discovery/__init__.py +5 -0
- agentpool_toolsets/mcp_discovery/data/mcp_servers.parquet +0 -0
- agentpool_toolsets/mcp_discovery/toolset.py +511 -0
- agentpool_toolsets/mcp_run_toolset.py +87 -12
- agentpool_toolsets/notifications.py +33 -33
- agentpool_toolsets/openapi.py +3 -1
- agentpool_toolsets/search_toolset.py +3 -1
- agentpool-2.1.9.dist-info/RECORD +0 -474
- 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/text_log_provider.py +0 -275
- agentpool_toolsets/builtin/agent_management.py +0 -239
- agentpool_toolsets/builtin/chain.py +0 -288
- agentpool_toolsets/builtin/history.py +0 -36
- agentpool_toolsets/builtin/integration.py +0 -85
- agentpool_toolsets/builtin/tool_management.py +0 -90
- agentpool_toolsets/builtin/user_interaction.py +0 -52
- agentpool_toolsets/semantic_memory_toolset.py +0 -536
- {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/entry_points.txt +0 -0
- {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -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,10 +126,10 @@ 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,
|
|
126
|
-
forwarded_from: list[str] | None = None,
|
|
127
133
|
provider_name: str | None = None,
|
|
128
134
|
provider_response_id: str | None = None,
|
|
129
135
|
messages: str | None = None,
|
|
@@ -138,6 +144,7 @@ class SQLModelProvider(StorageProvider):
|
|
|
138
144
|
msg = Message(
|
|
139
145
|
conversation_id=conversation_id,
|
|
140
146
|
id=message_id,
|
|
147
|
+
parent_id=parent_id,
|
|
141
148
|
content=content,
|
|
142
149
|
role=role,
|
|
143
150
|
name=name,
|
|
@@ -149,7 +156,6 @@ class SQLModelProvider(StorageProvider):
|
|
|
149
156
|
input_tokens=cost_info.token_usage.input_tokens if cost_info else None,
|
|
150
157
|
output_tokens=cost_info.token_usage.output_tokens if cost_info else None,
|
|
151
158
|
cost=float(cost_info.total_cost) if cost_info else None,
|
|
152
|
-
forwarded_from=forwarded_from,
|
|
153
159
|
provider_name=provider_name,
|
|
154
160
|
provider_response_id=provider_response_id,
|
|
155
161
|
messages=messages,
|
|
@@ -202,6 +208,141 @@ class SQLModelProvider(StorageProvider):
|
|
|
202
208
|
)
|
|
203
209
|
return result.scalar_one_or_none()
|
|
204
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
|
+
|
|
205
346
|
async def log_command(
|
|
206
347
|
self,
|
|
207
348
|
*,
|
|
@@ -442,3 +583,128 @@ class SQLModelProvider(StorageProvider):
|
|
|
442
583
|
msg_count = len(msg_result.scalars().all())
|
|
443
584
|
|
|
444
585
|
return conv_count, msg_count
|
|
586
|
+
|
|
587
|
+
async def delete_conversation_messages(
|
|
588
|
+
self,
|
|
589
|
+
conversation_id: str,
|
|
590
|
+
) -> int:
|
|
591
|
+
"""Delete all messages for a conversation."""
|
|
592
|
+
from sqlalchemy import delete, func
|
|
593
|
+
|
|
594
|
+
async with AsyncSession(self.engine) as session:
|
|
595
|
+
# First count messages to return
|
|
596
|
+
count_result = await session.execute(
|
|
597
|
+
select(func.count()).where(Message.conversation_id == conversation_id)
|
|
598
|
+
)
|
|
599
|
+
count = count_result.scalar() or 0
|
|
600
|
+
|
|
601
|
+
# Then delete
|
|
602
|
+
await session.execute(
|
|
603
|
+
delete(Message).where(Message.conversation_id == conversation_id) # type: ignore[arg-type]
|
|
604
|
+
)
|
|
605
|
+
await session.commit()
|
|
606
|
+
return count
|
|
607
|
+
|
|
608
|
+
# Project methods
|
|
609
|
+
|
|
610
|
+
def _to_project_data(self, row: Project) -> ProjectData:
|
|
611
|
+
"""Convert database model to ProjectData."""
|
|
612
|
+
from agentpool.sessions.models import ProjectData
|
|
613
|
+
|
|
614
|
+
return ProjectData(
|
|
615
|
+
project_id=row.project_id,
|
|
616
|
+
worktree=row.worktree,
|
|
617
|
+
name=row.name,
|
|
618
|
+
vcs=row.vcs,
|
|
619
|
+
config_path=row.config_path,
|
|
620
|
+
created_at=row.created_at,
|
|
621
|
+
last_active=row.last_active,
|
|
622
|
+
settings=row.settings_json or {},
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
def _to_project_model(self, data: ProjectData) -> Project:
|
|
626
|
+
"""Convert ProjectData to database model."""
|
|
627
|
+
return Project(
|
|
628
|
+
project_id=data.project_id,
|
|
629
|
+
worktree=data.worktree,
|
|
630
|
+
name=data.name,
|
|
631
|
+
vcs=data.vcs,
|
|
632
|
+
config_path=data.config_path,
|
|
633
|
+
created_at=data.created_at,
|
|
634
|
+
last_active=data.last_active,
|
|
635
|
+
settings_json=data.settings,
|
|
636
|
+
)
|
|
637
|
+
|
|
638
|
+
async def save_project(self, project: ProjectData) -> None:
|
|
639
|
+
"""Save or update a project."""
|
|
640
|
+
from sqlalchemy import delete
|
|
641
|
+
|
|
642
|
+
async with AsyncSession(self.engine) as session:
|
|
643
|
+
# Delete existing if present (upsert via delete+insert)
|
|
644
|
+
stmt = delete(Project).where(Project.project_id == project.project_id) # type: ignore[arg-type]
|
|
645
|
+
await session.execute(stmt)
|
|
646
|
+
|
|
647
|
+
# Insert new/updated
|
|
648
|
+
db_project = self._to_project_model(project)
|
|
649
|
+
session.add(db_project)
|
|
650
|
+
await session.commit()
|
|
651
|
+
logger.debug("Saved project", project_id=project.project_id)
|
|
652
|
+
|
|
653
|
+
async def get_project(self, project_id: str) -> ProjectData | None:
|
|
654
|
+
"""Get a project by ID."""
|
|
655
|
+
async with AsyncSession(self.engine) as session:
|
|
656
|
+
stmt = select(Project).where(Project.project_id == project_id)
|
|
657
|
+
result = await session.execute(stmt)
|
|
658
|
+
row = result.scalars().first()
|
|
659
|
+
return self._to_project_data(row) if row else None
|
|
660
|
+
|
|
661
|
+
async def get_project_by_worktree(self, worktree: str) -> ProjectData | None:
|
|
662
|
+
"""Get a project by worktree path."""
|
|
663
|
+
async with AsyncSession(self.engine) as session:
|
|
664
|
+
stmt = select(Project).where(Project.worktree == worktree)
|
|
665
|
+
result = await session.execute(stmt)
|
|
666
|
+
row = result.scalars().first()
|
|
667
|
+
return self._to_project_data(row) if row else None
|
|
668
|
+
|
|
669
|
+
async def get_project_by_name(self, name: str) -> ProjectData | None:
|
|
670
|
+
"""Get a project by friendly name."""
|
|
671
|
+
async with AsyncSession(self.engine) as session:
|
|
672
|
+
stmt = select(Project).where(Project.name == name)
|
|
673
|
+
result = await session.execute(stmt)
|
|
674
|
+
row = result.scalars().first()
|
|
675
|
+
return self._to_project_data(row) if row else None
|
|
676
|
+
|
|
677
|
+
async def list_projects(self, limit: int | None = None) -> list[ProjectData]:
|
|
678
|
+
"""List all projects, ordered by last_active descending."""
|
|
679
|
+
async with AsyncSession(self.engine) as session:
|
|
680
|
+
stmt = select(Project).order_by(desc(Project.last_active))
|
|
681
|
+
if limit is not None:
|
|
682
|
+
stmt = stmt.limit(limit)
|
|
683
|
+
result = await session.execute(stmt)
|
|
684
|
+
return [self._to_project_data(row) for row in result.scalars().all()]
|
|
685
|
+
|
|
686
|
+
async def delete_project(self, project_id: str) -> bool:
|
|
687
|
+
"""Delete a project."""
|
|
688
|
+
from sqlalchemy import delete
|
|
689
|
+
|
|
690
|
+
async with AsyncSession(self.engine) as session:
|
|
691
|
+
stmt = delete(Project).where(Project.project_id == project_id) # type: ignore[arg-type]
|
|
692
|
+
result = await session.execute(stmt)
|
|
693
|
+
await session.commit()
|
|
694
|
+
deleted: bool = result.rowcount > 0 # type: ignore[attr-defined]
|
|
695
|
+
if deleted:
|
|
696
|
+
logger.debug("Deleted project", project_id=project_id)
|
|
697
|
+
return deleted
|
|
698
|
+
|
|
699
|
+
async def touch_project(self, project_id: str) -> None:
|
|
700
|
+
"""Update project's last_active timestamp."""
|
|
701
|
+
from sqlalchemy import update
|
|
702
|
+
|
|
703
|
+
async with AsyncSession(self.engine) as session:
|
|
704
|
+
stmt = (
|
|
705
|
+
update(Project)
|
|
706
|
+
.where(Project.project_id == project_id) # type: ignore[arg-type]
|
|
707
|
+
.values(last_active=get_now())
|
|
708
|
+
)
|
|
709
|
+
await session.execute(stmt)
|
|
710
|
+
await session.commit()
|
|
@@ -2,13 +2,13 @@
|
|
|
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
|
|
8
9
|
|
|
9
10
|
from pydantic_ai import RunUsage
|
|
10
|
-
from sqlalchemy import
|
|
11
|
-
from sqlalchemy.sql import expression
|
|
11
|
+
from sqlalchemy import Column, and_
|
|
12
12
|
from sqlmodel import select
|
|
13
13
|
|
|
14
14
|
from agentpool.messaging import ChatMessage, TokenCost
|
|
@@ -67,7 +67,6 @@ def to_chat_message(db_message: Message) -> ChatMessage[str]:
|
|
|
67
67
|
model_name=db_message.model,
|
|
68
68
|
cost_info=cost_info,
|
|
69
69
|
response_time=db_message.response_time,
|
|
70
|
-
forwarded_from=db_message.forwarded_from or [],
|
|
71
70
|
timestamp=db_message.timestamp,
|
|
72
71
|
provider_name=db_message.provider_name,
|
|
73
72
|
provider_response_id=db_message.provider_response_id,
|
|
@@ -101,9 +100,14 @@ def auto_migrate_columns(sync_conn: Any, dialect: Any) -> None:
|
|
|
101
100
|
from sqlmodel import SQLModel
|
|
102
101
|
|
|
103
102
|
inspector = inspect(sync_conn)
|
|
103
|
+
existing_tables = set(inspector.get_table_names())
|
|
104
104
|
|
|
105
105
|
# For each table in our models
|
|
106
106
|
for table_name, table in SQLModel.metadata.tables.items():
|
|
107
|
+
# Skip tables that don't exist yet (they'll be created fresh)
|
|
108
|
+
if table_name not in existing_tables:
|
|
109
|
+
continue
|
|
110
|
+
|
|
107
111
|
existing = {col["name"] for col in inspector.get_columns(table_name)}
|
|
108
112
|
|
|
109
113
|
# For each column in model that doesn't exist in DB
|
|
@@ -116,7 +120,9 @@ def auto_migrate_columns(sync_conn: Any, dialect: Any) -> None:
|
|
|
116
120
|
sql = (
|
|
117
121
|
f"ALTER TABLE {table_name} ADD COLUMN {col.name} {type_sql}{nullable}{default}"
|
|
118
122
|
)
|
|
119
|
-
|
|
123
|
+
# Column may already exist (race condition or stale inspector cache)
|
|
124
|
+
with contextlib.suppress(Exception):
|
|
125
|
+
sync_conn.execute(text(sql))
|
|
120
126
|
|
|
121
127
|
|
|
122
128
|
def parse_model_info(model: str | None) -> tuple[str | None, str | None]:
|
|
@@ -160,15 +166,7 @@ def build_message_query(query: SessionQuery) -> SelectOfScalar[Any]:
|
|
|
160
166
|
if query.name:
|
|
161
167
|
conditions.append(Message.conversation_id == query.name)
|
|
162
168
|
if query.agents:
|
|
163
|
-
|
|
164
|
-
if query.include_forwarded:
|
|
165
|
-
agent_conditions.append(
|
|
166
|
-
and_(
|
|
167
|
-
Column("forwarded_from").isnot(None),
|
|
168
|
-
expression.cast(Column("forwarded_from"), JSON).contains(list(query.agents)), # type: ignore
|
|
169
|
-
)
|
|
170
|
-
)
|
|
171
|
-
conditions.append(or_(*agent_conditions))
|
|
169
|
+
conditions.append(Column("name").in_(query.agents))
|
|
172
170
|
if query.since and (cutoff := query.get_time_cutoff()):
|
|
173
171
|
conditions.append(Message.timestamp >= cutoff)
|
|
174
172
|
if query.until:
|
|
@@ -222,6 +220,7 @@ def format_conversation(
|
|
|
222
220
|
},
|
|
223
221
|
cost=float(msg.cost_info.total_cost) if msg.cost_info else None,
|
|
224
222
|
response_time=msg.response_time,
|
|
223
|
+
parent_id=msg.parent_id,
|
|
225
224
|
)
|
|
226
225
|
for msg in chat_messages
|
|
227
226
|
],
|
|
@@ -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
|