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.
Files changed (250) hide show
  1. acp/__init__.py +0 -4
  2. acp/acp_requests.py +20 -77
  3. acp/agent/connection.py +8 -0
  4. acp/agent/implementations/debug_server/debug_server.py +6 -2
  5. acp/agent/protocol.py +6 -0
  6. acp/client/connection.py +38 -29
  7. acp/client/implementations/default_client.py +3 -2
  8. acp/client/implementations/headless_client.py +2 -2
  9. acp/connection.py +2 -2
  10. acp/notifications.py +18 -49
  11. acp/schema/__init__.py +2 -0
  12. acp/schema/agent_responses.py +21 -0
  13. acp/schema/client_requests.py +3 -3
  14. acp/schema/session_state.py +63 -29
  15. acp/task/supervisor.py +2 -2
  16. acp/utils.py +2 -2
  17. agentpool/__init__.py +2 -0
  18. agentpool/agents/acp_agent/acp_agent.py +278 -263
  19. agentpool/agents/acp_agent/acp_converters.py +150 -17
  20. agentpool/agents/acp_agent/client_handler.py +35 -24
  21. agentpool/agents/acp_agent/session_state.py +14 -6
  22. agentpool/agents/agent.py +471 -643
  23. agentpool/agents/agui_agent/agui_agent.py +104 -107
  24. agentpool/agents/agui_agent/helpers.py +3 -4
  25. agentpool/agents/base_agent.py +485 -32
  26. agentpool/agents/claude_code_agent/FORKING.md +191 -0
  27. agentpool/agents/claude_code_agent/__init__.py +13 -1
  28. agentpool/agents/claude_code_agent/claude_code_agent.py +654 -334
  29. agentpool/agents/claude_code_agent/converters.py +4 -141
  30. agentpool/agents/claude_code_agent/models.py +77 -0
  31. agentpool/agents/claude_code_agent/static_info.py +100 -0
  32. agentpool/agents/claude_code_agent/usage.py +242 -0
  33. agentpool/agents/events/__init__.py +22 -0
  34. agentpool/agents/events/builtin_handlers.py +65 -0
  35. agentpool/agents/events/event_emitter.py +3 -0
  36. agentpool/agents/events/events.py +84 -3
  37. agentpool/agents/events/infer_info.py +145 -0
  38. agentpool/agents/events/processors.py +254 -0
  39. agentpool/agents/interactions.py +41 -6
  40. agentpool/agents/modes.py +13 -0
  41. agentpool/agents/slashed_agent.py +5 -4
  42. agentpool/agents/tool_wrapping.py +18 -6
  43. agentpool/common_types.py +35 -21
  44. agentpool/config_resources/acp_assistant.yml +2 -2
  45. agentpool/config_resources/agents.yml +3 -0
  46. agentpool/config_resources/agents_template.yml +1 -0
  47. agentpool/config_resources/claude_code_agent.yml +9 -8
  48. agentpool/config_resources/external_acp_agents.yml +2 -1
  49. agentpool/delegation/base_team.py +4 -30
  50. agentpool/delegation/pool.py +104 -265
  51. agentpool/delegation/team.py +57 -57
  52. agentpool/delegation/teamrun.py +50 -55
  53. agentpool/functional/run.py +10 -4
  54. agentpool/mcp_server/client.py +73 -38
  55. agentpool/mcp_server/conversions.py +54 -13
  56. agentpool/mcp_server/manager.py +9 -23
  57. agentpool/mcp_server/registries/official_registry_client.py +10 -1
  58. agentpool/mcp_server/tool_bridge.py +114 -79
  59. agentpool/messaging/connection_manager.py +11 -10
  60. agentpool/messaging/event_manager.py +5 -5
  61. agentpool/messaging/message_container.py +6 -30
  62. agentpool/messaging/message_history.py +87 -8
  63. agentpool/messaging/messagenode.py +52 -14
  64. agentpool/messaging/messages.py +2 -26
  65. agentpool/messaging/processing.py +10 -22
  66. agentpool/models/__init__.py +1 -1
  67. agentpool/models/acp_agents/base.py +6 -2
  68. agentpool/models/acp_agents/mcp_capable.py +124 -15
  69. agentpool/models/acp_agents/non_mcp.py +0 -23
  70. agentpool/models/agents.py +66 -66
  71. agentpool/models/agui_agents.py +1 -1
  72. agentpool/models/claude_code_agents.py +111 -17
  73. agentpool/models/file_parsing.py +0 -1
  74. agentpool/models/manifest.py +70 -50
  75. agentpool/prompts/conversion_manager.py +1 -1
  76. agentpool/prompts/prompts.py +5 -2
  77. agentpool/resource_providers/__init__.py +2 -0
  78. agentpool/resource_providers/aggregating.py +4 -2
  79. agentpool/resource_providers/base.py +13 -3
  80. agentpool/resource_providers/codemode/code_executor.py +72 -5
  81. agentpool/resource_providers/codemode/helpers.py +2 -2
  82. agentpool/resource_providers/codemode/provider.py +64 -12
  83. agentpool/resource_providers/codemode/remote_mcp_execution.py +2 -2
  84. agentpool/resource_providers/codemode/remote_provider.py +9 -12
  85. agentpool/resource_providers/filtering.py +3 -1
  86. agentpool/resource_providers/mcp_provider.py +66 -12
  87. agentpool/resource_providers/plan_provider.py +111 -18
  88. agentpool/resource_providers/pool.py +5 -3
  89. agentpool/resource_providers/resource_info.py +111 -0
  90. agentpool/resource_providers/static.py +2 -2
  91. agentpool/sessions/__init__.py +2 -0
  92. agentpool/sessions/manager.py +2 -3
  93. agentpool/sessions/models.py +9 -6
  94. agentpool/sessions/protocol.py +28 -0
  95. agentpool/sessions/session.py +11 -55
  96. agentpool/storage/manager.py +361 -54
  97. agentpool/talk/registry.py +4 -4
  98. agentpool/talk/talk.py +9 -10
  99. agentpool/testing.py +1 -1
  100. agentpool/tool_impls/__init__.py +6 -0
  101. agentpool/tool_impls/agent_cli/__init__.py +42 -0
  102. agentpool/tool_impls/agent_cli/tool.py +95 -0
  103. agentpool/tool_impls/bash/__init__.py +64 -0
  104. agentpool/tool_impls/bash/helpers.py +35 -0
  105. agentpool/tool_impls/bash/tool.py +171 -0
  106. agentpool/tool_impls/delete_path/__init__.py +70 -0
  107. agentpool/tool_impls/delete_path/tool.py +142 -0
  108. agentpool/tool_impls/download_file/__init__.py +80 -0
  109. agentpool/tool_impls/download_file/tool.py +183 -0
  110. agentpool/tool_impls/execute_code/__init__.py +55 -0
  111. agentpool/tool_impls/execute_code/tool.py +163 -0
  112. agentpool/tool_impls/grep/__init__.py +80 -0
  113. agentpool/tool_impls/grep/tool.py +200 -0
  114. agentpool/tool_impls/list_directory/__init__.py +73 -0
  115. agentpool/tool_impls/list_directory/tool.py +197 -0
  116. agentpool/tool_impls/question/__init__.py +42 -0
  117. agentpool/tool_impls/question/tool.py +127 -0
  118. agentpool/tool_impls/read/__init__.py +104 -0
  119. agentpool/tool_impls/read/tool.py +305 -0
  120. agentpool/tools/__init__.py +2 -1
  121. agentpool/tools/base.py +114 -34
  122. agentpool/tools/manager.py +57 -1
  123. agentpool/ui/base.py +2 -2
  124. agentpool/ui/mock_provider.py +2 -2
  125. agentpool/ui/stdlib_provider.py +2 -2
  126. agentpool/utils/streams.py +21 -96
  127. agentpool/vfs_registry.py +7 -2
  128. {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/METADATA +16 -22
  129. {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/RECORD +242 -195
  130. {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/WHEEL +1 -1
  131. agentpool_cli/__main__.py +20 -0
  132. agentpool_cli/create.py +1 -1
  133. agentpool_cli/serve_acp.py +59 -1
  134. agentpool_cli/serve_opencode.py +1 -1
  135. agentpool_cli/ui.py +557 -0
  136. agentpool_commands/__init__.py +12 -5
  137. agentpool_commands/agents.py +1 -1
  138. agentpool_commands/pool.py +260 -0
  139. agentpool_commands/session.py +1 -1
  140. agentpool_commands/text_sharing/__init__.py +119 -0
  141. agentpool_commands/text_sharing/base.py +123 -0
  142. agentpool_commands/text_sharing/github_gist.py +80 -0
  143. agentpool_commands/text_sharing/opencode.py +462 -0
  144. agentpool_commands/text_sharing/paste_rs.py +59 -0
  145. agentpool_commands/text_sharing/pastebin.py +116 -0
  146. agentpool_commands/text_sharing/shittycodingagent.py +112 -0
  147. agentpool_commands/utils.py +31 -32
  148. agentpool_config/__init__.py +30 -2
  149. agentpool_config/agentpool_tools.py +498 -0
  150. agentpool_config/converters.py +1 -1
  151. agentpool_config/event_handlers.py +42 -0
  152. agentpool_config/events.py +1 -1
  153. agentpool_config/forward_targets.py +1 -4
  154. agentpool_config/jinja.py +3 -3
  155. agentpool_config/mcp_server.py +1 -5
  156. agentpool_config/nodes.py +1 -1
  157. agentpool_config/observability.py +44 -0
  158. agentpool_config/session.py +0 -3
  159. agentpool_config/storage.py +38 -39
  160. agentpool_config/task.py +3 -3
  161. agentpool_config/tools.py +11 -28
  162. agentpool_config/toolsets.py +22 -90
  163. agentpool_server/a2a_server/agent_worker.py +307 -0
  164. agentpool_server/a2a_server/server.py +23 -18
  165. agentpool_server/acp_server/acp_agent.py +125 -56
  166. agentpool_server/acp_server/commands/acp_commands.py +46 -216
  167. agentpool_server/acp_server/commands/docs_commands/fetch_repo.py +8 -7
  168. agentpool_server/acp_server/event_converter.py +651 -0
  169. agentpool_server/acp_server/input_provider.py +53 -10
  170. agentpool_server/acp_server/server.py +1 -11
  171. agentpool_server/acp_server/session.py +90 -410
  172. agentpool_server/acp_server/session_manager.py +8 -34
  173. agentpool_server/agui_server/server.py +3 -1
  174. agentpool_server/mcp_server/server.py +5 -2
  175. agentpool_server/opencode_server/ENDPOINTS.md +53 -14
  176. agentpool_server/opencode_server/OPENCODE_UI_TOOLS_COMPLETE.md +202 -0
  177. agentpool_server/opencode_server/__init__.py +0 -8
  178. agentpool_server/opencode_server/converters.py +132 -26
  179. agentpool_server/opencode_server/input_provider.py +160 -8
  180. agentpool_server/opencode_server/models/__init__.py +42 -20
  181. agentpool_server/opencode_server/models/app.py +12 -0
  182. agentpool_server/opencode_server/models/events.py +203 -29
  183. agentpool_server/opencode_server/models/mcp.py +19 -0
  184. agentpool_server/opencode_server/models/message.py +18 -1
  185. agentpool_server/opencode_server/models/parts.py +134 -1
  186. agentpool_server/opencode_server/models/question.py +56 -0
  187. agentpool_server/opencode_server/models/session.py +13 -1
  188. agentpool_server/opencode_server/routes/__init__.py +4 -0
  189. agentpool_server/opencode_server/routes/agent_routes.py +33 -2
  190. agentpool_server/opencode_server/routes/app_routes.py +66 -3
  191. agentpool_server/opencode_server/routes/config_routes.py +66 -5
  192. agentpool_server/opencode_server/routes/file_routes.py +184 -5
  193. agentpool_server/opencode_server/routes/global_routes.py +1 -1
  194. agentpool_server/opencode_server/routes/lsp_routes.py +1 -1
  195. agentpool_server/opencode_server/routes/message_routes.py +122 -66
  196. agentpool_server/opencode_server/routes/permission_routes.py +63 -0
  197. agentpool_server/opencode_server/routes/pty_routes.py +23 -22
  198. agentpool_server/opencode_server/routes/question_routes.py +128 -0
  199. agentpool_server/opencode_server/routes/session_routes.py +139 -68
  200. agentpool_server/opencode_server/routes/tui_routes.py +1 -1
  201. agentpool_server/opencode_server/server.py +47 -2
  202. agentpool_server/opencode_server/state.py +30 -0
  203. agentpool_storage/__init__.py +0 -4
  204. agentpool_storage/base.py +81 -2
  205. agentpool_storage/claude_provider/ARCHITECTURE.md +433 -0
  206. agentpool_storage/claude_provider/__init__.py +42 -0
  207. agentpool_storage/{claude_provider.py → claude_provider/provider.py} +190 -8
  208. agentpool_storage/file_provider.py +149 -15
  209. agentpool_storage/memory_provider.py +132 -12
  210. agentpool_storage/opencode_provider/ARCHITECTURE.md +386 -0
  211. agentpool_storage/opencode_provider/__init__.py +16 -0
  212. agentpool_storage/opencode_provider/helpers.py +414 -0
  213. agentpool_storage/opencode_provider/provider.py +895 -0
  214. agentpool_storage/session_store.py +20 -6
  215. agentpool_storage/sql_provider/sql_provider.py +135 -2
  216. agentpool_storage/sql_provider/utils.py +2 -12
  217. agentpool_storage/zed_provider/__init__.py +16 -0
  218. agentpool_storage/zed_provider/helpers.py +281 -0
  219. agentpool_storage/zed_provider/models.py +130 -0
  220. agentpool_storage/zed_provider/provider.py +442 -0
  221. agentpool_storage/zed_provider.py +803 -0
  222. agentpool_toolsets/__init__.py +0 -2
  223. agentpool_toolsets/builtin/__init__.py +2 -4
  224. agentpool_toolsets/builtin/code.py +4 -4
  225. agentpool_toolsets/builtin/debug.py +115 -40
  226. agentpool_toolsets/builtin/execution_environment.py +54 -165
  227. agentpool_toolsets/builtin/skills.py +0 -77
  228. agentpool_toolsets/builtin/subagent_tools.py +64 -51
  229. agentpool_toolsets/builtin/workers.py +4 -2
  230. agentpool_toolsets/composio_toolset.py +2 -2
  231. agentpool_toolsets/entry_points.py +3 -1
  232. agentpool_toolsets/fsspec_toolset/grep.py +25 -5
  233. agentpool_toolsets/fsspec_toolset/helpers.py +3 -2
  234. agentpool_toolsets/fsspec_toolset/toolset.py +350 -66
  235. agentpool_toolsets/mcp_discovery/data/mcp_servers.parquet +0 -0
  236. agentpool_toolsets/mcp_discovery/toolset.py +74 -17
  237. agentpool_toolsets/mcp_run_toolset.py +8 -11
  238. agentpool_toolsets/notifications.py +33 -33
  239. agentpool_toolsets/openapi.py +3 -1
  240. agentpool_toolsets/search_toolset.py +3 -1
  241. agentpool_config/resources.py +0 -33
  242. agentpool_server/acp_server/acp_tools.py +0 -43
  243. agentpool_server/acp_server/commands/spawn.py +0 -210
  244. agentpool_storage/opencode_provider.py +0 -730
  245. agentpool_storage/text_log_provider.py +0 -276
  246. agentpool_toolsets/builtin/chain.py +0 -288
  247. agentpool_toolsets/builtin/user_interaction.py +0 -52
  248. agentpool_toolsets/semantic_memory_toolset.py +0 -536
  249. {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/entry_points.txt +0 -0
  250. {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 # noqa: TC001
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 # noqa: TC001
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
- _ = state # unused for now
156
- return Config()
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
- return ProvidersResponse(providers=providers)
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 # noqa: TC001
13
- from agentpool_server.opencode_server.models import ( # noqa: TC001
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
- regex = re.compile(pattern)
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 # noqa: TC001
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 # noqa: TC001
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