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
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
"""Helper functions for OpenCode storage provider.
|
|
2
|
+
|
|
3
|
+
Stateless conversion and utility functions for working with OpenCode format.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import base64
|
|
9
|
+
from datetime import UTC, datetime
|
|
10
|
+
from decimal import Decimal
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
import anyenv
|
|
14
|
+
from pydantic_ai import RunUsage
|
|
15
|
+
from pydantic_ai.messages import (
|
|
16
|
+
AudioUrl,
|
|
17
|
+
BinaryContent,
|
|
18
|
+
DocumentUrl,
|
|
19
|
+
ImageUrl,
|
|
20
|
+
ModelRequest,
|
|
21
|
+
ModelResponse,
|
|
22
|
+
TextPart,
|
|
23
|
+
ThinkingPart,
|
|
24
|
+
ToolCallPart,
|
|
25
|
+
ToolReturnPart,
|
|
26
|
+
UserPromptPart,
|
|
27
|
+
VideoUrl,
|
|
28
|
+
)
|
|
29
|
+
from pydantic_ai.usage import RequestUsage
|
|
30
|
+
|
|
31
|
+
from agentpool.log import get_logger
|
|
32
|
+
from agentpool.messaging import ChatMessage, TokenCost
|
|
33
|
+
from agentpool_server.opencode_server.models import (
|
|
34
|
+
AssistantMessage,
|
|
35
|
+
FilePart as OpenCodeFilePart,
|
|
36
|
+
ReasoningPart as OpenCodeReasoningPart,
|
|
37
|
+
Session,
|
|
38
|
+
TextPart as OpenCodeTextPart,
|
|
39
|
+
ToolPart as OpenCodeToolPart,
|
|
40
|
+
ToolStateCompleted,
|
|
41
|
+
UserMessage,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
if TYPE_CHECKING:
|
|
46
|
+
from collections.abc import Sequence
|
|
47
|
+
from pathlib import Path
|
|
48
|
+
|
|
49
|
+
from pydantic_ai.messages import (
|
|
50
|
+
UserContent,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
from agentpool_server.opencode_server.models import (
|
|
54
|
+
MessageInfo as OpenCodeMessage,
|
|
55
|
+
Part as OpenCodePart,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
logger = get_logger(__name__)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def ms_to_datetime(ms: int) -> datetime:
|
|
63
|
+
"""Convert milliseconds timestamp to datetime.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
ms: Milliseconds since epoch
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Datetime object in UTC
|
|
70
|
+
"""
|
|
71
|
+
return datetime.fromtimestamp(ms / 1000, tz=UTC)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def convert_user_content_to_parts(
|
|
75
|
+
content: str | Sequence[UserContent],
|
|
76
|
+
message_id: str,
|
|
77
|
+
session_id: str,
|
|
78
|
+
part_counter_start: int,
|
|
79
|
+
) -> list[OpenCodeTextPart | OpenCodeFilePart]:
|
|
80
|
+
"""Convert UserContent to OpenCode parts.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
content: User content (str or Sequence[UserContent])
|
|
84
|
+
message_id: Message ID
|
|
85
|
+
session_id: Session ID
|
|
86
|
+
part_counter_start: Starting counter for part IDs
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
List of OpenCode parts (TextPart for text/strings, FilePart for media)
|
|
90
|
+
|
|
91
|
+
Note:
|
|
92
|
+
CachePoint markers are lost in conversion (no OpenCode equivalent)
|
|
93
|
+
"""
|
|
94
|
+
parts: list[OpenCodeTextPart | OpenCodeFilePart] = []
|
|
95
|
+
part_counter = part_counter_start
|
|
96
|
+
|
|
97
|
+
if isinstance(content, str):
|
|
98
|
+
# Simple text
|
|
99
|
+
part_id = f"{message_id}-{part_counter}"
|
|
100
|
+
parts.append(
|
|
101
|
+
OpenCodeTextPart(
|
|
102
|
+
id=part_id,
|
|
103
|
+
session_id=session_id,
|
|
104
|
+
message_id=message_id,
|
|
105
|
+
type="text",
|
|
106
|
+
text=content,
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
else:
|
|
110
|
+
# Sequence of UserContent
|
|
111
|
+
for content_item in content:
|
|
112
|
+
part_id = f"{message_id}-{part_counter}"
|
|
113
|
+
part_counter += 1
|
|
114
|
+
|
|
115
|
+
if isinstance(content_item, str):
|
|
116
|
+
# Text content
|
|
117
|
+
parts.append(
|
|
118
|
+
OpenCodeTextPart(
|
|
119
|
+
id=part_id,
|
|
120
|
+
session_id=session_id,
|
|
121
|
+
message_id=message_id,
|
|
122
|
+
type="text",
|
|
123
|
+
text=content_item,
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
elif isinstance(content_item, ImageUrl):
|
|
127
|
+
# Image URL
|
|
128
|
+
parts.append(
|
|
129
|
+
OpenCodeFilePart(
|
|
130
|
+
id=part_id,
|
|
131
|
+
session_id=session_id,
|
|
132
|
+
message_id=message_id,
|
|
133
|
+
type="file",
|
|
134
|
+
mime=content_item.media_type or "image/*",
|
|
135
|
+
url=content_item.url,
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
elif isinstance(content_item, AudioUrl):
|
|
139
|
+
# Audio URL
|
|
140
|
+
parts.append(
|
|
141
|
+
OpenCodeFilePart(
|
|
142
|
+
id=part_id,
|
|
143
|
+
session_id=session_id,
|
|
144
|
+
message_id=message_id,
|
|
145
|
+
type="file",
|
|
146
|
+
mime=content_item.media_type or "audio/*",
|
|
147
|
+
url=content_item.url,
|
|
148
|
+
)
|
|
149
|
+
)
|
|
150
|
+
elif isinstance(content_item, DocumentUrl):
|
|
151
|
+
# Document URL
|
|
152
|
+
parts.append(
|
|
153
|
+
OpenCodeFilePart(
|
|
154
|
+
id=part_id,
|
|
155
|
+
session_id=session_id,
|
|
156
|
+
message_id=message_id,
|
|
157
|
+
type="file",
|
|
158
|
+
mime=content_item.media_type or "application/pdf",
|
|
159
|
+
url=content_item.url,
|
|
160
|
+
)
|
|
161
|
+
)
|
|
162
|
+
elif isinstance(content_item, VideoUrl):
|
|
163
|
+
# Video URL
|
|
164
|
+
parts.append(
|
|
165
|
+
OpenCodeFilePart(
|
|
166
|
+
id=part_id,
|
|
167
|
+
session_id=session_id,
|
|
168
|
+
message_id=message_id,
|
|
169
|
+
type="file",
|
|
170
|
+
mime=content_item.media_type or "video/*",
|
|
171
|
+
url=content_item.url,
|
|
172
|
+
)
|
|
173
|
+
)
|
|
174
|
+
elif isinstance(content_item, BinaryContent):
|
|
175
|
+
# Binary content - convert to data URL
|
|
176
|
+
b64_data = base64.b64encode(content_item.data).decode("ascii")
|
|
177
|
+
data_url = f"data:{content_item.media_type};base64,{b64_data}"
|
|
178
|
+
parts.append(
|
|
179
|
+
OpenCodeFilePart(
|
|
180
|
+
id=part_id,
|
|
181
|
+
session_id=session_id,
|
|
182
|
+
message_id=message_id,
|
|
183
|
+
type="file",
|
|
184
|
+
mime=content_item.media_type,
|
|
185
|
+
url=data_url,
|
|
186
|
+
)
|
|
187
|
+
)
|
|
188
|
+
# CachePoint is just a marker for prompt caching - no data to store
|
|
189
|
+
|
|
190
|
+
return parts
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def extract_text_content(parts: list[OpenCodePart]) -> str:
|
|
194
|
+
"""Extract text content from parts for display.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
parts: List of OpenCode parts
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
Combined text content from all text and reasoning parts
|
|
201
|
+
"""
|
|
202
|
+
text_parts: list[str] = []
|
|
203
|
+
for part in parts:
|
|
204
|
+
if isinstance(part, OpenCodeTextPart) and part.text:
|
|
205
|
+
text_parts.append(part.text)
|
|
206
|
+
elif isinstance(part, OpenCodeReasoningPart) and part.text:
|
|
207
|
+
text_parts.append(f"<thinking>\n{part.text}\n</thinking>")
|
|
208
|
+
return "\n".join(text_parts)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def build_pydantic_messages( # noqa: PLR0915
|
|
212
|
+
msg: OpenCodeMessage,
|
|
213
|
+
parts: list[OpenCodePart],
|
|
214
|
+
timestamp: datetime,
|
|
215
|
+
) -> list[ModelRequest | ModelResponse]:
|
|
216
|
+
"""Build pydantic-ai ModelRequest and/or ModelResponse from OpenCode data.
|
|
217
|
+
|
|
218
|
+
In OpenCode's model, assistant messages contain both tool calls AND their
|
|
219
|
+
results in the same message. We split these into:
|
|
220
|
+
- ModelResponse with ToolCallPart (the call)
|
|
221
|
+
- ModelRequest with ToolReturnPart (the result)
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
msg: OpenCode message metadata
|
|
225
|
+
parts: OpenCode message parts
|
|
226
|
+
timestamp: Message timestamp
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
List of pydantic-ai messages (ModelRequest and/or ModelResponse)
|
|
230
|
+
"""
|
|
231
|
+
result: list[ModelRequest | ModelResponse] = []
|
|
232
|
+
|
|
233
|
+
if isinstance(msg, UserMessage):
|
|
234
|
+
# Build UserPromptPart content from text and file parts
|
|
235
|
+
user_content: list[UserContent] = []
|
|
236
|
+
|
|
237
|
+
for part in parts:
|
|
238
|
+
if isinstance(part, OpenCodeTextPart) and part.text:
|
|
239
|
+
user_content.append(part.text)
|
|
240
|
+
elif isinstance(part, OpenCodeFilePart):
|
|
241
|
+
# Convert FilePart back to appropriate UserContent type
|
|
242
|
+
url = part.url
|
|
243
|
+
mime = part.mime
|
|
244
|
+
|
|
245
|
+
# Detect data URLs for BinaryContent
|
|
246
|
+
if url.startswith("data:"):
|
|
247
|
+
# Parse data URL: data:mime;base64,data
|
|
248
|
+
if ";base64," in url:
|
|
249
|
+
mime_part, b64_data = url.split(";base64,", 1)
|
|
250
|
+
media_type = mime_part.replace("data:", "")
|
|
251
|
+
data = base64.b64decode(b64_data)
|
|
252
|
+
user_content.append(BinaryContent(data=data, media_type=media_type))
|
|
253
|
+
continue
|
|
254
|
+
|
|
255
|
+
# Convert to appropriate URL type based on MIME
|
|
256
|
+
if mime.startswith("image/") or mime == "image/*":
|
|
257
|
+
user_content.append(
|
|
258
|
+
ImageUrl(url=url, media_type=mime if mime != "image/*" else None)
|
|
259
|
+
)
|
|
260
|
+
elif mime.startswith("audio/") or mime == "audio/*":
|
|
261
|
+
user_content.append(
|
|
262
|
+
AudioUrl(url=url, media_type=mime if mime != "audio/*" else None)
|
|
263
|
+
)
|
|
264
|
+
elif mime.startswith("video/") or mime == "video/*":
|
|
265
|
+
user_content.append(
|
|
266
|
+
VideoUrl(url=url, media_type=mime if mime != "video/*" else None)
|
|
267
|
+
)
|
|
268
|
+
elif mime == "application/pdf" or mime.startswith("application/"):
|
|
269
|
+
user_content.append(DocumentUrl(url=url, media_type=mime))
|
|
270
|
+
else:
|
|
271
|
+
# Unknown MIME type - treat as document
|
|
272
|
+
user_content.append(DocumentUrl(url=url, media_type=mime))
|
|
273
|
+
|
|
274
|
+
if user_content:
|
|
275
|
+
# Create UserPromptPart with content (str or Sequence)
|
|
276
|
+
content: str | Sequence[UserContent]
|
|
277
|
+
if len(user_content) == 1 and isinstance(user_content[0], str):
|
|
278
|
+
content = user_content[0]
|
|
279
|
+
else:
|
|
280
|
+
content = user_content
|
|
281
|
+
|
|
282
|
+
request_parts: list[UserPromptPart | ToolReturnPart] = [
|
|
283
|
+
UserPromptPart(content=content, timestamp=timestamp)
|
|
284
|
+
]
|
|
285
|
+
result.append(ModelRequest(parts=request_parts, timestamp=timestamp))
|
|
286
|
+
|
|
287
|
+
return result
|
|
288
|
+
|
|
289
|
+
# Assistant message - may contain both tool calls and results
|
|
290
|
+
response_parts: list[TextPart | ToolCallPart | ThinkingPart] = []
|
|
291
|
+
tool_return_parts: list[ToolReturnPart] = []
|
|
292
|
+
|
|
293
|
+
# Build usage
|
|
294
|
+
usage = RequestUsage()
|
|
295
|
+
if isinstance(msg, AssistantMessage) and msg.tokens:
|
|
296
|
+
usage = RequestUsage(
|
|
297
|
+
input_tokens=msg.tokens.input,
|
|
298
|
+
output_tokens=msg.tokens.output,
|
|
299
|
+
cache_read_tokens=msg.tokens.cache.read,
|
|
300
|
+
cache_write_tokens=msg.tokens.cache.write,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
for part in parts:
|
|
304
|
+
if isinstance(part, OpenCodeTextPart) and part.text:
|
|
305
|
+
response_parts.append(TextPart(content=part.text))
|
|
306
|
+
elif isinstance(part, OpenCodeReasoningPart) and part.text:
|
|
307
|
+
response_parts.append(ThinkingPart(content=part.text))
|
|
308
|
+
elif isinstance(part, OpenCodeToolPart):
|
|
309
|
+
# Add tool call to response
|
|
310
|
+
args = part.state.input or {}
|
|
311
|
+
tc_part = ToolCallPart(tool_name=part.tool, args=args, tool_call_id=part.call_id)
|
|
312
|
+
response_parts.append(tc_part)
|
|
313
|
+
# If completed, also create a tool return
|
|
314
|
+
if isinstance(part.state, ToolStateCompleted) and part.state.output:
|
|
315
|
+
return_part = ToolReturnPart(
|
|
316
|
+
tool_name=part.tool,
|
|
317
|
+
content=part.state.output,
|
|
318
|
+
tool_call_id=part.call_id,
|
|
319
|
+
timestamp=timestamp,
|
|
320
|
+
)
|
|
321
|
+
tool_return_parts.append(return_part)
|
|
322
|
+
|
|
323
|
+
# Add the response if we have parts
|
|
324
|
+
if response_parts:
|
|
325
|
+
# AssistantMessage only has model_id, not model
|
|
326
|
+
model_name = msg.model_id if isinstance(msg, AssistantMessage) else None
|
|
327
|
+
result.append(
|
|
328
|
+
ModelResponse(
|
|
329
|
+
parts=response_parts,
|
|
330
|
+
usage=usage,
|
|
331
|
+
model_name=model_name,
|
|
332
|
+
timestamp=timestamp,
|
|
333
|
+
)
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
# Add tool returns as a separate request (simulating user sending results back)
|
|
337
|
+
if tool_return_parts:
|
|
338
|
+
result.append(ModelRequest(parts=tool_return_parts, timestamp=timestamp))
|
|
339
|
+
|
|
340
|
+
return result
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def read_session(session_path: Path) -> Session | None:
|
|
344
|
+
"""Read session metadata from file.
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
session_path: Path to session JSON file
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
Session object or None if read/parse fails
|
|
351
|
+
"""
|
|
352
|
+
if not session_path.exists():
|
|
353
|
+
return None
|
|
354
|
+
try:
|
|
355
|
+
content = session_path.read_text(encoding="utf-8")
|
|
356
|
+
return anyenv.load_json(content, return_type=Session)
|
|
357
|
+
except anyenv.JsonLoadError as e:
|
|
358
|
+
logger.warning("Failed to parse session", path=str(session_path), error=str(e))
|
|
359
|
+
return None
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def message_to_chat_message(
|
|
363
|
+
msg: OpenCodeMessage,
|
|
364
|
+
parts: list[OpenCodePart],
|
|
365
|
+
conversation_id: str,
|
|
366
|
+
) -> ChatMessage[str]:
|
|
367
|
+
"""Convert OpenCode message + parts to ChatMessage.
|
|
368
|
+
|
|
369
|
+
Args:
|
|
370
|
+
msg: OpenCode message metadata
|
|
371
|
+
parts: OpenCode message parts
|
|
372
|
+
conversation_id: Conversation/session ID
|
|
373
|
+
|
|
374
|
+
Returns:
|
|
375
|
+
ChatMessage suitable for agentpool storage
|
|
376
|
+
"""
|
|
377
|
+
timestamp = ms_to_datetime(msg.time.created)
|
|
378
|
+
# Extract text content for display
|
|
379
|
+
content = extract_text_content(parts)
|
|
380
|
+
# Build pydantic-ai messages
|
|
381
|
+
pydantic_messages = build_pydantic_messages(msg, parts, timestamp)
|
|
382
|
+
# Extract cost info (only for assistant messages)
|
|
383
|
+
cost_info = None
|
|
384
|
+
model_name = None
|
|
385
|
+
parent_id = None
|
|
386
|
+
provider_details = {}
|
|
387
|
+
if isinstance(msg, AssistantMessage):
|
|
388
|
+
if msg.tokens:
|
|
389
|
+
input_tokens = msg.tokens.input + msg.tokens.cache.read
|
|
390
|
+
output_tokens = msg.tokens.output
|
|
391
|
+
if input_tokens or output_tokens:
|
|
392
|
+
usage = RunUsage(input_tokens=input_tokens, output_tokens=output_tokens)
|
|
393
|
+
cost = Decimal(str(msg.cost)) if msg.cost else Decimal(0)
|
|
394
|
+
cost_info = TokenCost(token_usage=usage, total_cost=cost)
|
|
395
|
+
model_name = msg.model_id
|
|
396
|
+
parent_id = msg.parent_id
|
|
397
|
+
if msg.finish:
|
|
398
|
+
provider_details["finish_reason"] = msg.finish
|
|
399
|
+
elif isinstance(msg, UserMessage) and msg.model:
|
|
400
|
+
model_name = msg.model.model_id
|
|
401
|
+
|
|
402
|
+
return ChatMessage[str](
|
|
403
|
+
content=content,
|
|
404
|
+
conversation_id=conversation_id,
|
|
405
|
+
role=msg.role,
|
|
406
|
+
message_id=msg.id,
|
|
407
|
+
name=msg.agent,
|
|
408
|
+
model_name=model_name,
|
|
409
|
+
cost_info=cost_info,
|
|
410
|
+
timestamp=timestamp,
|
|
411
|
+
parent_id=parent_id,
|
|
412
|
+
messages=pydantic_messages,
|
|
413
|
+
provider_details=provider_details,
|
|
414
|
+
)
|