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,139 @@
|
|
|
1
|
+
"""TUI routes for external control of the TUI.
|
|
2
|
+
|
|
3
|
+
These endpoints allow external integrations (e.g., VSCode extension) to control
|
|
4
|
+
the TUI by broadcasting events that the TUI listens for via SSE.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Literal
|
|
10
|
+
|
|
11
|
+
from fastapi import APIRouter
|
|
12
|
+
from pydantic import BaseModel, Field
|
|
13
|
+
|
|
14
|
+
from agentpool_server.opencode_server.dependencies import StateDep
|
|
15
|
+
from agentpool_server.opencode_server.models.events import (
|
|
16
|
+
TuiCommandExecuteEvent,
|
|
17
|
+
TuiPromptAppendEvent,
|
|
18
|
+
TuiToastShowEvent,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
router = APIRouter(prefix="/tui", tags=["tui"])
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AppendPromptRequest(BaseModel):
|
|
26
|
+
"""Request body for appending text to the prompt."""
|
|
27
|
+
|
|
28
|
+
text: str = Field(..., description="Text to append to the prompt")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ExecuteCommandRequest(BaseModel):
|
|
32
|
+
"""Request body for executing a TUI command."""
|
|
33
|
+
|
|
34
|
+
command: str = Field(
|
|
35
|
+
...,
|
|
36
|
+
description="Command to execute (e.g., 'prompt.submit', 'prompt.clear', 'session.new')",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ShowToastRequest(BaseModel):
|
|
41
|
+
"""Request body for showing a toast notification."""
|
|
42
|
+
|
|
43
|
+
title: str | None = Field(None, description="Optional toast title")
|
|
44
|
+
message: str = Field(..., description="Toast message")
|
|
45
|
+
variant: Literal["info", "success", "warning", "error"] = Field(
|
|
46
|
+
"info", description="Toast variant"
|
|
47
|
+
)
|
|
48
|
+
duration: int = Field(5000, description="Duration in milliseconds")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@router.post("/append-prompt")
|
|
52
|
+
async def append_prompt(request: AppendPromptRequest, state: StateDep) -> bool:
|
|
53
|
+
"""Append text to the TUI prompt.
|
|
54
|
+
|
|
55
|
+
Used by external integrations (e.g., VSCode) to insert text like file
|
|
56
|
+
references into the prompt input.
|
|
57
|
+
"""
|
|
58
|
+
await state.broadcast_event(TuiPromptAppendEvent.create(request.text))
|
|
59
|
+
return True
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@router.post("/submit-prompt")
|
|
63
|
+
async def submit_prompt(state: StateDep) -> bool:
|
|
64
|
+
"""Submit the current prompt.
|
|
65
|
+
|
|
66
|
+
Triggers the TUI to submit whatever is currently in the prompt input.
|
|
67
|
+
"""
|
|
68
|
+
await state.broadcast_event(TuiCommandExecuteEvent.create("prompt.submit"))
|
|
69
|
+
return True
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@router.post("/clear-prompt")
|
|
73
|
+
async def clear_prompt(state: StateDep) -> bool:
|
|
74
|
+
"""Clear the TUI prompt.
|
|
75
|
+
|
|
76
|
+
Clears any text currently in the prompt input.
|
|
77
|
+
"""
|
|
78
|
+
await state.broadcast_event(TuiCommandExecuteEvent.create("prompt.clear"))
|
|
79
|
+
return True
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@router.post("/execute-command")
|
|
83
|
+
async def execute_command(request: ExecuteCommandRequest, state: StateDep) -> bool:
|
|
84
|
+
"""Execute a TUI command.
|
|
85
|
+
|
|
86
|
+
Available commands:
|
|
87
|
+
- session.list, session.new, session.share, session.interrupt, session.compact
|
|
88
|
+
- session.page.up, session.page.down, session.half.page.up, session.half.page.down
|
|
89
|
+
- session.first, session.last
|
|
90
|
+
- prompt.clear, prompt.submit
|
|
91
|
+
- agent.cycle
|
|
92
|
+
"""
|
|
93
|
+
await state.broadcast_event(TuiCommandExecuteEvent.create(request.command))
|
|
94
|
+
return True
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@router.post("/show-toast")
|
|
98
|
+
async def show_toast(request: ShowToastRequest, state: StateDep) -> bool:
|
|
99
|
+
"""Show a toast notification in the TUI."""
|
|
100
|
+
await state.broadcast_event(
|
|
101
|
+
TuiToastShowEvent.create(
|
|
102
|
+
message=request.message,
|
|
103
|
+
variant=request.variant,
|
|
104
|
+
title=request.title,
|
|
105
|
+
duration=request.duration,
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
return True
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# Additional convenience endpoints matching OpenCode's API
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@router.post("/open-help")
|
|
115
|
+
async def open_help(state: StateDep) -> bool:
|
|
116
|
+
"""Open the help dialog."""
|
|
117
|
+
await state.broadcast_event(TuiCommandExecuteEvent.create("help.open"))
|
|
118
|
+
return True
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@router.post("/open-sessions")
|
|
122
|
+
async def open_sessions(state: StateDep) -> bool:
|
|
123
|
+
"""Open the session selector."""
|
|
124
|
+
await state.broadcast_event(TuiCommandExecuteEvent.create("session.list"))
|
|
125
|
+
return True
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@router.post("/open-themes")
|
|
129
|
+
async def open_themes(state: StateDep) -> bool:
|
|
130
|
+
"""Open the theme selector."""
|
|
131
|
+
await state.broadcast_event(TuiCommandExecuteEvent.create("theme.list"))
|
|
132
|
+
return True
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@router.post("/open-models")
|
|
136
|
+
async def open_models(state: StateDep) -> bool:
|
|
137
|
+
"""Open the model selector."""
|
|
138
|
+
await state.broadcast_event(TuiCommandExecuteEvent.create("model.list"))
|
|
139
|
+
return True
|
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
"""OpenCode-compatible FastAPI server.
|
|
2
|
+
|
|
3
|
+
This server implements the OpenCode API endpoints to allow OpenCode SDK clients
|
|
4
|
+
to interact with AgentPool agents.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from contextlib import asynccontextmanager
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import TYPE_CHECKING, Any
|
|
12
|
+
|
|
13
|
+
from fastapi import FastAPI, Request # noqa: TC002
|
|
14
|
+
from fastapi.exceptions import RequestValidationError
|
|
15
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
16
|
+
from fastapi.responses import JSONResponse, RedirectResponse, Response
|
|
17
|
+
|
|
18
|
+
from agentpool import AgentPool
|
|
19
|
+
from agentpool_server.opencode_server.routes import (
|
|
20
|
+
agent_router,
|
|
21
|
+
app_router,
|
|
22
|
+
config_router,
|
|
23
|
+
file_router,
|
|
24
|
+
global_router,
|
|
25
|
+
lsp_router,
|
|
26
|
+
message_router,
|
|
27
|
+
permission_router,
|
|
28
|
+
pty_router,
|
|
29
|
+
question_router,
|
|
30
|
+
session_router,
|
|
31
|
+
tui_router,
|
|
32
|
+
)
|
|
33
|
+
from agentpool_server.opencode_server.state import ServerState
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class OpenCodeJSONResponse(JSONResponse):
|
|
37
|
+
"""Custom JSON response that excludes None values (like OpenCode does)."""
|
|
38
|
+
|
|
39
|
+
def render(self, content: Any) -> bytes:
|
|
40
|
+
from fastapi.encoders import jsonable_encoder
|
|
41
|
+
|
|
42
|
+
return super().render(jsonable_encoder(content, exclude_none=True))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
if TYPE_CHECKING:
|
|
46
|
+
from collections.abc import AsyncIterator, Set as AbstractSet
|
|
47
|
+
|
|
48
|
+
from agentpool.storage.manager import TitleGeneratedEvent
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
VERSION = "0.1.0"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
async def check_pypi_version(package: str = "agentpool") -> str | None:
|
|
55
|
+
"""Check PyPI for the latest version of a package.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
package: Package name to check
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Latest version string, or None if check fails
|
|
62
|
+
"""
|
|
63
|
+
import httpx
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
async with httpx.AsyncClient(timeout=5.0) as client:
|
|
67
|
+
response = await client.get(f"https://pypi.org/pypi/{package}/json")
|
|
68
|
+
if response.status_code == 200: # noqa: PLR2004
|
|
69
|
+
data: dict[str, Any] = response.json()
|
|
70
|
+
info: dict[str, Any] = data.get("info", {})
|
|
71
|
+
version: str | None = info.get("version")
|
|
72
|
+
return version
|
|
73
|
+
except Exception: # noqa: BLE001
|
|
74
|
+
pass
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def compare_versions(current: str, latest: str) -> bool:
|
|
79
|
+
"""Check if latest version is newer than current.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
current: Current version string
|
|
83
|
+
latest: Latest version string
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
True if latest is newer than current
|
|
87
|
+
"""
|
|
88
|
+
from packaging.version import Version
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
return Version(latest) > Version(current)
|
|
92
|
+
except Exception: # noqa: BLE001
|
|
93
|
+
return False
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def create_app( # noqa: PLR0915
|
|
97
|
+
*,
|
|
98
|
+
pool: AgentPool[Any],
|
|
99
|
+
agent_name: str | None = None,
|
|
100
|
+
working_dir: str | None = None,
|
|
101
|
+
) -> FastAPI:
|
|
102
|
+
"""Create the FastAPI application.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
pool: AgentPool for session persistence and agent access.
|
|
106
|
+
agent_name: Name of the agent to use for handling messages.
|
|
107
|
+
If None, uses the first agent in the pool.
|
|
108
|
+
working_dir: Working directory for file operations. Defaults to cwd.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Configured FastAPI application.
|
|
112
|
+
|
|
113
|
+
Raises:
|
|
114
|
+
ValueError: If specified agent_name not found or pool has no agents.
|
|
115
|
+
"""
|
|
116
|
+
# Resolve the agent from the pool
|
|
117
|
+
import logfire
|
|
118
|
+
|
|
119
|
+
if agent_name:
|
|
120
|
+
agent = pool.all_agents.get(agent_name)
|
|
121
|
+
if agent is None:
|
|
122
|
+
msg = f"Agent '{agent_name}' not found in pool"
|
|
123
|
+
raise ValueError(msg)
|
|
124
|
+
else:
|
|
125
|
+
# Use first agent as default
|
|
126
|
+
agent = next(iter(pool.all_agents.values()), None)
|
|
127
|
+
if agent is None:
|
|
128
|
+
msg = "Pool has no agents"
|
|
129
|
+
raise ValueError(msg)
|
|
130
|
+
|
|
131
|
+
state = ServerState(
|
|
132
|
+
working_dir=working_dir or str(Path.cwd()),
|
|
133
|
+
pool=pool,
|
|
134
|
+
agent=agent,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# Set up todo change callback to broadcast events
|
|
138
|
+
async def on_todo_change(tracker: Any) -> None:
|
|
139
|
+
"""Broadcast todo updates to all active sessions."""
|
|
140
|
+
from agentpool_server.opencode_server.models.events import Todo, TodoUpdatedEvent
|
|
141
|
+
|
|
142
|
+
# Convert tracker entries to OpenCode Todo models
|
|
143
|
+
todos = [
|
|
144
|
+
Todo(id=e.id, content=e.content, status=e.status, priority=e.priority)
|
|
145
|
+
for e in tracker.entries
|
|
146
|
+
]
|
|
147
|
+
# Broadcast to all active sessions
|
|
148
|
+
for session_id in state.sessions:
|
|
149
|
+
event = TodoUpdatedEvent.create(session_id=session_id, todos=todos)
|
|
150
|
+
await state.broadcast_event(event)
|
|
151
|
+
|
|
152
|
+
pool.todos.on_change = on_todo_change
|
|
153
|
+
|
|
154
|
+
# Set up title generation callback to update OpenCode sessions
|
|
155
|
+
|
|
156
|
+
async def on_title_generated(event: TitleGeneratedEvent) -> None:
|
|
157
|
+
"""Update session when title is generated by StorageManager."""
|
|
158
|
+
import logging
|
|
159
|
+
|
|
160
|
+
from agentpool_server.opencode_server.models.events import SessionUpdatedEvent
|
|
161
|
+
from agentpool_server.opencode_server.routes.session_routes import opencode_to_session_data
|
|
162
|
+
|
|
163
|
+
log = logging.getLogger(__name__)
|
|
164
|
+
log.info("on_title_generated called: %s, title=%s", event.conversation_id, event.title)
|
|
165
|
+
|
|
166
|
+
session_id = event.conversation_id
|
|
167
|
+
if session_id in state.sessions:
|
|
168
|
+
# Update in-memory session
|
|
169
|
+
session = state.sessions[session_id]
|
|
170
|
+
updated_session = session.model_copy(update={"title": event.title})
|
|
171
|
+
state.sessions[session_id] = updated_session
|
|
172
|
+
|
|
173
|
+
# Persist to storage
|
|
174
|
+
session_data = opencode_to_session_data(
|
|
175
|
+
updated_session,
|
|
176
|
+
agent_name=state.agent.name,
|
|
177
|
+
pool_id=state.pool.manifest.config_file_path,
|
|
178
|
+
)
|
|
179
|
+
await state.pool.sessions.store.save(session_data)
|
|
180
|
+
|
|
181
|
+
# Broadcast session update to UI
|
|
182
|
+
await state.broadcast_event(SessionUpdatedEvent.create(updated_session))
|
|
183
|
+
else:
|
|
184
|
+
log.warning("Session %s not found in state.sessions", session_id)
|
|
185
|
+
|
|
186
|
+
# Connect to storage manager's title_generated signal
|
|
187
|
+
if pool.storage:
|
|
188
|
+
pool.storage.title_generated.connect(on_title_generated)
|
|
189
|
+
|
|
190
|
+
# Watchers for VCS and file events
|
|
191
|
+
git_branch_watcher: Any = None
|
|
192
|
+
project_file_watcher: Any = None
|
|
193
|
+
|
|
194
|
+
@asynccontextmanager
|
|
195
|
+
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
|
|
196
|
+
nonlocal git_branch_watcher, project_file_watcher
|
|
197
|
+
import logging
|
|
198
|
+
|
|
199
|
+
from watchfiles import Change
|
|
200
|
+
|
|
201
|
+
from agentpool.utils.file_watcher import FileWatcher, GitBranchWatcher
|
|
202
|
+
from agentpool_server.opencode_server.models.events import (
|
|
203
|
+
FileWatcherUpdatedEvent,
|
|
204
|
+
VcsBranchUpdatedEvent,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
log = logging.getLogger(__name__)
|
|
208
|
+
|
|
209
|
+
# --- Git branch watcher ---
|
|
210
|
+
async def on_branch_change(branch: str | None) -> None:
|
|
211
|
+
"""Broadcast branch change to all subscribers."""
|
|
212
|
+
log.info("Broadcasting vcs.branch.updated event: %s", branch)
|
|
213
|
+
event = VcsBranchUpdatedEvent.create(branch=branch)
|
|
214
|
+
await state.broadcast_event(event)
|
|
215
|
+
|
|
216
|
+
log.info("Setting up GitBranchWatcher for: %s", state.working_dir)
|
|
217
|
+
git_branch_watcher = GitBranchWatcher(
|
|
218
|
+
repo_path=state.working_dir,
|
|
219
|
+
callback=on_branch_change,
|
|
220
|
+
)
|
|
221
|
+
await git_branch_watcher.start()
|
|
222
|
+
log.info("GitBranchWatcher started, current branch: %s", git_branch_watcher.current_branch)
|
|
223
|
+
|
|
224
|
+
# --- Project file watcher ---
|
|
225
|
+
# Map watchfiles Change types to OpenCode event types
|
|
226
|
+
change_type_map: dict[Change, str] = {
|
|
227
|
+
Change.added: "add",
|
|
228
|
+
Change.modified: "change",
|
|
229
|
+
Change.deleted: "unlink",
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async def on_file_change(changes: AbstractSet[tuple[Change, str]]) -> None:
|
|
233
|
+
"""Broadcast file changes to all subscribers."""
|
|
234
|
+
for change_type, file_path in changes:
|
|
235
|
+
# Skip .git directory changes
|
|
236
|
+
if "/.git/" in file_path or file_path.endswith("/.git"):
|
|
237
|
+
continue
|
|
238
|
+
event_type = change_type_map.get(change_type, "change")
|
|
239
|
+
log.info("Broadcasting file.watcher.updated: %s %s", event_type, file_path)
|
|
240
|
+
event = FileWatcherUpdatedEvent.create(file=file_path, event=event_type) # type: ignore[arg-type]
|
|
241
|
+
await state.broadcast_event(event)
|
|
242
|
+
|
|
243
|
+
log.info("Setting up project FileWatcher for: %s", state.working_dir)
|
|
244
|
+
project_file_watcher = FileWatcher(
|
|
245
|
+
paths=[state.working_dir],
|
|
246
|
+
callback=on_file_change,
|
|
247
|
+
debounce=500, # 500ms debounce to batch rapid changes
|
|
248
|
+
)
|
|
249
|
+
await project_file_watcher.start()
|
|
250
|
+
log.info("Project FileWatcher started")
|
|
251
|
+
|
|
252
|
+
# --- Version update check (triggered when first client connects) ---
|
|
253
|
+
async def check_for_updates() -> None:
|
|
254
|
+
"""Check PyPI for updates and notify via toast."""
|
|
255
|
+
from agentpool import __version__ as current_version
|
|
256
|
+
from agentpool_server.opencode_server.models.events import TuiToastShowEvent
|
|
257
|
+
|
|
258
|
+
latest = await check_pypi_version("agentpool")
|
|
259
|
+
if latest and compare_versions(current_version, latest):
|
|
260
|
+
log.info("Update available: %s -> %s", current_version, latest)
|
|
261
|
+
event = TuiToastShowEvent.create(
|
|
262
|
+
title="Update Available",
|
|
263
|
+
message=f"agentpool {latest} is available (current: {current_version})",
|
|
264
|
+
variant="info",
|
|
265
|
+
duration=10000,
|
|
266
|
+
)
|
|
267
|
+
await state.broadcast_event(event)
|
|
268
|
+
|
|
269
|
+
# Register callback to run when first SSE client connects
|
|
270
|
+
state.on_first_subscriber = check_for_updates
|
|
271
|
+
|
|
272
|
+
# Enter pool context to initialize session store and other components
|
|
273
|
+
async with pool:
|
|
274
|
+
yield
|
|
275
|
+
|
|
276
|
+
# Shutdown - clean up
|
|
277
|
+
pool.todos.on_change = None
|
|
278
|
+
if git_branch_watcher:
|
|
279
|
+
await git_branch_watcher.stop()
|
|
280
|
+
if project_file_watcher:
|
|
281
|
+
await project_file_watcher.stop()
|
|
282
|
+
# Clean up LSP servers
|
|
283
|
+
if state.lsp_manager is not None:
|
|
284
|
+
await state.lsp_manager.stop_all()
|
|
285
|
+
|
|
286
|
+
app = FastAPI(
|
|
287
|
+
title="OpenCode-Compatible API",
|
|
288
|
+
description="AgentPool server with OpenCode API compatibility",
|
|
289
|
+
version=VERSION,
|
|
290
|
+
lifespan=lifespan,
|
|
291
|
+
default_response_class=OpenCodeJSONResponse,
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
# Add CORS middleware (required for OpenCode TUI)
|
|
295
|
+
app.add_middleware(
|
|
296
|
+
CORSMiddleware,
|
|
297
|
+
allow_origins=["*"],
|
|
298
|
+
allow_credentials=True,
|
|
299
|
+
allow_methods=["*"],
|
|
300
|
+
allow_headers=["*"],
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
# Store state on app for access in routes
|
|
304
|
+
app.state.server_state = state
|
|
305
|
+
|
|
306
|
+
@app.exception_handler(RequestValidationError)
|
|
307
|
+
async def validation_exception_handler(
|
|
308
|
+
request: Request, exc: RequestValidationError
|
|
309
|
+
) -> JSONResponse:
|
|
310
|
+
body = await request.body()
|
|
311
|
+
print(f"Validation error for {request.url}")
|
|
312
|
+
print(f"Body: {body.decode()}")
|
|
313
|
+
print(f"Errors: {exc.errors()}")
|
|
314
|
+
return JSONResponse(
|
|
315
|
+
status_code=422,
|
|
316
|
+
content={"detail": exc.errors(), "body": body.decode()},
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
# Register routers
|
|
320
|
+
app.include_router(global_router)
|
|
321
|
+
app.include_router(app_router)
|
|
322
|
+
app.include_router(config_router)
|
|
323
|
+
app.include_router(session_router)
|
|
324
|
+
app.include_router(message_router)
|
|
325
|
+
app.include_router(file_router)
|
|
326
|
+
app.include_router(agent_router)
|
|
327
|
+
app.include_router(permission_router)
|
|
328
|
+
app.include_router(question_router)
|
|
329
|
+
app.include_router(pty_router)
|
|
330
|
+
app.include_router(tui_router)
|
|
331
|
+
app.include_router(lsp_router)
|
|
332
|
+
|
|
333
|
+
# OpenAPI doc redirect
|
|
334
|
+
@app.get("/doc")
|
|
335
|
+
async def get_doc() -> RedirectResponse:
|
|
336
|
+
"""Redirect to OpenAPI docs."""
|
|
337
|
+
return RedirectResponse(url="/docs")
|
|
338
|
+
|
|
339
|
+
# Proxy catch-all for OpenCode's hosted web UI
|
|
340
|
+
# This must be registered LAST so it doesn't catch API routes
|
|
341
|
+
@app.api_route("/{path:path}", methods=["GET", "HEAD", "OPTIONS"])
|
|
342
|
+
async def proxy_web_ui(request: Request, path: str) -> Response:
|
|
343
|
+
"""Proxy unmatched GET requests to OpenCode's hosted web UI.
|
|
344
|
+
|
|
345
|
+
This allows users to open http://localhost:4096 in a browser and get
|
|
346
|
+
the full OpenCode web interface, which then makes API calls back to
|
|
347
|
+
this local server for all data operations.
|
|
348
|
+
"""
|
|
349
|
+
import httpx
|
|
350
|
+
|
|
351
|
+
# Build target URL
|
|
352
|
+
url = f"https://app.opencode.ai/{path}"
|
|
353
|
+
if request.url.query:
|
|
354
|
+
url += f"?{request.url.query}"
|
|
355
|
+
|
|
356
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
357
|
+
# Forward the request
|
|
358
|
+
response = await client.request(
|
|
359
|
+
method=request.method,
|
|
360
|
+
url=url,
|
|
361
|
+
headers={"host": "app.opencode.ai"},
|
|
362
|
+
follow_redirects=True,
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
# Filter out hop-by-hop headers that shouldn't be forwarded
|
|
366
|
+
excluded_headers = {
|
|
367
|
+
"content-encoding",
|
|
368
|
+
"content-length",
|
|
369
|
+
"transfer-encoding",
|
|
370
|
+
"connection",
|
|
371
|
+
}
|
|
372
|
+
headers = {
|
|
373
|
+
k: v for k, v in response.headers.items() if k.lower() not in excluded_headers
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return Response(
|
|
377
|
+
content=response.content,
|
|
378
|
+
status_code=response.status_code,
|
|
379
|
+
headers=headers,
|
|
380
|
+
media_type=response.headers.get("content-type"),
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
logfire.instrument_fastapi(app)
|
|
384
|
+
return app
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
class OpenCodeServer:
|
|
388
|
+
"""OpenCode-compatible server wrapper.
|
|
389
|
+
|
|
390
|
+
Provides a convenient interface for running the server.
|
|
391
|
+
"""
|
|
392
|
+
|
|
393
|
+
def __init__(
|
|
394
|
+
self,
|
|
395
|
+
pool: AgentPool[Any],
|
|
396
|
+
*,
|
|
397
|
+
host: str = "127.0.0.1",
|
|
398
|
+
port: int = 4096,
|
|
399
|
+
agent_name: str | None = None,
|
|
400
|
+
working_dir: str | None = None,
|
|
401
|
+
) -> None:
|
|
402
|
+
"""Initialize the server.
|
|
403
|
+
|
|
404
|
+
Args:
|
|
405
|
+
pool: AgentPool for session persistence and agent access.
|
|
406
|
+
host: Host to bind to.
|
|
407
|
+
port: Port to listen on.
|
|
408
|
+
agent_name: Name of the agent to use for handling messages.
|
|
409
|
+
working_dir: Working directory for file operations.
|
|
410
|
+
"""
|
|
411
|
+
self.host = host
|
|
412
|
+
self.port = port
|
|
413
|
+
self.pool = pool
|
|
414
|
+
self.agent_name = agent_name
|
|
415
|
+
self.working_dir = working_dir
|
|
416
|
+
self._app: FastAPI | None = None
|
|
417
|
+
|
|
418
|
+
@property
|
|
419
|
+
def app(self) -> FastAPI:
|
|
420
|
+
"""Get or create the FastAPI application."""
|
|
421
|
+
if self._app is None:
|
|
422
|
+
self._app = create_app(
|
|
423
|
+
pool=self.pool,
|
|
424
|
+
agent_name=self.agent_name,
|
|
425
|
+
working_dir=self.working_dir,
|
|
426
|
+
)
|
|
427
|
+
return self._app
|
|
428
|
+
|
|
429
|
+
def run(self) -> None:
|
|
430
|
+
"""Run the server (blocking)."""
|
|
431
|
+
import uvicorn
|
|
432
|
+
|
|
433
|
+
uvicorn.run(self.app, host=self.host, port=self.port)
|
|
434
|
+
|
|
435
|
+
async def run_async(self) -> None:
|
|
436
|
+
"""Run the server asynchronously."""
|
|
437
|
+
import uvicorn
|
|
438
|
+
|
|
439
|
+
config = uvicorn.Config(self.app, host=self.host, port=self.port, ws="websockets-sansio")
|
|
440
|
+
server = uvicorn.Server(config)
|
|
441
|
+
await server.serve()
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def run_server(
|
|
445
|
+
pool: AgentPool[Any],
|
|
446
|
+
*,
|
|
447
|
+
host: str = "127.0.0.1",
|
|
448
|
+
port: int = 4096,
|
|
449
|
+
agent_name: str | None = None,
|
|
450
|
+
working_dir: str | None = None,
|
|
451
|
+
) -> None:
|
|
452
|
+
"""Run the OpenCode-compatible server.
|
|
453
|
+
|
|
454
|
+
Args:
|
|
455
|
+
pool: AgentPool for session persistence and agent access.
|
|
456
|
+
host: Host to bind to.
|
|
457
|
+
port: Port to listen on.
|
|
458
|
+
agent_name: Name of the agent to use for handling messages.
|
|
459
|
+
working_dir: Working directory for file operations.
|
|
460
|
+
"""
|
|
461
|
+
server = OpenCodeServer(
|
|
462
|
+
pool,
|
|
463
|
+
host=host,
|
|
464
|
+
port=port,
|
|
465
|
+
agent_name=agent_name,
|
|
466
|
+
working_dir=working_dir,
|
|
467
|
+
)
|
|
468
|
+
server.run()
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
if __name__ == "__main__":
|
|
472
|
+
from agentpool import config_resources
|
|
473
|
+
|
|
474
|
+
pool = AgentPool(config_resources.CLAUDE_CODE_ASSISTANT)
|
|
475
|
+
run_server(pool)
|