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
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Permission routes for OpenCode TUI compatibility."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, HTTPException
|
|
6
|
+
|
|
7
|
+
from agentpool_server.opencode_server.dependencies import StateDep
|
|
8
|
+
from agentpool_server.opencode_server.models.events import PermissionResolvedEvent
|
|
9
|
+
from agentpool_server.opencode_server.routes.session_routes import PermissionResponse
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
router = APIRouter(prefix="/permission", tags=["permission"])
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@router.post("/{permission_id}/reply")
|
|
16
|
+
async def reply_to_permission(
|
|
17
|
+
permission_id: str,
|
|
18
|
+
body: PermissionResponse,
|
|
19
|
+
state: StateDep,
|
|
20
|
+
) -> bool:
|
|
21
|
+
"""Respond to a pending permission request (OpenCode TUI compatibility).
|
|
22
|
+
|
|
23
|
+
This endpoint handles the OpenCode TUI's expected format:
|
|
24
|
+
POST /permission/{permission_id}/reply
|
|
25
|
+
|
|
26
|
+
The response can be:
|
|
27
|
+
- "once": Allow this tool execution once
|
|
28
|
+
- "always": Always allow this tool (remembered for session)
|
|
29
|
+
- "reject": Reject this tool execution
|
|
30
|
+
"""
|
|
31
|
+
print(f"DEBUG permission endpoint: received reply '{body.reply}' for perm_id={permission_id}")
|
|
32
|
+
print(f"DEBUG permission endpoint: searching in {len(state.input_providers)} sessions")
|
|
33
|
+
# Find which session has this permission request
|
|
34
|
+
for session_id, input_provider in state.input_providers.items():
|
|
35
|
+
pending_perms = list(input_provider._pending_permissions.keys())
|
|
36
|
+
print(
|
|
37
|
+
f"DEBUG permission endpoint: session {session_id} has "
|
|
38
|
+
f"{len(pending_perms)} pending: {pending_perms}"
|
|
39
|
+
)
|
|
40
|
+
# Check if this permission belongs to this session
|
|
41
|
+
if permission_id in input_provider._pending_permissions:
|
|
42
|
+
print(f"DEBUG permission endpoint: found permission in session {session_id}")
|
|
43
|
+
# Resolve the permission
|
|
44
|
+
resolved = input_provider.resolve_permission(permission_id, body.reply)
|
|
45
|
+
print(f"DEBUG permission endpoint: resolve_permission returned {resolved}")
|
|
46
|
+
if not resolved:
|
|
47
|
+
raise HTTPException(
|
|
48
|
+
status_code=404,
|
|
49
|
+
detail="Permission not found or already resolved",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
await state.broadcast_event(
|
|
53
|
+
PermissionResolvedEvent.create(
|
|
54
|
+
session_id=session_id,
|
|
55
|
+
request_id=permission_id,
|
|
56
|
+
reply=body.reply,
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
return True
|
|
61
|
+
|
|
62
|
+
# Permission not found in any session
|
|
63
|
+
raise HTTPException(status_code=404, detail="Permission not found")
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
"""PTY (Pseudo-Terminal) routes.
|
|
2
|
+
|
|
3
|
+
Uses the agent's execution environment PTY manager for terminal sessions.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import contextlib
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import TYPE_CHECKING, Any
|
|
12
|
+
|
|
13
|
+
from fastapi import APIRouter, HTTPException, WebSocket, WebSocketDisconnect # noqa: TC002
|
|
14
|
+
|
|
15
|
+
from agentpool_server.opencode_server.dependencies import StateDep
|
|
16
|
+
from agentpool_server.opencode_server.models import PtyCreateRequest, PtyInfo, PtyUpdateRequest
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from exxec.pty_manager import PtyManagerProtocol
|
|
21
|
+
|
|
22
|
+
from agentpool_server.opencode_server.state import ServerState
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
router = APIRouter(prefix="/pty", tags=["pty"])
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class PtySession:
|
|
30
|
+
"""Active PTY session with WebSocket subscribers."""
|
|
31
|
+
|
|
32
|
+
pty_id: str
|
|
33
|
+
subscribers: set[WebSocket] = field(default_factory=set)
|
|
34
|
+
read_task: asyncio.Task[Any] | None = None
|
|
35
|
+
buffer: str = ""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# Track WebSocket subscribers per PTY session
|
|
39
|
+
_pty_sessions: dict[str, PtySession] = {}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _get_pty_manager(state: StateDep) -> PtyManagerProtocol:
|
|
43
|
+
"""Get PTY manager from agent's execution environment.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
state: Server state with agent
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
PTY manager from the agent's execution environment
|
|
50
|
+
|
|
51
|
+
Raises:
|
|
52
|
+
HTTPException: If PTY is not supported
|
|
53
|
+
"""
|
|
54
|
+
try:
|
|
55
|
+
return state.agent.env.get_pty_manager()
|
|
56
|
+
except NotImplementedError as e:
|
|
57
|
+
raise HTTPException(
|
|
58
|
+
status_code=501, detail="PTY not supported by this execution environment"
|
|
59
|
+
) from e
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _convert_pty_info(info: Any, title: str | None = None) -> PtyInfo:
|
|
63
|
+
"""Convert exxec PtyInfo to OpenCode PtyInfo model.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
info: PtyInfo from exxec
|
|
67
|
+
title: Optional title override
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
OpenCode PtyInfo model
|
|
71
|
+
"""
|
|
72
|
+
return PtyInfo(
|
|
73
|
+
id=info.id,
|
|
74
|
+
title=title or f"Terminal {info.id[-4:]}",
|
|
75
|
+
command=info.command,
|
|
76
|
+
args=info.args,
|
|
77
|
+
cwd=info.cwd or "",
|
|
78
|
+
status=info.status,
|
|
79
|
+
pid=info.pid,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@router.get("")
|
|
84
|
+
async def list_ptys(state: StateDep) -> list[PtyInfo]:
|
|
85
|
+
"""List all PTY sessions."""
|
|
86
|
+
manager = _get_pty_manager(state)
|
|
87
|
+
sessions = await manager.list_sessions()
|
|
88
|
+
return [_convert_pty_info(s) for s in sessions]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@router.post("")
|
|
92
|
+
async def create_pty(request: PtyCreateRequest, state: StateDep) -> PtyInfo:
|
|
93
|
+
"""Create a new PTY session."""
|
|
94
|
+
from agentpool_server.opencode_server.models.events import PtyCreatedEvent
|
|
95
|
+
|
|
96
|
+
manager = _get_pty_manager(state)
|
|
97
|
+
# Limit number of PTY sessions to prevent resource exhaustion
|
|
98
|
+
sessions = await manager.list_sessions()
|
|
99
|
+
if len(sessions) >= 20: # Max 20 concurrent PTY sessions # noqa: PLR2004
|
|
100
|
+
detail = f"Too many PTY sessions ({len(sessions)}). Close some terminals first."
|
|
101
|
+
raise HTTPException(status_code=429, detail=detail)
|
|
102
|
+
|
|
103
|
+
# Use working dir from state if not specified
|
|
104
|
+
cwd = request.cwd or state.working_dir
|
|
105
|
+
print(f"Creating PTY: command={request.command}, args={request.args}, cwd={cwd}")
|
|
106
|
+
try:
|
|
107
|
+
info = await manager.create(
|
|
108
|
+
command=request.command,
|
|
109
|
+
args=request.args,
|
|
110
|
+
cwd=cwd,
|
|
111
|
+
env=request.env,
|
|
112
|
+
)
|
|
113
|
+
print(f"PTY created successfully: {info.id}, status={info.status}")
|
|
114
|
+
except Exception as e:
|
|
115
|
+
raise HTTPException(status_code=400, detail=f"Failed to create PTY: {e}") from e
|
|
116
|
+
|
|
117
|
+
pty_id = info.id
|
|
118
|
+
title = request.title or f"Terminal {pty_id[-4:]}"
|
|
119
|
+
# Create session tracker for WebSocket subscribers
|
|
120
|
+
session = PtySession(pty_id=pty_id)
|
|
121
|
+
_pty_sessions[pty_id] = session
|
|
122
|
+
print(f"PTY session registered: {pty_id}, total sessions: {len(_pty_sessions)}")
|
|
123
|
+
# Start background task to read output and distribute to subscribers
|
|
124
|
+
session.read_task = asyncio.create_task(_read_pty_output(manager, pty_id, state))
|
|
125
|
+
pty_info = _convert_pty_info(info, title=title)
|
|
126
|
+
# Broadcast PTY created event
|
|
127
|
+
event = PtyCreatedEvent.create(info=pty_info.model_dump(by_alias=True))
|
|
128
|
+
await state.broadcast_event(event)
|
|
129
|
+
return pty_info
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
async def _read_pty_output(manager: PtyManagerProtocol, pty_id: str, state: ServerState) -> None:
|
|
133
|
+
"""Background task to read PTY output and distribute to subscribers."""
|
|
134
|
+
from agentpool_server.opencode_server.models.events import PtyExitedEvent
|
|
135
|
+
|
|
136
|
+
session = _pty_sessions.get(pty_id)
|
|
137
|
+
if not session:
|
|
138
|
+
return
|
|
139
|
+
|
|
140
|
+
exit_code = 0
|
|
141
|
+
try:
|
|
142
|
+
async for data in manager.stream(pty_id):
|
|
143
|
+
decoded = data.decode("utf-8", errors="replace")
|
|
144
|
+
|
|
145
|
+
if session.subscribers:
|
|
146
|
+
# Send to all connected WebSocket clients
|
|
147
|
+
disconnected: set[WebSocket] = set()
|
|
148
|
+
for ws in session.subscribers:
|
|
149
|
+
try:
|
|
150
|
+
await ws.send_text(decoded)
|
|
151
|
+
except Exception: # noqa: BLE001
|
|
152
|
+
disconnected.add(ws)
|
|
153
|
+
session.subscribers -= disconnected
|
|
154
|
+
else:
|
|
155
|
+
# Buffer output if no subscribers
|
|
156
|
+
session.buffer += decoded
|
|
157
|
+
# Limit buffer size
|
|
158
|
+
if len(session.buffer) > 100000: # noqa: PLR2004
|
|
159
|
+
session.buffer = session.buffer[-50000:]
|
|
160
|
+
|
|
161
|
+
except asyncio.CancelledError:
|
|
162
|
+
return # Don't broadcast exit if cancelled
|
|
163
|
+
except Exception: # noqa: BLE001
|
|
164
|
+
exit_code = -1
|
|
165
|
+
|
|
166
|
+
# Stream ended - process exited, broadcast event
|
|
167
|
+
event = PtyExitedEvent.create(pty_id=pty_id, exit_code=exit_code)
|
|
168
|
+
await state.broadcast_event(event)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@router.get("/{pty_id}")
|
|
172
|
+
async def get_pty(pty_id: str, state: StateDep) -> PtyInfo:
|
|
173
|
+
"""Get PTY session details."""
|
|
174
|
+
manager = _get_pty_manager(state)
|
|
175
|
+
info = await manager.get_info(pty_id)
|
|
176
|
+
if not info:
|
|
177
|
+
raise HTTPException(status_code=404, detail="PTY session not found")
|
|
178
|
+
return _convert_pty_info(info)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@router.put("/{pty_id}")
|
|
182
|
+
@router.patch("/{pty_id}")
|
|
183
|
+
async def update_pty(pty_id: str, request: PtyUpdateRequest, state: StateDep) -> PtyInfo:
|
|
184
|
+
"""Update PTY session (title, resize)."""
|
|
185
|
+
from exxec.pty_manager import PtySize
|
|
186
|
+
|
|
187
|
+
from agentpool_server.opencode_server.models.events import PtyUpdatedEvent
|
|
188
|
+
|
|
189
|
+
manager = _get_pty_manager(state)
|
|
190
|
+
info = await manager.get_info(pty_id)
|
|
191
|
+
if not info:
|
|
192
|
+
raise HTTPException(status_code=404, detail="PTY session not found")
|
|
193
|
+
|
|
194
|
+
# Handle resize if requested
|
|
195
|
+
if request.size:
|
|
196
|
+
await manager.resize(pty_id, PtySize(rows=request.size.rows, cols=request.size.cols))
|
|
197
|
+
# Refresh info after resize
|
|
198
|
+
info = await manager.get_info(pty_id)
|
|
199
|
+
if not info:
|
|
200
|
+
raise HTTPException(status_code=404, detail="PTY session not found after resize")
|
|
201
|
+
|
|
202
|
+
# Title is handled at the API level, not in the PTY manager
|
|
203
|
+
title = request.title if request.title else f"Terminal {pty_id[-4:]}"
|
|
204
|
+
pty_info = _convert_pty_info(info, title=title)
|
|
205
|
+
# Broadcast PTY updated event
|
|
206
|
+
event = PtyUpdatedEvent.create(info=pty_info.model_dump(by_alias=True))
|
|
207
|
+
await state.broadcast_event(event)
|
|
208
|
+
return pty_info
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@router.delete("/{pty_id}")
|
|
212
|
+
async def remove_pty(pty_id: str, state: StateDep) -> dict[str, bool]:
|
|
213
|
+
"""Remove/kill PTY session."""
|
|
214
|
+
from agentpool_server.opencode_server.models.events import PtyDeletedEvent
|
|
215
|
+
|
|
216
|
+
manager = _get_pty_manager(state)
|
|
217
|
+
# Kill the PTY session
|
|
218
|
+
success = await manager.kill(pty_id)
|
|
219
|
+
if not success:
|
|
220
|
+
raise HTTPException(status_code=404, detail="PTY session not found")
|
|
221
|
+
# Cleanup session tracker
|
|
222
|
+
session = _pty_sessions.pop(pty_id, None)
|
|
223
|
+
if session:
|
|
224
|
+
# Cancel read task
|
|
225
|
+
if session.read_task and not session.read_task.done():
|
|
226
|
+
session.read_task.cancel()
|
|
227
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
228
|
+
await session.read_task
|
|
229
|
+
|
|
230
|
+
# Close all WebSocket connections
|
|
231
|
+
for ws in session.subscribers:
|
|
232
|
+
with contextlib.suppress(Exception):
|
|
233
|
+
await ws.close()
|
|
234
|
+
|
|
235
|
+
# Broadcast PTY deleted event
|
|
236
|
+
event = PtyDeletedEvent.create(pty_id=pty_id)
|
|
237
|
+
await state.broadcast_event(event)
|
|
238
|
+
|
|
239
|
+
return {"success": True}
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
@router.websocket("/{pty_id}/connect")
|
|
243
|
+
async def connect_pty(websocket: WebSocket, pty_id: str) -> None:
|
|
244
|
+
"""Connect to PTY via WebSocket for interactive terminal."""
|
|
245
|
+
# Get state from websocket's app
|
|
246
|
+
|
|
247
|
+
state: ServerState = websocket.app.state.server_state
|
|
248
|
+
try:
|
|
249
|
+
manager = _get_pty_manager(state)
|
|
250
|
+
except HTTPException:
|
|
251
|
+
# Must accept before we can close
|
|
252
|
+
await websocket.accept()
|
|
253
|
+
await websocket.close(code=1003, reason="PTY not supported")
|
|
254
|
+
return
|
|
255
|
+
except Exception as e: # noqa: BLE001
|
|
256
|
+
await websocket.accept()
|
|
257
|
+
await websocket.close(code=1011, reason=f"Error: {e}")
|
|
258
|
+
return
|
|
259
|
+
|
|
260
|
+
# Check if PTY exists - if not, immediately reject like OpenCode does
|
|
261
|
+
info = await manager.get_info(pty_id)
|
|
262
|
+
if not info:
|
|
263
|
+
await websocket.accept()
|
|
264
|
+
await websocket.close(code=1003, reason="PTY session not found")
|
|
265
|
+
return
|
|
266
|
+
|
|
267
|
+
# PTY exists, accept the WebSocket connection
|
|
268
|
+
await websocket.accept()
|
|
269
|
+
# Get or create session tracker
|
|
270
|
+
if pty_id not in _pty_sessions:
|
|
271
|
+
_pty_sessions[pty_id] = PtySession(pty_id=pty_id)
|
|
272
|
+
session = _pty_sessions[pty_id]
|
|
273
|
+
session.subscribers.add(websocket)
|
|
274
|
+
# Send buffered output
|
|
275
|
+
if session.buffer:
|
|
276
|
+
try:
|
|
277
|
+
await websocket.send_text(session.buffer)
|
|
278
|
+
session.buffer = ""
|
|
279
|
+
except Exception: # noqa: BLE001
|
|
280
|
+
pass
|
|
281
|
+
|
|
282
|
+
try:
|
|
283
|
+
while True:
|
|
284
|
+
# Receive input from client
|
|
285
|
+
data = await websocket.receive_text()
|
|
286
|
+
# Write to PTY stdin
|
|
287
|
+
info = await manager.get_info(pty_id)
|
|
288
|
+
if info and info.status == "running":
|
|
289
|
+
try:
|
|
290
|
+
await manager.write(pty_id, data.encode())
|
|
291
|
+
except Exception: # noqa: BLE001
|
|
292
|
+
break
|
|
293
|
+
else:
|
|
294
|
+
break
|
|
295
|
+
except WebSocketDisconnect:
|
|
296
|
+
pass
|
|
297
|
+
except Exception: # noqa: BLE001
|
|
298
|
+
pass
|
|
299
|
+
finally:
|
|
300
|
+
session.subscribers.discard(websocket)
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Question routes for OpenCode compatibility."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, HTTPException
|
|
6
|
+
|
|
7
|
+
from agentpool_server.opencode_server.dependencies import StateDep
|
|
8
|
+
from agentpool_server.opencode_server.input_provider import OpenCodeInputProvider
|
|
9
|
+
from agentpool_server.opencode_server.models.events import (
|
|
10
|
+
QuestionRejectedEvent,
|
|
11
|
+
QuestionRepliedEvent,
|
|
12
|
+
)
|
|
13
|
+
from agentpool_server.opencode_server.models.question import QuestionReply, QuestionRequest
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
router = APIRouter(prefix="/question", tags=["question"])
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@router.get("/", response_model=list[QuestionRequest])
|
|
20
|
+
async def list_questions(state: StateDep) -> list[QuestionRequest]:
|
|
21
|
+
"""List all pending question requests.
|
|
22
|
+
|
|
23
|
+
Returns a list of all pending questions awaiting user response.
|
|
24
|
+
"""
|
|
25
|
+
questions = []
|
|
26
|
+
for question_id, pending in state.pending_questions.items():
|
|
27
|
+
questions.append(
|
|
28
|
+
QuestionRequest(
|
|
29
|
+
id=question_id,
|
|
30
|
+
session_id=pending.session_id,
|
|
31
|
+
questions=pending.questions,
|
|
32
|
+
tool=pending.tool,
|
|
33
|
+
)
|
|
34
|
+
)
|
|
35
|
+
return questions
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@router.post("/{requestID}/reply")
|
|
39
|
+
async def reply_to_question(
|
|
40
|
+
requestID: str, # noqa: N803
|
|
41
|
+
reply: QuestionReply,
|
|
42
|
+
state: StateDep,
|
|
43
|
+
) -> bool:
|
|
44
|
+
"""Reply to a question request.
|
|
45
|
+
|
|
46
|
+
The user provides answers to the questions. Answers must be provided
|
|
47
|
+
as an array of arrays, where each inner array contains the selected
|
|
48
|
+
label(s) for that question.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
requestID: The question request ID
|
|
52
|
+
reply: The user's answers
|
|
53
|
+
state: Server state
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
True if the question was resolved successfully
|
|
57
|
+
|
|
58
|
+
Raises:
|
|
59
|
+
HTTPException: If question not found or invalid provider
|
|
60
|
+
"""
|
|
61
|
+
pending = state.pending_questions.get(requestID)
|
|
62
|
+
if not pending:
|
|
63
|
+
raise HTTPException(status_code=404, detail="Question request not found")
|
|
64
|
+
|
|
65
|
+
session_id = pending.session_id
|
|
66
|
+
provider = state.input_providers.get(session_id)
|
|
67
|
+
|
|
68
|
+
if not isinstance(provider, OpenCodeInputProvider):
|
|
69
|
+
raise HTTPException(status_code=500, detail="Invalid provider for session")
|
|
70
|
+
|
|
71
|
+
# Resolve via provider
|
|
72
|
+
success = provider.resolve_question(requestID, reply.answers)
|
|
73
|
+
|
|
74
|
+
if not success:
|
|
75
|
+
raise HTTPException(status_code=404, detail="Question already resolved")
|
|
76
|
+
|
|
77
|
+
# Broadcast replied event
|
|
78
|
+
event = QuestionRepliedEvent.create(
|
|
79
|
+
session_id=session_id,
|
|
80
|
+
request_id=requestID,
|
|
81
|
+
answers=reply.answers,
|
|
82
|
+
)
|
|
83
|
+
await state.broadcast_event(event)
|
|
84
|
+
|
|
85
|
+
return True
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@router.post("/{requestID}/reject")
|
|
89
|
+
async def reject_question(
|
|
90
|
+
requestID: str, # noqa: N803
|
|
91
|
+
state: StateDep,
|
|
92
|
+
) -> bool:
|
|
93
|
+
"""Reject a question request.
|
|
94
|
+
|
|
95
|
+
Called when the user dismisses the question without providing an answer.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
requestID: The question request ID
|
|
99
|
+
state: Server state
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
True if the question was rejected successfully
|
|
103
|
+
|
|
104
|
+
Raises:
|
|
105
|
+
HTTPException: If question not found
|
|
106
|
+
"""
|
|
107
|
+
pending = state.pending_questions.get(requestID)
|
|
108
|
+
if not pending:
|
|
109
|
+
raise HTTPException(status_code=404, detail="Question request not found")
|
|
110
|
+
|
|
111
|
+
session_id = pending.session_id
|
|
112
|
+
future = pending.future
|
|
113
|
+
|
|
114
|
+
# Cancel the future
|
|
115
|
+
if not future.done():
|
|
116
|
+
future.cancel()
|
|
117
|
+
|
|
118
|
+
# Remove from pending
|
|
119
|
+
del state.pending_questions[requestID]
|
|
120
|
+
|
|
121
|
+
# Broadcast rejected event
|
|
122
|
+
event = QuestionRejectedEvent.create(
|
|
123
|
+
session_id=session_id,
|
|
124
|
+
request_id=requestID,
|
|
125
|
+
)
|
|
126
|
+
await state.broadcast_event(event)
|
|
127
|
+
|
|
128
|
+
return True
|