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.
Files changed (311) hide show
  1. acp/__init__.py +13 -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/bridge/README.md +15 -2
  7. acp/bridge/__init__.py +3 -2
  8. acp/bridge/__main__.py +60 -19
  9. acp/bridge/ws_server.py +173 -0
  10. acp/bridge/ws_server_cli.py +89 -0
  11. acp/client/connection.py +38 -29
  12. acp/client/implementations/default_client.py +3 -2
  13. acp/client/implementations/headless_client.py +2 -2
  14. acp/connection.py +2 -2
  15. acp/notifications.py +20 -50
  16. acp/schema/__init__.py +2 -0
  17. acp/schema/agent_responses.py +21 -0
  18. acp/schema/client_requests.py +3 -3
  19. acp/schema/session_state.py +63 -29
  20. acp/stdio.py +39 -9
  21. acp/task/supervisor.py +2 -2
  22. acp/transports.py +362 -2
  23. acp/utils.py +17 -4
  24. agentpool/__init__.py +6 -1
  25. agentpool/agents/__init__.py +2 -0
  26. agentpool/agents/acp_agent/acp_agent.py +407 -277
  27. agentpool/agents/acp_agent/acp_converters.py +196 -38
  28. agentpool/agents/acp_agent/client_handler.py +191 -26
  29. agentpool/agents/acp_agent/session_state.py +17 -6
  30. agentpool/agents/agent.py +607 -572
  31. agentpool/agents/agui_agent/__init__.py +0 -2
  32. agentpool/agents/agui_agent/agui_agent.py +176 -110
  33. agentpool/agents/agui_agent/agui_converters.py +0 -131
  34. agentpool/agents/agui_agent/helpers.py +3 -4
  35. agentpool/agents/base_agent.py +632 -17
  36. agentpool/agents/claude_code_agent/FORKING.md +191 -0
  37. agentpool/agents/claude_code_agent/__init__.py +13 -1
  38. agentpool/agents/claude_code_agent/claude_code_agent.py +1058 -291
  39. agentpool/agents/claude_code_agent/converters.py +74 -143
  40. agentpool/agents/claude_code_agent/history.py +474 -0
  41. agentpool/agents/claude_code_agent/models.py +77 -0
  42. agentpool/agents/claude_code_agent/static_info.py +100 -0
  43. agentpool/agents/claude_code_agent/usage.py +242 -0
  44. agentpool/agents/context.py +40 -0
  45. agentpool/agents/events/__init__.py +24 -0
  46. agentpool/agents/events/builtin_handlers.py +67 -1
  47. agentpool/agents/events/event_emitter.py +32 -2
  48. agentpool/agents/events/events.py +104 -3
  49. agentpool/agents/events/infer_info.py +145 -0
  50. agentpool/agents/events/processors.py +254 -0
  51. agentpool/agents/interactions.py +41 -6
  52. agentpool/agents/modes.py +67 -0
  53. agentpool/agents/slashed_agent.py +5 -4
  54. agentpool/agents/tool_call_accumulator.py +213 -0
  55. agentpool/agents/tool_wrapping.py +18 -6
  56. agentpool/common_types.py +56 -21
  57. agentpool/config_resources/__init__.py +38 -1
  58. agentpool/config_resources/acp_assistant.yml +2 -2
  59. agentpool/config_resources/agents.yml +3 -0
  60. agentpool/config_resources/agents_template.yml +1 -0
  61. agentpool/config_resources/claude_code_agent.yml +10 -6
  62. agentpool/config_resources/external_acp_agents.yml +2 -1
  63. agentpool/delegation/base_team.py +4 -30
  64. agentpool/delegation/pool.py +136 -289
  65. agentpool/delegation/team.py +58 -57
  66. agentpool/delegation/teamrun.py +51 -55
  67. agentpool/diagnostics/__init__.py +53 -0
  68. agentpool/diagnostics/lsp_manager.py +1593 -0
  69. agentpool/diagnostics/lsp_proxy.py +41 -0
  70. agentpool/diagnostics/lsp_proxy_script.py +229 -0
  71. agentpool/diagnostics/models.py +398 -0
  72. agentpool/functional/run.py +10 -4
  73. agentpool/mcp_server/__init__.py +0 -2
  74. agentpool/mcp_server/client.py +76 -32
  75. agentpool/mcp_server/conversions.py +54 -13
  76. agentpool/mcp_server/manager.py +34 -54
  77. agentpool/mcp_server/registries/official_registry_client.py +35 -1
  78. agentpool/mcp_server/tool_bridge.py +186 -139
  79. agentpool/messaging/__init__.py +0 -2
  80. agentpool/messaging/compaction.py +72 -197
  81. agentpool/messaging/connection_manager.py +11 -10
  82. agentpool/messaging/event_manager.py +5 -5
  83. agentpool/messaging/message_container.py +6 -30
  84. agentpool/messaging/message_history.py +99 -8
  85. agentpool/messaging/messagenode.py +52 -14
  86. agentpool/messaging/messages.py +54 -35
  87. agentpool/messaging/processing.py +12 -22
  88. agentpool/models/__init__.py +1 -1
  89. agentpool/models/acp_agents/base.py +6 -24
  90. agentpool/models/acp_agents/mcp_capable.py +126 -157
  91. agentpool/models/acp_agents/non_mcp.py +129 -95
  92. agentpool/models/agents.py +98 -76
  93. agentpool/models/agui_agents.py +1 -1
  94. agentpool/models/claude_code_agents.py +144 -19
  95. agentpool/models/file_parsing.py +0 -1
  96. agentpool/models/manifest.py +113 -50
  97. agentpool/prompts/conversion_manager.py +1 -1
  98. agentpool/prompts/prompts.py +5 -2
  99. agentpool/repomap.py +1 -1
  100. agentpool/resource_providers/__init__.py +11 -1
  101. agentpool/resource_providers/aggregating.py +56 -5
  102. agentpool/resource_providers/base.py +70 -4
  103. agentpool/resource_providers/codemode/code_executor.py +72 -5
  104. agentpool/resource_providers/codemode/helpers.py +2 -2
  105. agentpool/resource_providers/codemode/provider.py +64 -12
  106. agentpool/resource_providers/codemode/remote_mcp_execution.py +2 -2
  107. agentpool/resource_providers/codemode/remote_provider.py +9 -12
  108. agentpool/resource_providers/filtering.py +3 -1
  109. agentpool/resource_providers/mcp_provider.py +89 -12
  110. agentpool/resource_providers/plan_provider.py +228 -46
  111. agentpool/resource_providers/pool.py +7 -3
  112. agentpool/resource_providers/resource_info.py +111 -0
  113. agentpool/resource_providers/static.py +4 -2
  114. agentpool/sessions/__init__.py +4 -1
  115. agentpool/sessions/manager.py +33 -5
  116. agentpool/sessions/models.py +59 -6
  117. agentpool/sessions/protocol.py +28 -0
  118. agentpool/sessions/session.py +11 -55
  119. agentpool/skills/registry.py +13 -8
  120. agentpool/storage/manager.py +572 -49
  121. agentpool/talk/registry.py +4 -4
  122. agentpool/talk/talk.py +9 -10
  123. agentpool/testing.py +538 -20
  124. agentpool/tool_impls/__init__.py +6 -0
  125. agentpool/tool_impls/agent_cli/__init__.py +42 -0
  126. agentpool/tool_impls/agent_cli/tool.py +95 -0
  127. agentpool/tool_impls/bash/__init__.py +64 -0
  128. agentpool/tool_impls/bash/helpers.py +35 -0
  129. agentpool/tool_impls/bash/tool.py +171 -0
  130. agentpool/tool_impls/delete_path/__init__.py +70 -0
  131. agentpool/tool_impls/delete_path/tool.py +142 -0
  132. agentpool/tool_impls/download_file/__init__.py +80 -0
  133. agentpool/tool_impls/download_file/tool.py +183 -0
  134. agentpool/tool_impls/execute_code/__init__.py +55 -0
  135. agentpool/tool_impls/execute_code/tool.py +163 -0
  136. agentpool/tool_impls/grep/__init__.py +80 -0
  137. agentpool/tool_impls/grep/tool.py +200 -0
  138. agentpool/tool_impls/list_directory/__init__.py +73 -0
  139. agentpool/tool_impls/list_directory/tool.py +197 -0
  140. agentpool/tool_impls/question/__init__.py +42 -0
  141. agentpool/tool_impls/question/tool.py +127 -0
  142. agentpool/tool_impls/read/__init__.py +104 -0
  143. agentpool/tool_impls/read/tool.py +305 -0
  144. agentpool/tools/__init__.py +2 -1
  145. agentpool/tools/base.py +114 -34
  146. agentpool/tools/manager.py +57 -1
  147. agentpool/ui/base.py +2 -2
  148. agentpool/ui/mock_provider.py +2 -2
  149. agentpool/ui/stdlib_provider.py +2 -2
  150. agentpool/utils/file_watcher.py +269 -0
  151. agentpool/utils/identifiers.py +121 -0
  152. agentpool/utils/pydantic_ai_helpers.py +46 -0
  153. agentpool/utils/streams.py +616 -2
  154. agentpool/utils/subprocess_utils.py +155 -0
  155. agentpool/utils/token_breakdown.py +461 -0
  156. agentpool/vfs_registry.py +7 -2
  157. {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/METADATA +41 -27
  158. agentpool-2.5.0.dist-info/RECORD +579 -0
  159. {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/WHEEL +1 -1
  160. agentpool_cli/__main__.py +24 -0
  161. agentpool_cli/create.py +1 -1
  162. agentpool_cli/serve_acp.py +100 -21
  163. agentpool_cli/serve_agui.py +87 -0
  164. agentpool_cli/serve_opencode.py +119 -0
  165. agentpool_cli/ui.py +557 -0
  166. agentpool_commands/__init__.py +42 -5
  167. agentpool_commands/agents.py +75 -2
  168. agentpool_commands/history.py +62 -0
  169. agentpool_commands/mcp.py +176 -0
  170. agentpool_commands/models.py +56 -3
  171. agentpool_commands/pool.py +260 -0
  172. agentpool_commands/session.py +1 -1
  173. agentpool_commands/text_sharing/__init__.py +119 -0
  174. agentpool_commands/text_sharing/base.py +123 -0
  175. agentpool_commands/text_sharing/github_gist.py +80 -0
  176. agentpool_commands/text_sharing/opencode.py +462 -0
  177. agentpool_commands/text_sharing/paste_rs.py +59 -0
  178. agentpool_commands/text_sharing/pastebin.py +116 -0
  179. agentpool_commands/text_sharing/shittycodingagent.py +112 -0
  180. agentpool_commands/tools.py +57 -0
  181. agentpool_commands/utils.py +80 -30
  182. agentpool_config/__init__.py +30 -2
  183. agentpool_config/agentpool_tools.py +498 -0
  184. agentpool_config/builtin_tools.py +77 -22
  185. agentpool_config/commands.py +24 -1
  186. agentpool_config/compaction.py +258 -0
  187. agentpool_config/converters.py +1 -1
  188. agentpool_config/event_handlers.py +42 -0
  189. agentpool_config/events.py +1 -1
  190. agentpool_config/forward_targets.py +1 -4
  191. agentpool_config/jinja.py +3 -3
  192. agentpool_config/mcp_server.py +132 -6
  193. agentpool_config/nodes.py +1 -1
  194. agentpool_config/observability.py +44 -0
  195. agentpool_config/session.py +0 -3
  196. agentpool_config/storage.py +82 -38
  197. agentpool_config/task.py +3 -3
  198. agentpool_config/tools.py +11 -22
  199. agentpool_config/toolsets.py +109 -233
  200. agentpool_server/a2a_server/agent_worker.py +307 -0
  201. agentpool_server/a2a_server/server.py +23 -18
  202. agentpool_server/acp_server/acp_agent.py +234 -181
  203. agentpool_server/acp_server/commands/acp_commands.py +151 -156
  204. agentpool_server/acp_server/commands/docs_commands/fetch_repo.py +18 -17
  205. agentpool_server/acp_server/event_converter.py +651 -0
  206. agentpool_server/acp_server/input_provider.py +53 -10
  207. agentpool_server/acp_server/server.py +24 -90
  208. agentpool_server/acp_server/session.py +173 -331
  209. agentpool_server/acp_server/session_manager.py +8 -34
  210. agentpool_server/agui_server/server.py +3 -1
  211. agentpool_server/mcp_server/server.py +5 -2
  212. agentpool_server/opencode_server/.rules +95 -0
  213. agentpool_server/opencode_server/ENDPOINTS.md +401 -0
  214. agentpool_server/opencode_server/OPENCODE_UI_TOOLS_COMPLETE.md +202 -0
  215. agentpool_server/opencode_server/__init__.py +19 -0
  216. agentpool_server/opencode_server/command_validation.py +172 -0
  217. agentpool_server/opencode_server/converters.py +975 -0
  218. agentpool_server/opencode_server/dependencies.py +24 -0
  219. agentpool_server/opencode_server/input_provider.py +421 -0
  220. agentpool_server/opencode_server/models/__init__.py +250 -0
  221. agentpool_server/opencode_server/models/agent.py +53 -0
  222. agentpool_server/opencode_server/models/app.py +72 -0
  223. agentpool_server/opencode_server/models/base.py +26 -0
  224. agentpool_server/opencode_server/models/common.py +23 -0
  225. agentpool_server/opencode_server/models/config.py +37 -0
  226. agentpool_server/opencode_server/models/events.py +821 -0
  227. agentpool_server/opencode_server/models/file.py +88 -0
  228. agentpool_server/opencode_server/models/mcp.py +44 -0
  229. agentpool_server/opencode_server/models/message.py +179 -0
  230. agentpool_server/opencode_server/models/parts.py +323 -0
  231. agentpool_server/opencode_server/models/provider.py +81 -0
  232. agentpool_server/opencode_server/models/pty.py +43 -0
  233. agentpool_server/opencode_server/models/question.py +56 -0
  234. agentpool_server/opencode_server/models/session.py +111 -0
  235. agentpool_server/opencode_server/routes/__init__.py +29 -0
  236. agentpool_server/opencode_server/routes/agent_routes.py +473 -0
  237. agentpool_server/opencode_server/routes/app_routes.py +202 -0
  238. agentpool_server/opencode_server/routes/config_routes.py +302 -0
  239. agentpool_server/opencode_server/routes/file_routes.py +571 -0
  240. agentpool_server/opencode_server/routes/global_routes.py +94 -0
  241. agentpool_server/opencode_server/routes/lsp_routes.py +319 -0
  242. agentpool_server/opencode_server/routes/message_routes.py +761 -0
  243. agentpool_server/opencode_server/routes/permission_routes.py +63 -0
  244. agentpool_server/opencode_server/routes/pty_routes.py +300 -0
  245. agentpool_server/opencode_server/routes/question_routes.py +128 -0
  246. agentpool_server/opencode_server/routes/session_routes.py +1276 -0
  247. agentpool_server/opencode_server/routes/tui_routes.py +139 -0
  248. agentpool_server/opencode_server/server.py +475 -0
  249. agentpool_server/opencode_server/state.py +151 -0
  250. agentpool_server/opencode_server/time_utils.py +8 -0
  251. agentpool_storage/__init__.py +12 -0
  252. agentpool_storage/base.py +184 -2
  253. agentpool_storage/claude_provider/ARCHITECTURE.md +433 -0
  254. agentpool_storage/claude_provider/__init__.py +42 -0
  255. agentpool_storage/claude_provider/provider.py +1089 -0
  256. agentpool_storage/file_provider.py +278 -15
  257. agentpool_storage/memory_provider.py +193 -12
  258. agentpool_storage/models.py +3 -0
  259. agentpool_storage/opencode_provider/ARCHITECTURE.md +386 -0
  260. agentpool_storage/opencode_provider/__init__.py +16 -0
  261. agentpool_storage/opencode_provider/helpers.py +414 -0
  262. agentpool_storage/opencode_provider/provider.py +895 -0
  263. agentpool_storage/project_store.py +325 -0
  264. agentpool_storage/session_store.py +26 -6
  265. agentpool_storage/sql_provider/__init__.py +4 -2
  266. agentpool_storage/sql_provider/models.py +48 -0
  267. agentpool_storage/sql_provider/sql_provider.py +269 -3
  268. agentpool_storage/sql_provider/utils.py +12 -13
  269. agentpool_storage/zed_provider/__init__.py +16 -0
  270. agentpool_storage/zed_provider/helpers.py +281 -0
  271. agentpool_storage/zed_provider/models.py +130 -0
  272. agentpool_storage/zed_provider/provider.py +442 -0
  273. agentpool_storage/zed_provider.py +803 -0
  274. agentpool_toolsets/__init__.py +0 -2
  275. agentpool_toolsets/builtin/__init__.py +2 -12
  276. agentpool_toolsets/builtin/code.py +96 -57
  277. agentpool_toolsets/builtin/debug.py +118 -48
  278. agentpool_toolsets/builtin/execution_environment.py +115 -230
  279. agentpool_toolsets/builtin/file_edit/file_edit.py +115 -7
  280. agentpool_toolsets/builtin/skills.py +9 -4
  281. agentpool_toolsets/builtin/subagent_tools.py +64 -51
  282. agentpool_toolsets/builtin/workers.py +4 -2
  283. agentpool_toolsets/composio_toolset.py +2 -2
  284. agentpool_toolsets/entry_points.py +3 -1
  285. agentpool_toolsets/fsspec_toolset/__init__.py +13 -1
  286. agentpool_toolsets/fsspec_toolset/diagnostics.py +860 -73
  287. agentpool_toolsets/fsspec_toolset/grep.py +99 -7
  288. agentpool_toolsets/fsspec_toolset/helpers.py +3 -2
  289. agentpool_toolsets/fsspec_toolset/image_utils.py +161 -0
  290. agentpool_toolsets/fsspec_toolset/toolset.py +500 -95
  291. agentpool_toolsets/mcp_discovery/__init__.py +5 -0
  292. agentpool_toolsets/mcp_discovery/data/mcp_servers.parquet +0 -0
  293. agentpool_toolsets/mcp_discovery/toolset.py +511 -0
  294. agentpool_toolsets/mcp_run_toolset.py +87 -12
  295. agentpool_toolsets/notifications.py +33 -33
  296. agentpool_toolsets/openapi.py +3 -1
  297. agentpool_toolsets/search_toolset.py +3 -1
  298. agentpool-2.1.9.dist-info/RECORD +0 -474
  299. agentpool_config/resources.py +0 -33
  300. agentpool_server/acp_server/acp_tools.py +0 -43
  301. agentpool_server/acp_server/commands/spawn.py +0 -210
  302. agentpool_storage/text_log_provider.py +0 -275
  303. agentpool_toolsets/builtin/agent_management.py +0 -239
  304. agentpool_toolsets/builtin/chain.py +0 -288
  305. agentpool_toolsets/builtin/history.py +0 -36
  306. agentpool_toolsets/builtin/integration.py +0 -85
  307. agentpool_toolsets/builtin/tool_management.py +0 -90
  308. agentpool_toolsets/builtin/user_interaction.py +0 -52
  309. agentpool_toolsets/semantic_memory_toolset.py +0 -536
  310. {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/entry_points.txt +0 -0
  311. {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,11 +1,9 @@
1
- """Provider for execution environment tools with event emission."""
1
+ """Provider for process management tools with event emission."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import TYPE_CHECKING, Any
6
- import uuid
7
-
8
- from exxec.events import OutputEvent, ProcessCompletedEvent, ProcessErrorEvent, ProcessStartedEvent
5
+ import re
6
+ from typing import TYPE_CHECKING
9
7
 
10
8
  from agentpool import log
11
9
  from agentpool.agents.context import AgentContext # noqa: TC001
@@ -16,26 +14,29 @@ logger = log.get_logger(__name__)
16
14
 
17
15
 
18
16
  if TYPE_CHECKING:
17
+ from collections.abc import Sequence
18
+
19
19
  from exxec import ExecutionEnvironment
20
20
 
21
21
  from agentpool.tools.base import Tool
22
22
 
23
23
 
24
- class ExecutionEnvironmentTools(ResourceProvider):
25
- """Provider for execution environment tools.
24
+ class ProcessManagementTools(ResourceProvider):
25
+ """Provider for background process management tools.
26
26
 
27
- Combines code execution and process management capabilities
28
- using any ExecutionEnvironment backend. Emits events via AgentContext.
27
+ Provides tools for starting, monitoring, and controlling background processes.
28
+ Uses any ExecutionEnvironment backend and emits events via AgentContext.
29
29
 
30
- NOTE: The ACP execution environment used handles the Terminal events of the protocol,
31
- the toolset should deal with the ToolCall events for UI display purposes.
30
+ For code execution and bash commands, use the standalone tools:
31
+ - agentpool.tool_impls.bash.BashTool
32
+ - agentpool.tool_impls.execute_code.ExecuteCodeTool
32
33
  """
33
34
 
34
- def __init__(self, env: ExecutionEnvironment | None = None, name: str = "execution") -> None:
35
- """Initialize execution environment toolset.
35
+ def __init__(self, env: ExecutionEnvironment | None = None, name: str = "process") -> None:
36
+ """Initialize process management toolset.
36
37
 
37
38
  Args:
38
- env: Execution environment to use (defaults to LocalExecutionEnvironment)
39
+ env: Execution environment to use (defaults to agent.env)
39
40
  name: The name of the toolset
40
41
  """
41
42
  super().__init__(name=name)
@@ -51,12 +52,8 @@ class ExecutionEnvironmentTools(ResourceProvider):
51
52
  return self._env
52
53
  return agent_ctx.agent.env
53
54
 
54
- async def get_tools(self) -> list[Tool]:
55
+ async def get_tools(self) -> Sequence[Tool]:
55
56
  return [
56
- # Code execution tools
57
- self.create_tool(self.execute_code, category="execute"),
58
- self.create_tool(self.execute_command, category="execute", open_world=True),
59
- # Process management tools
60
57
  self.create_tool(self.start_process, category="execute", open_world=True),
61
58
  self.create_tool(
62
59
  self.get_process_output, category="execute", read_only=True, idempotent=True
@@ -71,142 +68,6 @@ class ExecutionEnvironmentTools(ResourceProvider):
71
68
  ),
72
69
  ]
73
70
 
74
- async def execute_code(self, agent_ctx: AgentContext, code: str) -> dict[str, Any]: # noqa: D417
75
- """Execute Python code and return the result.
76
-
77
- Args:
78
- code: Python code to execute
79
- """
80
- process_id: str | None = None
81
- output_parts: list[str] = []
82
- exit_code: int | None = None
83
- error_msg: str | None = None
84
- duration: float | None = None
85
- try:
86
- async for event in self.get_env(agent_ctx).stream_code(code):
87
- match event:
88
- case ProcessStartedEvent(process_id=pid, command=cmd):
89
- process_id = pid # save for later on.
90
- await agent_ctx.events.process_started(pid, cmd, success=True)
91
- case OutputEvent(data=data):
92
- output_parts.append(data)
93
- if process_id:
94
- await agent_ctx.events.process_output(process_id, data)
95
- case ProcessCompletedEvent(exit_code=code_, duration=dur):
96
- exit_code = code_
97
- duration = dur
98
- out = "".join(output_parts)
99
- if process_id:
100
- await agent_ctx.events.process_exit(
101
- process_id, exit_code, final_output=out
102
- )
103
- case ProcessErrorEvent(error=err, exit_code=code_):
104
- error_msg = err
105
- exit_code = code_
106
- if process_id:
107
- await agent_ctx.events.process_exit(
108
- process_id, exit_code or 1, final_output=err
109
- )
110
-
111
- combined_output = "".join(output_parts)
112
- if error_msg:
113
- return {"error": error_msg, "output": combined_output, "exit_code": exit_code}
114
-
115
- except Exception as e: # noqa: BLE001
116
- error_id = process_id or f"code_{uuid.uuid4().hex[:8]}"
117
- await agent_ctx.events.process_started(
118
- error_id, "execute_code", success=False, error=str(e)
119
- )
120
- return {"error": f"Error executing code: {e}"}
121
- else:
122
- return {"output": combined_output, "exit_code": exit_code, "duration": duration}
123
-
124
- async def execute_command( # noqa: PLR0915, D417
125
- self,
126
- agent_ctx: AgentContext,
127
- command: str,
128
- output_limit: int | None = None,
129
- ) -> dict[str, Any]:
130
- """Execute a shell command and return the output.
131
-
132
- Args:
133
- command: Shell command to execute
134
- output_limit: Maximum bytes of output to return
135
- """
136
- # process_id comes from exxec events (is terminal_id when using ACP)
137
- process_id: str | None = None
138
- stdout_parts: list[str] = []
139
- stderr_parts: list[str] = []
140
- exit_code: int | None = None
141
- error_msg: str | None = None
142
- duration: float | None = None
143
- try:
144
- async for event in self.get_env(agent_ctx).stream_command(command):
145
- match event:
146
- case ProcessStartedEvent(process_id=pid, command=cmd):
147
- process_id = pid
148
- if pid:
149
- await agent_ctx.events.process_started(pid, cmd, success=True)
150
- else:
151
- logger.warning("ProcessStartedEvent missing process_id", command=cmd)
152
- case OutputEvent(process_id=pid, data=data, stream=stream):
153
- if stream == "stderr":
154
- stderr_parts.append(data)
155
- else:
156
- stdout_parts.append(data)
157
- if pid:
158
- await agent_ctx.events.process_output(pid, data)
159
- else:
160
- logger.warning("OutputEvent missing process_id", stream=stream)
161
- case ProcessCompletedEvent(process_id=pid, exit_code=code_, duration=dur):
162
- exit_code = code_
163
- duration = dur
164
- combined = "".join(stdout_parts) + "".join(stderr_parts)
165
- if pid:
166
- await agent_ctx.events.process_exit(
167
- pid, exit_code, final_output=combined
168
- )
169
- else:
170
- msg = "ProcessCompletedEvent missing process_id,"
171
- logger.warning(msg, exit_code=code_)
172
- case ProcessErrorEvent(process_id=pid, error=err, exit_code=code_):
173
- error_msg = err
174
- exit_code = code_
175
-
176
- stdout = "".join(stdout_parts)
177
- stderr = "".join(stderr_parts)
178
- # Apply output limit if specified
179
- truncated = False
180
- if output_limit:
181
- if len(stdout.encode()) > output_limit:
182
- out = stdout.encode()[-output_limit:].decode(errors="ignore")
183
- stdout = "...[truncated]\n" + out
184
- truncated = True
185
- if len(stderr.encode()) > output_limit:
186
- out = stderr.encode()[-output_limit:].decode(errors="ignore")
187
- stderr = "...[truncated]\n" + out
188
- truncated = True
189
- if error_msg:
190
- return {
191
- "error": error_msg,
192
- "stdout": stdout,
193
- "stderr": stderr,
194
- "exit_code": exit_code,
195
- }
196
- except Exception as e: # noqa: BLE001
197
- # Use process_id from events if available, otherwise generate fallback
198
- error_id = process_id or f"cmd_{uuid.uuid4().hex[:8]}"
199
- await agent_ctx.events.process_started(error_id, command, success=False, error=str(e))
200
- return {"success": False, "error": f"Error executing command: {e}"}
201
- else:
202
- return {
203
- "stdout": stdout,
204
- "stderr": stderr,
205
- "exit_code": exit_code,
206
- "duration": duration,
207
- "truncated": truncated,
208
- }
209
-
210
71
  async def start_process( # noqa: D417
211
72
  self,
212
73
  agent_ctx: AgentContext,
@@ -215,7 +76,7 @@ class ExecutionEnvironmentTools(ResourceProvider):
215
76
  cwd: str | None = None,
216
77
  env: dict[str, str] | None = None,
217
78
  output_limit: int | None = None,
218
- ) -> dict[str, Any]:
79
+ ) -> str:
219
80
  """Start a command in the background and return process ID.
220
81
 
221
82
  Args:
@@ -238,73 +99,109 @@ class ExecutionEnvironmentTools(ResourceProvider):
238
99
 
239
100
  except Exception as e: # noqa: BLE001
240
101
  await agent_ctx.events.process_started("", command, success=False, error=str(e))
241
- return {"error": f"Failed to start process: {e}"}
102
+ return f"Failed to start process: {e}"
242
103
  else:
243
- return {
244
- "process_id": process_id,
245
- "command": command,
246
- "args": args or [],
247
- "cwd": cwd,
248
- "status": "started",
249
- }
250
-
251
- async def get_process_output(self, agent_ctx: AgentContext, process_id: str) -> dict[str, Any]: # noqa: D417
104
+ full_cmd = f"{command} {' '.join(args)}" if args else command
105
+ return f"Started background process {process_id}\nCommand: {full_cmd}"
106
+
107
+ async def get_process_output( # noqa: D417
108
+ self,
109
+ agent_ctx: AgentContext,
110
+ process_id: str,
111
+ filter_lines: str | None = None,
112
+ ) -> str:
252
113
  """Get current output from a background process.
253
114
 
254
115
  Args:
255
116
  process_id: Process identifier from start_process
117
+ filter_lines: Optional regex pattern to filter output lines
118
+ (only matching lines returned)
256
119
  """
257
120
  manager = self.get_env(agent_ctx).process_manager
258
121
  try:
259
122
  output = await manager.get_output(process_id)
260
123
  await agent_ctx.events.process_output(process_id, output.combined or "")
261
- result: dict[str, Any] = {
262
- "process_id": process_id,
263
- "stdout": output.stdout or "",
264
- "stderr": output.stderr or "",
265
- "combined": output.combined or "",
266
- "truncated": output.truncated,
267
- }
124
+
125
+ combined = output.combined or ""
126
+
127
+ # Apply regex filter if specified
128
+ if filter_lines and combined:
129
+ try:
130
+ pattern = re.compile(filter_lines)
131
+ filtered_lines = [
132
+ line for line in combined.splitlines(keepends=True) if pattern.search(line)
133
+ ]
134
+ combined = "".join(filtered_lines)
135
+ except re.error as regex_err:
136
+ return f"Invalid filter regex: {regex_err}"
137
+
138
+ status = "completed" if output.exit_code is not None else "running"
139
+
140
+ # Format as plain text
141
+ suffix_parts = [f"Status: {status}"]
268
142
  if output.exit_code is not None:
269
- result["exit_code"] = output.exit_code
270
- result["status"] = "completed"
271
- else:
272
- result["status"] = "running"
143
+ suffix_parts.append(f"Exit code: {output.exit_code}")
144
+ if output.truncated:
145
+ suffix_parts.append("[output truncated]")
146
+
147
+ return (
148
+ f"{combined}\n\n{' | '.join(suffix_parts)}"
149
+ if combined
150
+ else " | ".join(suffix_parts)
151
+ )
273
152
  except ValueError as e:
274
- return {"error": str(e)}
153
+ return f"Error: {e}"
275
154
  except Exception as e: # noqa: BLE001
276
- return {"error": f"Error getting process output: {e}"}
277
- else:
278
- return result
155
+ return f"Error getting process output: {e}"
279
156
 
280
- async def wait_for_process(self, agent_ctx: AgentContext, process_id: str) -> dict[str, Any]: # noqa: D417
157
+ async def wait_for_process( # noqa: D417
158
+ self,
159
+ agent_ctx: AgentContext,
160
+ process_id: str,
161
+ filter_lines: str | None = None,
162
+ ) -> str:
281
163
  """Wait for background process to complete and return final output.
282
164
 
283
165
  Args:
284
166
  process_id: Process identifier from start_process
167
+ filter_lines: Optional regex pattern to filter output lines
168
+ (only matching lines returned)
285
169
  """
286
170
  manager = self.get_env(agent_ctx).process_manager
287
171
  try:
288
172
  exit_code = await manager.wait_for_exit(process_id)
289
173
  output = await manager.get_output(process_id)
290
174
  await agent_ctx.events.process_exit(process_id, exit_code, final_output=output.combined)
291
-
292
175
  except ValueError as e:
293
- return {"error": str(e)}
176
+ return f"Error: {e}"
294
177
  except Exception as e: # noqa: BLE001
295
- return {"error": f"Error waiting for process: {e}"}
178
+ return f"Error waiting for process: {e}"
296
179
  else:
297
- return {
298
- "process_id": process_id,
299
- "exit_code": exit_code,
300
- "status": "completed",
301
- "stdout": output.stdout or "",
302
- "stderr": output.stderr or "",
303
- "combined": output.combined or "",
304
- "truncated": output.truncated,
305
- }
306
-
307
- async def kill_process(self, agent_ctx: AgentContext, process_id: str) -> dict[str, Any]: # noqa: D417
180
+ combined = output.combined or ""
181
+
182
+ # Apply regex filter if specified
183
+ if filter_lines and combined:
184
+ try:
185
+ pattern = re.compile(filter_lines)
186
+ filtered_lines = [
187
+ line for line in combined.splitlines(keepends=True) if pattern.search(line)
188
+ ]
189
+ combined = "".join(filtered_lines)
190
+ except re.error as regex_err:
191
+ return f"Invalid filter regex: {regex_err}"
192
+
193
+ # Format as plain text
194
+ suffix_parts = []
195
+ if output.truncated:
196
+ suffix_parts.append("[output truncated]")
197
+ if exit_code != 0:
198
+ suffix_parts.append(f"Exit code: {exit_code}")
199
+
200
+ if suffix_parts:
201
+ return f"{combined}\n\n{' | '.join(suffix_parts)}"
202
+ return combined
203
+
204
+ async def kill_process(self, agent_ctx: AgentContext, process_id: str) -> str: # noqa: D417
308
205
  """Terminate a background process.
309
206
 
310
207
  Args:
@@ -315,18 +212,14 @@ class ExecutionEnvironmentTools(ResourceProvider):
315
212
  await agent_ctx.events.process_killed(process_id=process_id, success=True)
316
213
  except ValueError as e:
317
214
  await agent_ctx.events.process_killed(process_id, success=False, error=str(e))
318
- return {"error": str(e)}
215
+ return f"Error: {e}"
319
216
  except Exception as e: # noqa: BLE001
320
217
  await agent_ctx.events.process_killed(process_id, success=False, error=str(e))
321
- return {"error": f"Error killing process: {e}"}
218
+ return f"Error killing process: {e}"
322
219
  else:
323
- return {
324
- "process_id": process_id,
325
- "status": "killed",
326
- "message": f"Process {process_id} has been terminated",
327
- }
220
+ return f"Process {process_id} has been terminated"
328
221
 
329
- async def release_process(self, agent_ctx: AgentContext, process_id: str) -> dict[str, Any]: # noqa: D417
222
+ async def release_process(self, agent_ctx: AgentContext, process_id: str) -> str: # noqa: D417
330
223
  """Release resources for a background process.
331
224
 
332
225
  Args:
@@ -335,47 +228,39 @@ class ExecutionEnvironmentTools(ResourceProvider):
335
228
  try:
336
229
  await self.get_env(agent_ctx).process_manager.release_process(process_id)
337
230
  await agent_ctx.events.process_released(process_id=process_id, success=True)
338
-
339
231
  except ValueError as e:
340
232
  await agent_ctx.events.process_released(process_id, success=False, error=str(e))
341
- return {"error": str(e)}
233
+ return f"Error: {e}"
342
234
  except Exception as e: # noqa: BLE001
343
235
  await agent_ctx.events.process_released(process_id, success=False, error=str(e))
344
- return {"error": f"Error releasing process: {e}"}
236
+ return f"Error releasing process: {e}"
345
237
  else:
346
- return {
347
- "process_id": process_id,
348
- "status": "released",
349
- "message": f"Process {process_id} resources have been released",
350
- }
238
+ return f"Process {process_id} resources have been released"
351
239
 
352
- async def list_processes(self, agent_ctx: AgentContext) -> dict[str, Any]:
240
+ async def list_processes(self, agent_ctx: AgentContext) -> str:
353
241
  """List all active background processes."""
354
242
  env = self.get_env(agent_ctx)
355
243
  try:
356
244
  process_ids = await env.process_manager.list_processes()
357
245
  if not process_ids:
358
- return {"processes": [], "count": 0, "message": "No active processes"}
246
+ return "No active background processes"
359
247
 
360
- processes = []
248
+ lines = [f"Active processes ({len(process_ids)}):"]
361
249
  for process_id in process_ids:
362
250
  try:
363
251
  info = await env.process_manager.get_process_info(process_id)
364
- processes.append({
365
- "process_id": process_id,
366
- "command": info["command"],
367
- "args": info.get("args", []),
368
- "cwd": info.get("cwd"),
369
- "is_running": info.get("is_running", False),
370
- "exit_code": info.get("exit_code"),
371
- "created_at": info.get("created_at"),
372
- })
252
+ command = info["command"]
253
+ args = info.get("args", [])
254
+ full_cmd = f"{command} {' '.join(args)}" if args else command
255
+ status = "running" if info.get("is_running", False) else "stopped"
256
+ exit_code = info.get("exit_code")
257
+ status_str = (
258
+ f"{status}" if exit_code is None else f"{status} (exit {exit_code})"
259
+ )
260
+ lines.append(f" - {process_id}: {full_cmd} [{status_str}]")
373
261
  except Exception as e: # noqa: BLE001
374
- processes.append({
375
- "process_id": process_id,
376
- "error": f"Error getting info: {e}",
377
- })
262
+ lines.append(f" - {process_id}: [error getting info: {e}]")
378
263
 
379
- return {"processes": processes, "count": len(processes)}
264
+ return "\n".join(lines)
380
265
  except Exception as e: # noqa: BLE001
381
- return {"error": f"Error listing processes: {e}"}
266
+ return f"Error listing processes: {e}"
@@ -479,9 +479,22 @@ def _trim_diff(diff_text: str) -> str:
479
479
 
480
480
 
481
481
  def replace_content(
482
- content: str, old_string: str, new_string: str, replace_all: bool = False
482
+ content: str,
483
+ old_string: str,
484
+ new_string: str,
485
+ replace_all: bool = False,
486
+ line_hint: int | None = None,
483
487
  ) -> str:
484
- """Replace content using multiple fallback strategies with detailed error messages."""
488
+ """Replace content using multiple fallback strategies with detailed error messages.
489
+
490
+ Args:
491
+ content: The file content to edit
492
+ old_string: Text to find and replace
493
+ new_string: Replacement text
494
+ replace_all: If True, replace all occurrences
495
+ line_hint: If provided and multiple matches exist, use the match closest to this line.
496
+ Useful for disambiguation after getting a "multiple matches" error.
497
+ """
485
498
  if old_string == new_string:
486
499
  msg = "old_string and new_string must be different"
487
500
  raise ValueError(msg)
@@ -518,6 +531,16 @@ def replace_content(
518
531
  # Check if there are multiple occurrences
519
532
  last_index = content.rfind(search_text)
520
533
  if index != last_index:
534
+ # Multiple occurrences found
535
+ if line_hint is not None:
536
+ # Use line_hint to pick the closest match
537
+ best_index = _find_closest_match(content, search_text, line_hint)
538
+ if best_index is not None:
539
+ return (
540
+ content[:best_index]
541
+ + new_string
542
+ + content[best_index + len(search_text) :]
543
+ )
521
544
  continue # Multiple occurrences, need more context
522
545
 
523
546
  # Single occurrence - replace it
@@ -528,11 +551,9 @@ def replace_content(
528
551
  error_msg = _build_not_found_error(content, old_string)
529
552
  raise ValueError(error_msg)
530
553
 
531
- msg = (
532
- "old_string found multiple times and requires more code context "
533
- "to uniquely identify the intended match"
534
- )
535
- raise ValueError(msg)
554
+ # Multiple matches found - provide helpful error with locations
555
+ error_msg = _build_multiple_matches_error(content, old_string)
556
+ raise ValueError(error_msg)
536
557
 
537
558
 
538
559
  def _find_best_fuzzy_match(
@@ -614,6 +635,93 @@ def _create_unified_diff(text1: str, text2: str) -> str:
614
635
  return result.rstrip()
615
636
 
616
637
 
638
+ def _find_all_match_locations(content: str, search_text: str) -> list[int]:
639
+ """Find all line numbers where search_text starts.
640
+
641
+ Returns 1-based line numbers for each occurrence.
642
+ """
643
+ lines = content.split("\n")
644
+ locations: list[int] = []
645
+
646
+ # For single-line search, find direct matches
647
+ search_lines = search_text.split("\n")
648
+ first_search_line = search_lines[0] if search_lines else search_text
649
+
650
+ for i, line in enumerate(lines):
651
+ if first_search_line in line:
652
+ # Verify full match if multi-line
653
+ if len(search_lines) > 1:
654
+ window = "\n".join(lines[i : i + len(search_lines)])
655
+ if search_text in window:
656
+ locations.append(i + 1) # 1-based
657
+ else:
658
+ locations.append(i + 1) # 1-based
659
+
660
+ return locations
661
+
662
+
663
+ def _find_closest_match(content: str, search_text: str, line_hint: int) -> int | None:
664
+ """Find the occurrence of search_text closest to line_hint.
665
+
666
+ Args:
667
+ content: The file content
668
+ search_text: Text to search for
669
+ line_hint: Target line number (1-based)
670
+
671
+ Returns:
672
+ The character index of the closest match, or None if no matches found.
673
+ """
674
+ matches: list[tuple[int, int]] = [] # (line_number, char_index)
675
+
676
+ # Find all occurrences with their positions
677
+ start = 0
678
+ while True:
679
+ index = content.find(search_text, start)
680
+ if index == -1:
681
+ break
682
+ # Calculate line number for this index
683
+ line_num = content[:index].count("\n") + 1
684
+ matches.append((line_num, index))
685
+ start = index + 1
686
+
687
+ if not matches:
688
+ return None
689
+
690
+ # Find the match closest to line_hint
691
+ closest = min(matches, key=lambda m: abs(m[0] - line_hint))
692
+ return closest[1]
693
+
694
+
695
+ def _build_multiple_matches_error(content: str, old_string: str) -> str:
696
+ """Build a helpful error message when old_string matches multiple locations."""
697
+ locations = _find_all_match_locations(content, old_string)
698
+
699
+ if not locations:
700
+ # Fallback - shouldn't happen but be safe
701
+ return (
702
+ "old_string found multiple times and requires more code context "
703
+ "to uniquely identify the intended match"
704
+ )
705
+
706
+ # Show first few lines of the search text for context
707
+ search_preview = old_string.split("\n")[0][:60]
708
+ if len(old_string.split("\n")[0]) > 60: # noqa: PLR2004
709
+ search_preview += "..."
710
+
711
+ location_str = ", ".join(str(loc) for loc in locations[:5])
712
+ if len(locations) > 5: # noqa: PLR2004
713
+ location_str += f", ... ({len(locations)} total)"
714
+
715
+ error_parts = [
716
+ f"Pattern found at multiple locations (lines: {location_str}).",
717
+ f"\nSearch text starts with: {search_preview!r}",
718
+ "\n\nTo fix, include more surrounding context in old_string to uniquely identify "
719
+ "the target location, or use replace_all=True to replace all occurrences.",
720
+ ]
721
+
722
+ return "".join(error_parts)
723
+
724
+
617
725
  def _build_not_found_error(content: str, old_string: str) -> str:
618
726
  """Build a helpful error message when old_string is not found."""
619
727
  lines = content.split("\n")
@@ -1,4 +1,4 @@
1
- """Provider for skills tools."""
1
+ """Provider for skills and commands tools."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
@@ -60,13 +60,18 @@ async def list_skills(ctx: AgentContext) -> str:
60
60
 
61
61
 
62
62
  class SkillsTools(StaticResourceProvider):
63
- """Provider for Claude Code Skills tools.
63
+ """Provider for skills and commands tools.
64
+
65
+ Provides tools to:
66
+ - Discover and load skills from the pool's skills registry
67
+ - Execute internal commands via the agent's command system
64
68
 
65
- Provides tools to discover and load skills from the pool's skills registry.
66
69
  Skills are discovered from configured directories (e.g., ~/.claude/skills/,
67
70
  .claude/skills/).
68
71
 
69
- The pool manages skill discovery; this toolset just provides access to them.
72
+ Commands provide access to management operations like creating agents,
73
+ managing tools, connecting nodes, etc. Use run_command("/help") to discover
74
+ available commands.
70
75
  """
71
76
 
72
77
  def __init__(self, name: str = "skills") -> None: