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,462 @@
|
|
|
1
|
+
"""OpenCode text sharing provider.
|
|
2
|
+
|
|
3
|
+
This implementation "fakes" OpenCode sessions by exploiting the fact that OpenCode's
|
|
4
|
+
share API doesn't validate session existence. The workflow is:
|
|
5
|
+
|
|
6
|
+
1. Generate random UUIDs for session, message, and part IDs
|
|
7
|
+
2. Call `/share_create` with the session ID to get a secret and share URL
|
|
8
|
+
3. Use `/share_sync` to populate the session with:
|
|
9
|
+
- Session info (title, timestamps, metadata)
|
|
10
|
+
- Messages (with different roles: user, assistant, system)
|
|
11
|
+
- Text parts (containing the actual shared content)
|
|
12
|
+
|
|
13
|
+
The OpenCode web interface then displays this as if it were a real session.
|
|
14
|
+
|
|
15
|
+
Two sharing modes are supported:
|
|
16
|
+
- `share()`: Simple string sharing (single user message)
|
|
17
|
+
- `share_conversation()`: Structured multi-turn conversations with different roles
|
|
18
|
+
|
|
19
|
+
Note: This is not the intended use of OpenCode's API, which is designed to share
|
|
20
|
+
actual OpenCode sessions created via their desktop app/CLI. However, since no
|
|
21
|
+
authentication is required and sessions aren't validated, this works for sharing
|
|
22
|
+
text and conversations.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import time
|
|
28
|
+
from typing import TYPE_CHECKING, Any, Self
|
|
29
|
+
import uuid
|
|
30
|
+
|
|
31
|
+
import httpx
|
|
32
|
+
|
|
33
|
+
from agentpool_commands.text_sharing.base import ShareResult, TextSharer
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
if TYPE_CHECKING:
|
|
37
|
+
from anyenv.text_sharing.base import Visibility
|
|
38
|
+
|
|
39
|
+
from agentpool.messaging.message_history import MessageHistory
|
|
40
|
+
from agentpool.messaging.messages import ChatMessage
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class OpenCodeSharer(TextSharer):
|
|
44
|
+
"""OpenCode text sharing service.
|
|
45
|
+
|
|
46
|
+
Creates fake sessions by generating UUIDs and syncing content via the share API.
|
|
47
|
+
OpenCode doesn't validate session existence, allowing us to create ad-hoc shares.
|
|
48
|
+
|
|
49
|
+
Examples:
|
|
50
|
+
Share a simple string:
|
|
51
|
+
```python
|
|
52
|
+
async with OpenCodeSharer() as sharer:
|
|
53
|
+
result = await sharer.share("Hello, World!", title="My Share")
|
|
54
|
+
print(result.url) # https://opencode.ai/s/abc12345
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Share a conversation:
|
|
58
|
+
```python
|
|
59
|
+
# Use share_conversation() with a Conversation object from agentpool
|
|
60
|
+
async with OpenCodeSharer() as sharer:
|
|
61
|
+
result = await sharer.share_conversation(
|
|
62
|
+
conversation,
|
|
63
|
+
title="Python Discussion"
|
|
64
|
+
)
|
|
65
|
+
print(result.url)
|
|
66
|
+
```
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
def __init__(
|
|
70
|
+
self,
|
|
71
|
+
*,
|
|
72
|
+
api_url: str | None = None,
|
|
73
|
+
timeout: float = 30.0,
|
|
74
|
+
) -> None:
|
|
75
|
+
"""Initialize OpenCode sharer.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
api_url: OpenCode API URL (defaults to production)
|
|
79
|
+
timeout: Request timeout in seconds
|
|
80
|
+
"""
|
|
81
|
+
self.api_url = api_url or "https://api.opencode.ai"
|
|
82
|
+
self.timeout = timeout
|
|
83
|
+
self._client = httpx.AsyncClient(timeout=timeout)
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def name(self) -> str:
|
|
87
|
+
"""Name of the sharing service."""
|
|
88
|
+
return "OpenCode"
|
|
89
|
+
|
|
90
|
+
async def share(
|
|
91
|
+
self,
|
|
92
|
+
content: str,
|
|
93
|
+
*,
|
|
94
|
+
title: str | None = None,
|
|
95
|
+
syntax: str | None = None,
|
|
96
|
+
visibility: Visibility = "unlisted",
|
|
97
|
+
expires_in: int | None = None,
|
|
98
|
+
) -> ShareResult:
|
|
99
|
+
"""Share text content via OpenCode.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
content: The text content to share
|
|
103
|
+
title: Optional title for the shared content
|
|
104
|
+
syntax: Syntax highlighting hint (ignored - OpenCode handles this)
|
|
105
|
+
visibility: Visibility level (ignored - OpenCode uses private shares)
|
|
106
|
+
expires_in: Expiration time (ignored - OpenCode doesn't support expiration)
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
ShareResult with OpenCode share URL
|
|
110
|
+
"""
|
|
111
|
+
try:
|
|
112
|
+
session_id = str(uuid.uuid4())
|
|
113
|
+
message_id = str(uuid.uuid4())
|
|
114
|
+
part_id = str(uuid.uuid4())
|
|
115
|
+
current_time = int(time.time() * 1000)
|
|
116
|
+
|
|
117
|
+
# Create share (returns secret and URL)
|
|
118
|
+
resp = await self._client.post(
|
|
119
|
+
f"{self.api_url}/share_create",
|
|
120
|
+
json={"sessionID": session_id},
|
|
121
|
+
)
|
|
122
|
+
resp.raise_for_status()
|
|
123
|
+
share_data = resp.json()
|
|
124
|
+
secret = share_data["secret"]
|
|
125
|
+
share_url = share_data["url"]
|
|
126
|
+
|
|
127
|
+
# Sync session info
|
|
128
|
+
info_key = f"session/info/{session_id}"
|
|
129
|
+
info_content = {
|
|
130
|
+
"id": session_id,
|
|
131
|
+
"projectID": "shared-content",
|
|
132
|
+
"directory": "/tmp",
|
|
133
|
+
"title": title or "Shared Content",
|
|
134
|
+
"version": "1.0.0",
|
|
135
|
+
"time": {
|
|
136
|
+
"created": current_time,
|
|
137
|
+
"updated": current_time,
|
|
138
|
+
},
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
resp = await self._client.post(
|
|
142
|
+
f"{self.api_url}/share_sync",
|
|
143
|
+
json={
|
|
144
|
+
"sessionID": session_id,
|
|
145
|
+
"secret": secret,
|
|
146
|
+
"key": info_key,
|
|
147
|
+
"content": info_content,
|
|
148
|
+
},
|
|
149
|
+
)
|
|
150
|
+
resp.raise_for_status()
|
|
151
|
+
|
|
152
|
+
# Sync message
|
|
153
|
+
msg_key = f"session/message/{session_id}/{message_id}"
|
|
154
|
+
msg_content = {
|
|
155
|
+
"id": message_id,
|
|
156
|
+
"sessionID": session_id,
|
|
157
|
+
"role": "user",
|
|
158
|
+
"time": {"created": current_time},
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
resp = await self._client.post(
|
|
162
|
+
f"{self.api_url}/share_sync",
|
|
163
|
+
json={
|
|
164
|
+
"sessionID": session_id,
|
|
165
|
+
"secret": secret,
|
|
166
|
+
"key": msg_key,
|
|
167
|
+
"content": msg_content,
|
|
168
|
+
},
|
|
169
|
+
)
|
|
170
|
+
resp.raise_for_status()
|
|
171
|
+
|
|
172
|
+
# Sync text part with actual content
|
|
173
|
+
part_key = f"session/part/{session_id}/{message_id}/{part_id}"
|
|
174
|
+
part_content = {
|
|
175
|
+
"id": part_id,
|
|
176
|
+
"sessionID": session_id,
|
|
177
|
+
"messageID": message_id,
|
|
178
|
+
"type": "text",
|
|
179
|
+
"text": content,
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
resp = await self._client.post(
|
|
183
|
+
f"{self.api_url}/share_sync",
|
|
184
|
+
json={
|
|
185
|
+
"sessionID": session_id,
|
|
186
|
+
"secret": secret,
|
|
187
|
+
"key": part_key,
|
|
188
|
+
"content": part_content,
|
|
189
|
+
},
|
|
190
|
+
)
|
|
191
|
+
resp.raise_for_status()
|
|
192
|
+
|
|
193
|
+
# Store secret in delete_url for later deletion
|
|
194
|
+
delete_url = f"{self.api_url}/share_delete#{secret}"
|
|
195
|
+
|
|
196
|
+
return ShareResult(
|
|
197
|
+
url=share_url,
|
|
198
|
+
raw_url=f"{self.api_url}/share_data?id={session_id[-8:]}",
|
|
199
|
+
delete_url=delete_url,
|
|
200
|
+
id=session_id,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
except httpx.HTTPStatusError as e:
|
|
204
|
+
if e.response.status_code == 404: # noqa: PLR2004
|
|
205
|
+
msg = "OpenCode API endpoint not found - service may be unavailable"
|
|
206
|
+
raise RuntimeError(msg) from e
|
|
207
|
+
if e.response.status_code == 429: # noqa: PLR2004
|
|
208
|
+
msg = "Rate limited by OpenCode API"
|
|
209
|
+
raise RuntimeError(msg) from e
|
|
210
|
+
msg = f"OpenCode API error (HTTP {e.response.status_code}): {e.response.text}"
|
|
211
|
+
raise RuntimeError(msg) from e
|
|
212
|
+
except httpx.RequestError as e:
|
|
213
|
+
msg = f"Failed to connect to OpenCode API: {e}"
|
|
214
|
+
raise RuntimeError(msg) from e
|
|
215
|
+
|
|
216
|
+
async def _share_chat_messages(
|
|
217
|
+
self,
|
|
218
|
+
messages: list[ChatMessage[Any]],
|
|
219
|
+
*,
|
|
220
|
+
title: str | None = None,
|
|
221
|
+
visibility: Visibility = "unlisted",
|
|
222
|
+
expires_in: int | None = None,
|
|
223
|
+
) -> ShareResult:
|
|
224
|
+
"""Share ChatMessages using the OpenCode API.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
messages: List of ChatMessage objects to share
|
|
228
|
+
|
|
229
|
+
This allows sharing AI chat sessions, multi-turn conversations,
|
|
230
|
+
or any structured dialogue with different roles.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
messages: List of messages to share (must have at least one)
|
|
234
|
+
title: Optional title for the conversation
|
|
235
|
+
visibility: Visibility level (ignored - OpenCode uses private shares)
|
|
236
|
+
expires_in: Expiration time (ignored - OpenCode doesn't support expiration)
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
ShareResult with OpenCode share URL
|
|
240
|
+
|
|
241
|
+
Example:
|
|
242
|
+
```python
|
|
243
|
+
messages = [
|
|
244
|
+
Message(role="user", parts=[MessagePart(type="text", text="Hello!")]),
|
|
245
|
+
Message(role="assistant", parts=[MessagePart(type="text", text="Hi!")]),
|
|
246
|
+
]
|
|
247
|
+
result = await sharer.share_conversation(messages, title="My Chat")
|
|
248
|
+
```
|
|
249
|
+
"""
|
|
250
|
+
if not messages:
|
|
251
|
+
msg = "Must provide at least one message"
|
|
252
|
+
raise ValueError(msg)
|
|
253
|
+
|
|
254
|
+
try:
|
|
255
|
+
session_id = str(uuid.uuid4())
|
|
256
|
+
current_time = int(time.time() * 1000)
|
|
257
|
+
|
|
258
|
+
# Create share
|
|
259
|
+
resp = await self._client.post(
|
|
260
|
+
f"{self.api_url}/share_create",
|
|
261
|
+
json={"sessionID": session_id},
|
|
262
|
+
)
|
|
263
|
+
resp.raise_for_status()
|
|
264
|
+
share_data = resp.json()
|
|
265
|
+
secret = share_data["secret"]
|
|
266
|
+
share_url = share_data["url"]
|
|
267
|
+
|
|
268
|
+
# Sync session info
|
|
269
|
+
info_key = f"session/info/{session_id}"
|
|
270
|
+
info_content = {
|
|
271
|
+
"id": session_id,
|
|
272
|
+
"projectID": "shared-conversation",
|
|
273
|
+
"directory": "/tmp",
|
|
274
|
+
"title": title or "Shared Conversation",
|
|
275
|
+
"version": "1.0.0",
|
|
276
|
+
"time": {
|
|
277
|
+
"created": current_time,
|
|
278
|
+
"updated": current_time,
|
|
279
|
+
},
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
resp = await self._client.post(
|
|
283
|
+
f"{self.api_url}/share_sync",
|
|
284
|
+
json={
|
|
285
|
+
"sessionID": session_id,
|
|
286
|
+
"secret": secret,
|
|
287
|
+
"key": info_key,
|
|
288
|
+
"content": info_content,
|
|
289
|
+
},
|
|
290
|
+
)
|
|
291
|
+
resp.raise_for_status()
|
|
292
|
+
|
|
293
|
+
# Sync each message and its parts
|
|
294
|
+
for chat_msg in messages:
|
|
295
|
+
from agentpool_server.opencode_server.converters import (
|
|
296
|
+
chat_message_to_opencode,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
# Convert ChatMessage to OpenCode format using the full converter
|
|
300
|
+
message_with_parts = chat_message_to_opencode(
|
|
301
|
+
chat_msg,
|
|
302
|
+
session_id=session_id,
|
|
303
|
+
working_dir="/tmp",
|
|
304
|
+
agent_name=chat_msg.name or "default",
|
|
305
|
+
model_id=chat_msg.model_name or "unknown",
|
|
306
|
+
provider_id="agentpool",
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
# Serialize to dicts with camelCase and no None values
|
|
310
|
+
msg_info = message_with_parts.info.model_dump(by_alias=True, exclude_none=True)
|
|
311
|
+
msg_parts = [
|
|
312
|
+
p.model_dump(by_alias=True, exclude_none=True) for p in message_with_parts.parts
|
|
313
|
+
]
|
|
314
|
+
|
|
315
|
+
# Sync message
|
|
316
|
+
msg_key = f"session/message/{session_id}/{msg_info['id']}"
|
|
317
|
+
resp = await self._client.post(
|
|
318
|
+
f"{self.api_url}/share_sync",
|
|
319
|
+
json={
|
|
320
|
+
"sessionID": session_id,
|
|
321
|
+
"secret": secret,
|
|
322
|
+
"key": msg_key,
|
|
323
|
+
"content": msg_info,
|
|
324
|
+
},
|
|
325
|
+
)
|
|
326
|
+
resp.raise_for_status()
|
|
327
|
+
|
|
328
|
+
# Sync parts
|
|
329
|
+
for part in msg_parts:
|
|
330
|
+
part_key = f"session/part/{session_id}/{msg_info['id']}/{part['id']}"
|
|
331
|
+
|
|
332
|
+
resp = await self._client.post(
|
|
333
|
+
f"{self.api_url}/share_sync",
|
|
334
|
+
json={
|
|
335
|
+
"sessionID": session_id,
|
|
336
|
+
"secret": secret,
|
|
337
|
+
"key": part_key,
|
|
338
|
+
"content": part,
|
|
339
|
+
},
|
|
340
|
+
)
|
|
341
|
+
resp.raise_for_status()
|
|
342
|
+
|
|
343
|
+
# Store secret in delete_url for later deletion
|
|
344
|
+
delete_url = f"{self.api_url}/share_delete#{secret}"
|
|
345
|
+
|
|
346
|
+
return ShareResult(
|
|
347
|
+
url=share_url,
|
|
348
|
+
raw_url=f"{self.api_url}/share_data?id={session_id[-8:]}",
|
|
349
|
+
delete_url=delete_url,
|
|
350
|
+
id=session_id,
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
except httpx.HTTPStatusError as e:
|
|
354
|
+
if e.response.status_code == 404: # noqa: PLR2004
|
|
355
|
+
msg = "OpenCode API endpoint not found - service may be unavailable"
|
|
356
|
+
raise RuntimeError(msg) from e
|
|
357
|
+
if e.response.status_code == 429: # noqa: PLR2004
|
|
358
|
+
msg = "Rate limited by OpenCode API"
|
|
359
|
+
raise RuntimeError(msg) from e
|
|
360
|
+
msg = f"OpenCode API error (HTTP {e.response.status_code}): {e.response.text}"
|
|
361
|
+
raise RuntimeError(msg) from e
|
|
362
|
+
except httpx.RequestError as e:
|
|
363
|
+
msg = f"Failed to connect to OpenCode API: {e}"
|
|
364
|
+
raise RuntimeError(msg) from e
|
|
365
|
+
|
|
366
|
+
async def share_conversation(
|
|
367
|
+
self,
|
|
368
|
+
conversation: MessageHistory,
|
|
369
|
+
*,
|
|
370
|
+
title: str | None = None,
|
|
371
|
+
visibility: Visibility = "unlisted",
|
|
372
|
+
expires_in: int | None = None,
|
|
373
|
+
num_messages: int | None = None,
|
|
374
|
+
) -> ShareResult:
|
|
375
|
+
"""Share conversation using OpenCode's native structured format.
|
|
376
|
+
|
|
377
|
+
Args:
|
|
378
|
+
conversation: Conversation object to share
|
|
379
|
+
title: Optional title for the conversation
|
|
380
|
+
visibility: Visibility level (ignored)
|
|
381
|
+
expires_in: Expiration time (ignored)
|
|
382
|
+
num_messages: Number of messages to include (None = all)
|
|
383
|
+
|
|
384
|
+
Returns:
|
|
385
|
+
ShareResult with OpenCode share URL
|
|
386
|
+
|
|
387
|
+
Note:
|
|
388
|
+
System prompts are stored as metadata on messages (UserMessage.system field
|
|
389
|
+
in OpenCode, ModelRequest.instructions in pydantic-ai), not as separate
|
|
390
|
+
"system" role messages. ChatMessage.role only supports "user" and "assistant".
|
|
391
|
+
"""
|
|
392
|
+
# Get messages to share
|
|
393
|
+
messages_to_share = list(
|
|
394
|
+
conversation.chat_messages[-num_messages:]
|
|
395
|
+
if num_messages
|
|
396
|
+
else conversation.chat_messages
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
return await self._share_chat_messages(
|
|
400
|
+
list(messages_to_share),
|
|
401
|
+
title=title,
|
|
402
|
+
visibility=visibility,
|
|
403
|
+
expires_in=expires_in,
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
async def delete_share(self, result: ShareResult) -> bool:
|
|
407
|
+
"""Delete a shared session.
|
|
408
|
+
|
|
409
|
+
Args:
|
|
410
|
+
result: The ShareResult containing session ID and secret
|
|
411
|
+
|
|
412
|
+
Returns:
|
|
413
|
+
True if deletion was successful
|
|
414
|
+
"""
|
|
415
|
+
if not result.delete_url or "#" not in result.delete_url:
|
|
416
|
+
return False
|
|
417
|
+
|
|
418
|
+
# Extract secret from delete_url
|
|
419
|
+
secret = result.delete_url.split("#", 1)[1]
|
|
420
|
+
|
|
421
|
+
try:
|
|
422
|
+
resp = await self._client.post(
|
|
423
|
+
f"{self.api_url}/share_delete",
|
|
424
|
+
json={
|
|
425
|
+
"sessionID": result.id,
|
|
426
|
+
"secret": secret,
|
|
427
|
+
},
|
|
428
|
+
)
|
|
429
|
+
resp.raise_for_status()
|
|
430
|
+
except httpx.HTTPError:
|
|
431
|
+
return False
|
|
432
|
+
else:
|
|
433
|
+
return True
|
|
434
|
+
|
|
435
|
+
async def close(self) -> None:
|
|
436
|
+
"""Close the HTTP client."""
|
|
437
|
+
await self._client.aclose()
|
|
438
|
+
|
|
439
|
+
async def __aenter__(self) -> Self:
|
|
440
|
+
return self
|
|
441
|
+
|
|
442
|
+
async def __aexit__(self, *args: object) -> None:
|
|
443
|
+
await self.close()
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
if __name__ == "__main__":
|
|
447
|
+
import asyncio
|
|
448
|
+
|
|
449
|
+
async def main() -> None:
|
|
450
|
+
"""Test OpenCode sharing functionality."""
|
|
451
|
+
async with OpenCodeSharer() as sharer:
|
|
452
|
+
# Test simple string sharing
|
|
453
|
+
print("=== Simple String Share ===")
|
|
454
|
+
result = await sharer.share(
|
|
455
|
+
"Hello, World!\n\nThis is a test of OpenCode sharing.",
|
|
456
|
+
title="Test Share",
|
|
457
|
+
)
|
|
458
|
+
print(f"Share URL: {result.url}")
|
|
459
|
+
print(f"Raw API URL: {result.raw_url}")
|
|
460
|
+
print(f"Session ID: {result.id}")
|
|
461
|
+
|
|
462
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""paste.rs text sharing provider."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from agentpool_commands.text_sharing.base import ShareResult, TextSharer
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from agentpool_commands.text_sharing.base import Visibility
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class PasteRsSharer(TextSharer):
|
|
15
|
+
"""Share text via paste.rs."""
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def name(self) -> str:
|
|
19
|
+
"""Name of the sharing service."""
|
|
20
|
+
return "paste.rs"
|
|
21
|
+
|
|
22
|
+
async def share(
|
|
23
|
+
self,
|
|
24
|
+
content: str,
|
|
25
|
+
*,
|
|
26
|
+
title: str | None = None,
|
|
27
|
+
syntax: str | None = None,
|
|
28
|
+
visibility: Visibility = "unlisted",
|
|
29
|
+
expires_in: int | None = None,
|
|
30
|
+
) -> ShareResult:
|
|
31
|
+
"""Share content on paste.rs.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
content: The text content to share
|
|
35
|
+
title: Ignored (paste.rs doesn't support titles)
|
|
36
|
+
syntax: Used as file extension for syntax highlighting
|
|
37
|
+
visibility: Ignored (all pastes are unlisted)
|
|
38
|
+
expires_in: Ignored (paste.rs doesn't support expiration)
|
|
39
|
+
"""
|
|
40
|
+
import anyenv
|
|
41
|
+
|
|
42
|
+
headers = {"Content-Type": "text/plain; charset=utf-8"}
|
|
43
|
+
data = content.encode()
|
|
44
|
+
response = await anyenv.request("POST", "https://paste.rs/", data=data, headers=headers)
|
|
45
|
+
url = (await response.text()).strip()
|
|
46
|
+
ext = syntax or "txt"
|
|
47
|
+
return ShareResult(url=f"{url}.{ext}", raw_url=url)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
if __name__ == "__main__":
|
|
51
|
+
import asyncio
|
|
52
|
+
|
|
53
|
+
async def main() -> None:
|
|
54
|
+
sharer = PasteRsSharer()
|
|
55
|
+
result = await sharer.share("# Test Paste\n\nHello from anyenv!", syntax="md")
|
|
56
|
+
print(f"URL: {result.url}")
|
|
57
|
+
print(f"Raw: {result.raw_url}")
|
|
58
|
+
|
|
59
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Pastebin.com text sharing provider."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
from agentpool_commands.text_sharing.base import ShareResult, TextSharer
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from agentpool_commands.text_sharing.base import Visibility
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# Pastebin expiry values
|
|
16
|
+
EXPIRY_MAP = {
|
|
17
|
+
None: "N", # Never
|
|
18
|
+
600: "10M", # 10 Minutes
|
|
19
|
+
3600: "1H", # 1 Hour
|
|
20
|
+
86400: "1D", # 1 Day
|
|
21
|
+
604800: "1W", # 1 Week
|
|
22
|
+
1209600: "2W", # 2 Weeks
|
|
23
|
+
2592000: "1M", # 1 Month
|
|
24
|
+
15552000: "6M", # 6 Months
|
|
25
|
+
31536000: "1Y", # 1 Year
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
# Map visibility to Pastebin's api_paste_private values
|
|
29
|
+
VISIBILITY_MAP = {"public": "0", "unlisted": "1", "private": "2"}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _get_expiry_code(seconds: int | None) -> str:
|
|
33
|
+
"""Convert seconds to Pastebin expiry code, rounding to nearest valid value."""
|
|
34
|
+
if seconds is None:
|
|
35
|
+
return "N"
|
|
36
|
+
|
|
37
|
+
valid_seconds = sorted(k for k in EXPIRY_MAP if k is not None)
|
|
38
|
+
closest = min(valid_seconds, key=lambda x: abs(x - seconds))
|
|
39
|
+
return EXPIRY_MAP[closest]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class PastebinSharer(TextSharer):
|
|
43
|
+
"""Share text via Pastebin.com."""
|
|
44
|
+
|
|
45
|
+
def __init__(self, api_key: str | None = None) -> None:
|
|
46
|
+
"""Initialize the Pastebin sharer.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
api_key: Pastebin API developer key. If not provided,
|
|
50
|
+
reads from PASTEBIN_API_KEY environment variable.
|
|
51
|
+
"""
|
|
52
|
+
self._api_key = api_key or os.environ.get("PASTEBIN_API_KEY")
|
|
53
|
+
if not self._api_key:
|
|
54
|
+
msg = "Pastebin API key required. Set PASTEBIN_API_KEY or pass api_key parameter."
|
|
55
|
+
raise ValueError(msg)
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def name(self) -> str:
|
|
59
|
+
"""Name of the sharing service."""
|
|
60
|
+
return "Pastebin"
|
|
61
|
+
|
|
62
|
+
async def share(
|
|
63
|
+
self,
|
|
64
|
+
content: str,
|
|
65
|
+
*,
|
|
66
|
+
title: str | None = None,
|
|
67
|
+
syntax: str | None = None,
|
|
68
|
+
visibility: Visibility = "unlisted",
|
|
69
|
+
expires_in: int | None = None,
|
|
70
|
+
) -> ShareResult:
|
|
71
|
+
"""Share content on Pastebin.com.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
content: The text content to share
|
|
75
|
+
title: Title/name of the paste
|
|
76
|
+
syntax: Syntax highlighting (e.g. "python", "markdown", "javascript")
|
|
77
|
+
visibility: "public", "unlisted", or "private"
|
|
78
|
+
expires_in: Expiration time in seconds (rounded to nearest valid value)
|
|
79
|
+
"""
|
|
80
|
+
import anyenv
|
|
81
|
+
|
|
82
|
+
data: dict[str, Any] = {
|
|
83
|
+
"api_dev_key": self._api_key,
|
|
84
|
+
"api_option": "paste",
|
|
85
|
+
"api_paste_code": content,
|
|
86
|
+
"api_paste_private": VISIBILITY_MAP.get(visibility, "1"),
|
|
87
|
+
"api_paste_expire_date": _get_expiry_code(expires_in),
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if title:
|
|
91
|
+
data["api_paste_name"] = title
|
|
92
|
+
|
|
93
|
+
if syntax:
|
|
94
|
+
data["api_paste_format"] = syntax
|
|
95
|
+
|
|
96
|
+
response = await anyenv.post("https://pastebin.com/api/api_post.php", data=data)
|
|
97
|
+
url = (await response.text()).strip()
|
|
98
|
+
if url.startswith("Bad API request"):
|
|
99
|
+
msg = f"Pastebin API error: {url}"
|
|
100
|
+
raise RuntimeError(msg)
|
|
101
|
+
paste_key = url.split("/")[-1]
|
|
102
|
+
raw_url = f"https://pastebin.com/raw/{paste_key}"
|
|
103
|
+
return ShareResult(url=url, raw_url=raw_url, id=paste_key)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
if __name__ == "__main__":
|
|
107
|
+
import asyncio
|
|
108
|
+
|
|
109
|
+
async def main() -> None:
|
|
110
|
+
sharer = PastebinSharer()
|
|
111
|
+
result = await sharer.share("# Test Paste\n\nHello from anyenv!", title="test.md")
|
|
112
|
+
print(f"URL: {result.url}")
|
|
113
|
+
print(f"Raw: {result.raw_url}")
|
|
114
|
+
print(f"ID: {result.id}")
|
|
115
|
+
|
|
116
|
+
asyncio.run(main())
|