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
|
@@ -6,18 +6,19 @@ from pathlib import Path
|
|
|
6
6
|
import subprocess
|
|
7
7
|
from typing import TYPE_CHECKING
|
|
8
8
|
|
|
9
|
-
from fastapi import APIRouter
|
|
9
|
+
from fastapi import APIRouter, HTTPException
|
|
10
10
|
|
|
11
|
-
from agentpool_server.opencode_server.dependencies import StateDep
|
|
11
|
+
from agentpool_server.opencode_server.dependencies import StateDep
|
|
12
12
|
from agentpool_server.opencode_server.models import (
|
|
13
13
|
App,
|
|
14
14
|
AppTimeInfo,
|
|
15
15
|
PathInfo,
|
|
16
16
|
Project,
|
|
17
17
|
ProjectTime,
|
|
18
|
+
ProjectUpdatedEvent,
|
|
19
|
+
ProjectUpdateRequest,
|
|
18
20
|
VcsInfo,
|
|
19
21
|
)
|
|
20
|
-
from agentpool_storage.project_store import ProjectStore
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
if TYPE_CHECKING:
|
|
@@ -65,6 +66,8 @@ def _project_data_to_response(data: ProjectData) -> Project:
|
|
|
65
66
|
|
|
66
67
|
async def _get_current_project(state: StateDep) -> ProjectData:
|
|
67
68
|
"""Get or create the current project from storage."""
|
|
69
|
+
from agentpool_storage.project_store import ProjectStore
|
|
70
|
+
|
|
68
71
|
storage = state.pool.storage
|
|
69
72
|
project_store = ProjectStore(storage)
|
|
70
73
|
return await project_store.get_or_create(state.working_dir)
|
|
@@ -73,6 +76,8 @@ async def _get_current_project(state: StateDep) -> ProjectData:
|
|
|
73
76
|
@router.get("/project")
|
|
74
77
|
async def list_projects(state: StateDep) -> list[Project]:
|
|
75
78
|
"""List all projects."""
|
|
79
|
+
from agentpool_storage.project_store import ProjectStore
|
|
80
|
+
|
|
76
81
|
storage = state.pool.storage
|
|
77
82
|
project_store = ProjectStore(storage)
|
|
78
83
|
projects = await project_store.list_recent(limit=50)
|
|
@@ -86,6 +91,64 @@ async def get_project_current(state: StateDep) -> Project:
|
|
|
86
91
|
return _project_data_to_response(project)
|
|
87
92
|
|
|
88
93
|
|
|
94
|
+
@router.patch("/project/{project_id}")
|
|
95
|
+
async def update_project(
|
|
96
|
+
project_id: str,
|
|
97
|
+
update: ProjectUpdateRequest,
|
|
98
|
+
state: StateDep,
|
|
99
|
+
) -> Project:
|
|
100
|
+
"""Update project metadata (name, settings).
|
|
101
|
+
|
|
102
|
+
Emits a project.updated event when successful.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
project_id: Project identifier
|
|
106
|
+
update: Fields to update (name and/or settings)
|
|
107
|
+
state: Server state
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Updated project data
|
|
111
|
+
|
|
112
|
+
Raises:
|
|
113
|
+
HTTPException: If project not found
|
|
114
|
+
"""
|
|
115
|
+
from agentpool_storage.project_store import ProjectStore
|
|
116
|
+
|
|
117
|
+
store = ProjectStore(state.pool.storage)
|
|
118
|
+
project_data = None
|
|
119
|
+
|
|
120
|
+
# Update name if provided
|
|
121
|
+
if update.name is not None:
|
|
122
|
+
project_data = await store.set_name(project_id, update.name)
|
|
123
|
+
if not project_data:
|
|
124
|
+
raise HTTPException(status_code=404, detail="Project not found")
|
|
125
|
+
|
|
126
|
+
# Update settings if provided
|
|
127
|
+
if update.settings:
|
|
128
|
+
if project_data:
|
|
129
|
+
# Already fetched from set_name, update with settings
|
|
130
|
+
project_data = await store.update_settings(project_id, **update.settings)
|
|
131
|
+
else:
|
|
132
|
+
project_data = await store.update_settings(project_id, **update.settings)
|
|
133
|
+
|
|
134
|
+
if not project_data:
|
|
135
|
+
raise HTTPException(status_code=404, detail="Project not found")
|
|
136
|
+
|
|
137
|
+
# If neither name nor settings provided, just fetch the project
|
|
138
|
+
if not project_data:
|
|
139
|
+
project_data = await store.get_by_id(project_id)
|
|
140
|
+
if not project_data:
|
|
141
|
+
raise HTTPException(status_code=404, detail="Project not found")
|
|
142
|
+
|
|
143
|
+
# Convert to OpenCode Project model
|
|
144
|
+
project = _project_data_to_response(project_data)
|
|
145
|
+
|
|
146
|
+
# Broadcast event
|
|
147
|
+
await state.broadcast_event(ProjectUpdatedEvent.create(project))
|
|
148
|
+
|
|
149
|
+
return project
|
|
150
|
+
|
|
151
|
+
|
|
89
152
|
@router.get("/path")
|
|
90
153
|
async def get_path(state: StateDep) -> PathInfo:
|
|
91
154
|
"""Get current path info."""
|
|
@@ -8,7 +8,7 @@ from typing import TYPE_CHECKING
|
|
|
8
8
|
|
|
9
9
|
from fastapi import APIRouter
|
|
10
10
|
|
|
11
|
-
from agentpool_server.opencode_server.dependencies import StateDep
|
|
11
|
+
from agentpool_server.opencode_server.dependencies import StateDep
|
|
12
12
|
from agentpool_server.opencode_server.models import (
|
|
13
13
|
Config,
|
|
14
14
|
Mode,
|
|
@@ -152,8 +152,49 @@ async def _get_available_models() -> list[TokoModelInfo]:
|
|
|
152
152
|
@router.get("/config")
|
|
153
153
|
async def get_config(state: StateDep) -> Config:
|
|
154
154
|
"""Get server configuration."""
|
|
155
|
-
|
|
156
|
-
|
|
155
|
+
import os
|
|
156
|
+
|
|
157
|
+
# Initialize config if not yet set
|
|
158
|
+
if state.config is None:
|
|
159
|
+
state.config = Config()
|
|
160
|
+
|
|
161
|
+
# Set a default model if not already configured
|
|
162
|
+
if state.config.model is None:
|
|
163
|
+
try:
|
|
164
|
+
# Get available models
|
|
165
|
+
toko_models = await state.agent.get_available_models()
|
|
166
|
+
if toko_models:
|
|
167
|
+
providers = _build_providers(toko_models)
|
|
168
|
+
|
|
169
|
+
# Find first connected provider and use its first model
|
|
170
|
+
for provider in providers:
|
|
171
|
+
if any(os.environ.get(env) for env in provider.env) and provider.models:
|
|
172
|
+
first_model = next(iter(provider.models.keys()))
|
|
173
|
+
state.config.model = f"{provider.id}/{first_model}"
|
|
174
|
+
break
|
|
175
|
+
except Exception: # noqa: BLE001
|
|
176
|
+
pass # If we can't set a default, that's okay
|
|
177
|
+
|
|
178
|
+
return state.config
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@router.patch("/config")
|
|
182
|
+
async def update_config(state: StateDep, config_update: Config) -> Config:
|
|
183
|
+
"""Update server configuration.
|
|
184
|
+
|
|
185
|
+
Only updates fields that are provided (non-None).
|
|
186
|
+
Returns the complete updated config.
|
|
187
|
+
"""
|
|
188
|
+
# Initialize config if not yet set
|
|
189
|
+
if state.config is None:
|
|
190
|
+
state.config = Config()
|
|
191
|
+
|
|
192
|
+
# Update only the fields that were provided
|
|
193
|
+
update_data = config_update.model_dump(exclude_unset=True)
|
|
194
|
+
for field_name, value in update_data.items():
|
|
195
|
+
setattr(state.config, field_name, value)
|
|
196
|
+
|
|
197
|
+
return state.config
|
|
157
198
|
|
|
158
199
|
|
|
159
200
|
def _get_dummy_providers() -> list[Provider]:
|
|
@@ -181,6 +222,8 @@ def _get_dummy_providers() -> list[Provider]:
|
|
|
181
222
|
@router.get("/config/providers")
|
|
182
223
|
async def get_providers(state: StateDep) -> ProvidersResponse:
|
|
183
224
|
"""Get available providers and models from agent."""
|
|
225
|
+
import os
|
|
226
|
+
|
|
184
227
|
providers: list[Provider] = []
|
|
185
228
|
|
|
186
229
|
# Try to get models from the agent
|
|
@@ -195,7 +238,18 @@ async def get_providers(state: StateDep) -> ProvidersResponse:
|
|
|
195
238
|
if not providers:
|
|
196
239
|
providers = _get_dummy_providers()
|
|
197
240
|
|
|
198
|
-
|
|
241
|
+
# Build default models map: use first model for each connected provider
|
|
242
|
+
default_models: dict[str, str] = {}
|
|
243
|
+
connected_providers = [
|
|
244
|
+
provider.id for provider in providers if any(os.environ.get(env) for env in provider.env)
|
|
245
|
+
]
|
|
246
|
+
|
|
247
|
+
for provider in providers:
|
|
248
|
+
if provider.id in connected_providers and provider.models:
|
|
249
|
+
# Simply use the first available model
|
|
250
|
+
default_models[provider.id] = next(iter(provider.models.keys()))
|
|
251
|
+
|
|
252
|
+
return ProvidersResponse(providers=providers, default=default_models)
|
|
199
253
|
|
|
200
254
|
|
|
201
255
|
@router.get("/provider")
|
|
@@ -222,9 +276,16 @@ async def list_providers(state: StateDep) -> ProviderListResponse:
|
|
|
222
276
|
provider.id for provider in providers if any(os.environ.get(env) for env in provider.env)
|
|
223
277
|
]
|
|
224
278
|
|
|
279
|
+
# Build default models map: use first model for each connected provider
|
|
280
|
+
default_models: dict[str, str] = {}
|
|
281
|
+
for provider in providers:
|
|
282
|
+
if provider.id in connected and provider.models:
|
|
283
|
+
# Simply use the first available model
|
|
284
|
+
default_models[provider.id] = next(iter(provider.models.keys()))
|
|
285
|
+
|
|
225
286
|
return ProviderListResponse(
|
|
226
287
|
all=providers,
|
|
227
|
-
default=
|
|
288
|
+
default=default_models,
|
|
228
289
|
connected=connected,
|
|
229
290
|
)
|
|
230
291
|
|
|
@@ -2,15 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import asyncio
|
|
5
6
|
import fnmatch
|
|
7
|
+
import json
|
|
6
8
|
from pathlib import Path
|
|
7
9
|
import re
|
|
10
|
+
import shutil
|
|
8
11
|
from typing import TYPE_CHECKING, Any
|
|
9
12
|
|
|
10
13
|
from fastapi import APIRouter, HTTPException, Query
|
|
11
14
|
|
|
12
|
-
from agentpool_server.opencode_server.dependencies import StateDep
|
|
13
|
-
from agentpool_server.opencode_server.models import (
|
|
15
|
+
from agentpool_server.opencode_server.dependencies import StateDep
|
|
16
|
+
from agentpool_server.opencode_server.models import (
|
|
14
17
|
FileContent,
|
|
15
18
|
FileNode,
|
|
16
19
|
FindMatch,
|
|
@@ -106,6 +109,157 @@ def _get_fs(state: StateDep) -> tuple[AsyncFileSystem, str] | None:
|
|
|
106
109
|
return (fs, base_path)
|
|
107
110
|
|
|
108
111
|
|
|
112
|
+
def _is_local_fs(fs: AsyncFileSystem) -> bool:
|
|
113
|
+
"""Check if filesystem is a local filesystem."""
|
|
114
|
+
return getattr(fs, "local_file", False)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _has_ripgrep() -> bool:
|
|
118
|
+
"""Check if ripgrep is available."""
|
|
119
|
+
return shutil.which("rg") is not None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
async def _search_with_ripgrep(
|
|
123
|
+
pattern: str,
|
|
124
|
+
base_path: str,
|
|
125
|
+
max_matches: int = 100,
|
|
126
|
+
) -> list[FindMatch]:
|
|
127
|
+
"""Search using ripgrep for better performance on local filesystems.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
pattern: Regex pattern to search for.
|
|
131
|
+
base_path: Directory to search in.
|
|
132
|
+
max_matches: Maximum number of matches to return.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
List of FindMatch objects.
|
|
136
|
+
"""
|
|
137
|
+
# Build ripgrep command with JSON output
|
|
138
|
+
cmd = [
|
|
139
|
+
"rg",
|
|
140
|
+
"--json",
|
|
141
|
+
"--max-count",
|
|
142
|
+
str(max_matches),
|
|
143
|
+
"--no-binary",
|
|
144
|
+
]
|
|
145
|
+
|
|
146
|
+
# Add exclude patterns for SKIP_DIRS
|
|
147
|
+
for skip_dir in SKIP_DIRS:
|
|
148
|
+
cmd.extend(["--glob", f"!{skip_dir}/"])
|
|
149
|
+
|
|
150
|
+
cmd.extend(["-e", pattern, base_path])
|
|
151
|
+
|
|
152
|
+
# Run ripgrep asynchronously
|
|
153
|
+
proc = await asyncio.create_subprocess_exec(
|
|
154
|
+
*cmd,
|
|
155
|
+
stdout=asyncio.subprocess.PIPE,
|
|
156
|
+
stderr=asyncio.subprocess.PIPE,
|
|
157
|
+
)
|
|
158
|
+
stdout, _ = await proc.communicate()
|
|
159
|
+
|
|
160
|
+
matches: list[FindMatch] = []
|
|
161
|
+
base_path_prefix = base_path.rstrip("/") + "/"
|
|
162
|
+
|
|
163
|
+
for line in stdout.decode("utf-8", errors="replace").splitlines():
|
|
164
|
+
if not line.strip():
|
|
165
|
+
continue
|
|
166
|
+
try:
|
|
167
|
+
data = json.loads(line)
|
|
168
|
+
if data.get("type") != "match":
|
|
169
|
+
continue
|
|
170
|
+
|
|
171
|
+
match_data = data.get("data", {})
|
|
172
|
+
path = match_data.get("path", {}).get("text", "")
|
|
173
|
+
line_number = match_data.get("line_number", 0)
|
|
174
|
+
line_text = match_data.get("lines", {}).get("text", "").rstrip("\n")
|
|
175
|
+
absolute_offset = match_data.get("absolute_offset", 0)
|
|
176
|
+
|
|
177
|
+
# Convert to relative path
|
|
178
|
+
rel_path = path[len(base_path_prefix) :] if path.startswith(base_path_prefix) else path
|
|
179
|
+
|
|
180
|
+
# Extract submatches
|
|
181
|
+
submatches = []
|
|
182
|
+
for sm in match_data.get("submatches", []):
|
|
183
|
+
match_text = sm.get("match", {}).get("text", "")
|
|
184
|
+
start = sm.get("start", 0)
|
|
185
|
+
end = sm.get("end", 0)
|
|
186
|
+
submatches.append(SubmatchInfo.create(match_text, start, end))
|
|
187
|
+
|
|
188
|
+
matches.append(
|
|
189
|
+
FindMatch.create(
|
|
190
|
+
path=rel_path,
|
|
191
|
+
lines=line_text.strip(),
|
|
192
|
+
line_number=line_number,
|
|
193
|
+
absolute_offset=absolute_offset,
|
|
194
|
+
submatches=submatches,
|
|
195
|
+
)
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
if len(matches) >= max_matches:
|
|
199
|
+
break
|
|
200
|
+
except json.JSONDecodeError:
|
|
201
|
+
continue
|
|
202
|
+
|
|
203
|
+
return matches
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
async def _find_files_with_ripgrep(
|
|
207
|
+
query: str,
|
|
208
|
+
base_path: str,
|
|
209
|
+
max_results: int = 100,
|
|
210
|
+
) -> list[str]:
|
|
211
|
+
"""Find files using ripgrep --files for better performance.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
query: Glob pattern to match file names.
|
|
215
|
+
base_path: Directory to search in.
|
|
216
|
+
max_results: Maximum number of results to return.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
List of relative file paths.
|
|
220
|
+
"""
|
|
221
|
+
# Build ripgrep command to list files matching glob
|
|
222
|
+
cmd = ["rg", "--files"]
|
|
223
|
+
|
|
224
|
+
# Add exclude patterns for SKIP_DIRS
|
|
225
|
+
for skip_dir in SKIP_DIRS:
|
|
226
|
+
cmd.extend(["--glob", f"!{skip_dir}/"])
|
|
227
|
+
|
|
228
|
+
# Add the file name pattern as a glob
|
|
229
|
+
# rg --files --glob supports matching anywhere in the path
|
|
230
|
+
# If query doesn't contain glob chars, wrap it with * for substring matching
|
|
231
|
+
glob_chars = {"*", "?", "[", "]"}
|
|
232
|
+
if not any(c in query for c in glob_chars):
|
|
233
|
+
query = f"*{query}*"
|
|
234
|
+
# Use **/ prefix to match the filename in any directory
|
|
235
|
+
cmd.extend(["--glob", f"**/{query}"])
|
|
236
|
+
cmd.append(base_path)
|
|
237
|
+
|
|
238
|
+
# Run ripgrep asynchronously
|
|
239
|
+
proc = await asyncio.create_subprocess_exec(
|
|
240
|
+
*cmd,
|
|
241
|
+
stdout=asyncio.subprocess.PIPE,
|
|
242
|
+
stderr=asyncio.subprocess.PIPE,
|
|
243
|
+
)
|
|
244
|
+
stdout, _ = await proc.communicate()
|
|
245
|
+
|
|
246
|
+
results: list[str] = []
|
|
247
|
+
base_path_prefix = base_path.rstrip("/") + "/"
|
|
248
|
+
|
|
249
|
+
for line in stdout.decode("utf-8", errors="replace").splitlines():
|
|
250
|
+
if not line.strip():
|
|
251
|
+
continue
|
|
252
|
+
|
|
253
|
+
# Convert to relative path
|
|
254
|
+
rel_path = line[len(base_path_prefix) :] if line.startswith(base_path_prefix) else line
|
|
255
|
+
|
|
256
|
+
results.append(rel_path)
|
|
257
|
+
if len(results) >= max_results:
|
|
258
|
+
break
|
|
259
|
+
|
|
260
|
+
return sorted(results)
|
|
261
|
+
|
|
262
|
+
|
|
109
263
|
@router.get("/file")
|
|
110
264
|
async def list_files(state: StateDep, path: str = Query(default="")) -> list[FileNode]:
|
|
111
265
|
"""List files in a directory."""
|
|
@@ -216,15 +370,29 @@ async def get_file_status(state: StateDep) -> list[dict[str, Any]]:
|
|
|
216
370
|
@router.get("/find")
|
|
217
371
|
async def find_text(state: StateDep, pattern: str = Query()) -> list[FindMatch]: # noqa: PLR0915
|
|
218
372
|
"""Search for text pattern in files using regex."""
|
|
373
|
+
# Validate regex pattern
|
|
219
374
|
try:
|
|
220
|
-
|
|
375
|
+
re.compile(pattern)
|
|
221
376
|
except re.error as e:
|
|
222
377
|
raise HTTPException(status_code=400, detail=f"Invalid regex: {e}") from e
|
|
223
378
|
|
|
224
|
-
matches: list[FindMatch] = []
|
|
225
379
|
max_matches = 100
|
|
226
380
|
fs_info = _get_fs(state)
|
|
227
381
|
|
|
382
|
+
# Fast path: use ripgrep for local filesystems
|
|
383
|
+
if fs_info is not None:
|
|
384
|
+
fs, base_path = fs_info
|
|
385
|
+
if _is_local_fs(fs) and _has_ripgrep():
|
|
386
|
+
return await _search_with_ripgrep(pattern, base_path, max_matches)
|
|
387
|
+
|
|
388
|
+
# Fallback: use ripgrep directly if no fs but ripgrep available
|
|
389
|
+
if fs_info is None and _has_ripgrep():
|
|
390
|
+
return await _search_with_ripgrep(pattern, state.working_dir, max_matches)
|
|
391
|
+
|
|
392
|
+
# Slow path: manual file iteration
|
|
393
|
+
matches: list[FindMatch] = []
|
|
394
|
+
regex = re.compile(pattern)
|
|
395
|
+
|
|
228
396
|
if fs_info is not None:
|
|
229
397
|
fs, base_path = fs_info
|
|
230
398
|
|
|
@@ -326,10 +494,21 @@ async def find_files(
|
|
|
326
494
|
) -> list[str]:
|
|
327
495
|
"""Find files by name pattern (glob-style matching)."""
|
|
328
496
|
include_dirs = dirs.lower() == "true"
|
|
329
|
-
results: list[str] = []
|
|
330
497
|
max_results = 100
|
|
331
498
|
fs_info = _get_fs(state)
|
|
332
499
|
|
|
500
|
+
# Fast path: use ripgrep for local filesystems (files only, not dirs)
|
|
501
|
+
if not include_dirs and _has_ripgrep():
|
|
502
|
+
if fs_info is not None:
|
|
503
|
+
fs, base_path = fs_info
|
|
504
|
+
if _is_local_fs(fs):
|
|
505
|
+
return await _find_files_with_ripgrep(query, base_path, max_results)
|
|
506
|
+
else:
|
|
507
|
+
return await _find_files_with_ripgrep(query, state.working_dir, max_results)
|
|
508
|
+
|
|
509
|
+
# Slow path: manual file iteration
|
|
510
|
+
results: list[str] = []
|
|
511
|
+
|
|
333
512
|
if fs_info is not None:
|
|
334
513
|
fs, base_path = fs_info
|
|
335
514
|
# Use fsspec filesystem
|
|
@@ -9,7 +9,7 @@ from typing import TYPE_CHECKING, Any
|
|
|
9
9
|
from fastapi import APIRouter
|
|
10
10
|
from sse_starlette.sse import EventSourceResponse
|
|
11
11
|
|
|
12
|
-
from agentpool_server.opencode_server.dependencies import StateDep
|
|
12
|
+
from agentpool_server.opencode_server.dependencies import StateDep
|
|
13
13
|
from agentpool_server.opencode_server.models import ( # noqa: TC001
|
|
14
14
|
Event,
|
|
15
15
|
HealthResponse,
|
|
@@ -13,7 +13,7 @@ from typing import Literal
|
|
|
13
13
|
from fastapi import APIRouter, HTTPException, Query
|
|
14
14
|
from pydantic import BaseModel
|
|
15
15
|
|
|
16
|
-
from agentpool_server.opencode_server.dependencies import StateDep
|
|
16
|
+
from agentpool_server.opencode_server.dependencies import StateDep
|
|
17
17
|
from agentpool_server.opencode_server.models.events import LspStatus, LspUpdatedEvent
|
|
18
18
|
|
|
19
19
|
|