agentpool 2.1.9__py3-none-any.whl → 2.2.3__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 (174) hide show
  1. acp/__init__.py +13 -0
  2. acp/bridge/README.md +15 -2
  3. acp/bridge/__init__.py +3 -2
  4. acp/bridge/__main__.py +60 -19
  5. acp/bridge/ws_server.py +173 -0
  6. acp/bridge/ws_server_cli.py +89 -0
  7. acp/notifications.py +2 -1
  8. acp/stdio.py +39 -9
  9. acp/transports.py +362 -2
  10. acp/utils.py +15 -2
  11. agentpool/__init__.py +4 -1
  12. agentpool/agents/__init__.py +2 -0
  13. agentpool/agents/acp_agent/acp_agent.py +203 -88
  14. agentpool/agents/acp_agent/acp_converters.py +46 -21
  15. agentpool/agents/acp_agent/client_handler.py +157 -3
  16. agentpool/agents/acp_agent/session_state.py +4 -1
  17. agentpool/agents/agent.py +314 -107
  18. agentpool/agents/agui_agent/__init__.py +0 -2
  19. agentpool/agents/agui_agent/agui_agent.py +90 -21
  20. agentpool/agents/agui_agent/agui_converters.py +0 -131
  21. agentpool/agents/base_agent.py +163 -1
  22. agentpool/agents/claude_code_agent/claude_code_agent.py +626 -179
  23. agentpool/agents/claude_code_agent/converters.py +71 -3
  24. agentpool/agents/claude_code_agent/history.py +474 -0
  25. agentpool/agents/context.py +40 -0
  26. agentpool/agents/events/__init__.py +2 -0
  27. agentpool/agents/events/builtin_handlers.py +2 -1
  28. agentpool/agents/events/event_emitter.py +29 -2
  29. agentpool/agents/events/events.py +20 -0
  30. agentpool/agents/modes.py +54 -0
  31. agentpool/agents/tool_call_accumulator.py +213 -0
  32. agentpool/common_types.py +21 -0
  33. agentpool/config_resources/__init__.py +38 -1
  34. agentpool/config_resources/claude_code_agent.yml +3 -0
  35. agentpool/delegation/pool.py +37 -29
  36. agentpool/delegation/team.py +1 -0
  37. agentpool/delegation/teamrun.py +1 -0
  38. agentpool/diagnostics/__init__.py +53 -0
  39. agentpool/diagnostics/lsp_manager.py +1593 -0
  40. agentpool/diagnostics/lsp_proxy.py +41 -0
  41. agentpool/diagnostics/lsp_proxy_script.py +229 -0
  42. agentpool/diagnostics/models.py +398 -0
  43. agentpool/mcp_server/__init__.py +0 -2
  44. agentpool/mcp_server/client.py +12 -3
  45. agentpool/mcp_server/manager.py +25 -31
  46. agentpool/mcp_server/registries/official_registry_client.py +25 -0
  47. agentpool/mcp_server/tool_bridge.py +78 -66
  48. agentpool/messaging/__init__.py +0 -2
  49. agentpool/messaging/compaction.py +72 -197
  50. agentpool/messaging/message_history.py +12 -0
  51. agentpool/messaging/messages.py +52 -9
  52. agentpool/messaging/processing.py +3 -1
  53. agentpool/models/acp_agents/base.py +0 -22
  54. agentpool/models/acp_agents/mcp_capable.py +8 -148
  55. agentpool/models/acp_agents/non_mcp.py +129 -72
  56. agentpool/models/agents.py +35 -13
  57. agentpool/models/claude_code_agents.py +33 -2
  58. agentpool/models/manifest.py +43 -0
  59. agentpool/repomap.py +1 -1
  60. agentpool/resource_providers/__init__.py +9 -1
  61. agentpool/resource_providers/aggregating.py +52 -3
  62. agentpool/resource_providers/base.py +57 -1
  63. agentpool/resource_providers/mcp_provider.py +23 -0
  64. agentpool/resource_providers/plan_provider.py +130 -41
  65. agentpool/resource_providers/pool.py +2 -0
  66. agentpool/resource_providers/static.py +2 -0
  67. agentpool/sessions/__init__.py +2 -1
  68. agentpool/sessions/manager.py +31 -2
  69. agentpool/sessions/models.py +50 -0
  70. agentpool/skills/registry.py +13 -8
  71. agentpool/storage/manager.py +217 -1
  72. agentpool/testing.py +537 -19
  73. agentpool/utils/file_watcher.py +269 -0
  74. agentpool/utils/identifiers.py +121 -0
  75. agentpool/utils/pydantic_ai_helpers.py +46 -0
  76. agentpool/utils/streams.py +690 -1
  77. agentpool/utils/subprocess_utils.py +155 -0
  78. agentpool/utils/token_breakdown.py +461 -0
  79. {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/METADATA +27 -7
  80. {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/RECORD +170 -112
  81. {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/WHEEL +1 -1
  82. agentpool_cli/__main__.py +4 -0
  83. agentpool_cli/serve_acp.py +41 -20
  84. agentpool_cli/serve_agui.py +87 -0
  85. agentpool_cli/serve_opencode.py +119 -0
  86. agentpool_commands/__init__.py +30 -0
  87. agentpool_commands/agents.py +74 -1
  88. agentpool_commands/history.py +62 -0
  89. agentpool_commands/mcp.py +176 -0
  90. agentpool_commands/models.py +56 -3
  91. agentpool_commands/tools.py +57 -0
  92. agentpool_commands/utils.py +51 -0
  93. agentpool_config/builtin_tools.py +77 -22
  94. agentpool_config/commands.py +24 -1
  95. agentpool_config/compaction.py +258 -0
  96. agentpool_config/mcp_server.py +131 -1
  97. agentpool_config/storage.py +46 -1
  98. agentpool_config/tools.py +7 -1
  99. agentpool_config/toolsets.py +92 -148
  100. agentpool_server/acp_server/acp_agent.py +134 -150
  101. agentpool_server/acp_server/commands/acp_commands.py +216 -51
  102. agentpool_server/acp_server/commands/docs_commands/fetch_repo.py +10 -10
  103. agentpool_server/acp_server/server.py +23 -79
  104. agentpool_server/acp_server/session.py +181 -19
  105. agentpool_server/opencode_server/.rules +95 -0
  106. agentpool_server/opencode_server/ENDPOINTS.md +362 -0
  107. agentpool_server/opencode_server/__init__.py +27 -0
  108. agentpool_server/opencode_server/command_validation.py +172 -0
  109. agentpool_server/opencode_server/converters.py +869 -0
  110. agentpool_server/opencode_server/dependencies.py +24 -0
  111. agentpool_server/opencode_server/input_provider.py +269 -0
  112. agentpool_server/opencode_server/models/__init__.py +228 -0
  113. agentpool_server/opencode_server/models/agent.py +53 -0
  114. agentpool_server/opencode_server/models/app.py +60 -0
  115. agentpool_server/opencode_server/models/base.py +26 -0
  116. agentpool_server/opencode_server/models/common.py +23 -0
  117. agentpool_server/opencode_server/models/config.py +37 -0
  118. agentpool_server/opencode_server/models/events.py +647 -0
  119. agentpool_server/opencode_server/models/file.py +88 -0
  120. agentpool_server/opencode_server/models/mcp.py +25 -0
  121. agentpool_server/opencode_server/models/message.py +162 -0
  122. agentpool_server/opencode_server/models/parts.py +190 -0
  123. agentpool_server/opencode_server/models/provider.py +81 -0
  124. agentpool_server/opencode_server/models/pty.py +43 -0
  125. agentpool_server/opencode_server/models/session.py +99 -0
  126. agentpool_server/opencode_server/routes/__init__.py +25 -0
  127. agentpool_server/opencode_server/routes/agent_routes.py +442 -0
  128. agentpool_server/opencode_server/routes/app_routes.py +139 -0
  129. agentpool_server/opencode_server/routes/config_routes.py +241 -0
  130. agentpool_server/opencode_server/routes/file_routes.py +392 -0
  131. agentpool_server/opencode_server/routes/global_routes.py +94 -0
  132. agentpool_server/opencode_server/routes/lsp_routes.py +319 -0
  133. agentpool_server/opencode_server/routes/message_routes.py +705 -0
  134. agentpool_server/opencode_server/routes/pty_routes.py +299 -0
  135. agentpool_server/opencode_server/routes/session_routes.py +1205 -0
  136. agentpool_server/opencode_server/routes/tui_routes.py +139 -0
  137. agentpool_server/opencode_server/server.py +430 -0
  138. agentpool_server/opencode_server/state.py +121 -0
  139. agentpool_server/opencode_server/time_utils.py +8 -0
  140. agentpool_storage/__init__.py +16 -0
  141. agentpool_storage/base.py +103 -0
  142. agentpool_storage/claude_provider.py +907 -0
  143. agentpool_storage/file_provider.py +129 -0
  144. agentpool_storage/memory_provider.py +61 -0
  145. agentpool_storage/models.py +3 -0
  146. agentpool_storage/opencode_provider.py +730 -0
  147. agentpool_storage/project_store.py +325 -0
  148. agentpool_storage/session_store.py +6 -0
  149. agentpool_storage/sql_provider/__init__.py +4 -2
  150. agentpool_storage/sql_provider/models.py +48 -0
  151. agentpool_storage/sql_provider/sql_provider.py +134 -1
  152. agentpool_storage/sql_provider/utils.py +10 -1
  153. agentpool_storage/text_log_provider.py +1 -0
  154. agentpool_toolsets/builtin/__init__.py +0 -8
  155. agentpool_toolsets/builtin/code.py +95 -56
  156. agentpool_toolsets/builtin/debug.py +16 -21
  157. agentpool_toolsets/builtin/execution_environment.py +99 -103
  158. agentpool_toolsets/builtin/file_edit/file_edit.py +115 -7
  159. agentpool_toolsets/builtin/skills.py +86 -4
  160. agentpool_toolsets/fsspec_toolset/__init__.py +13 -1
  161. agentpool_toolsets/fsspec_toolset/diagnostics.py +860 -73
  162. agentpool_toolsets/fsspec_toolset/grep.py +74 -2
  163. agentpool_toolsets/fsspec_toolset/image_utils.py +161 -0
  164. agentpool_toolsets/fsspec_toolset/toolset.py +159 -38
  165. agentpool_toolsets/mcp_discovery/__init__.py +5 -0
  166. agentpool_toolsets/mcp_discovery/data/mcp_servers.parquet +0 -0
  167. agentpool_toolsets/mcp_discovery/toolset.py +454 -0
  168. agentpool_toolsets/mcp_run_toolset.py +84 -6
  169. agentpool_toolsets/builtin/agent_management.py +0 -239
  170. agentpool_toolsets/builtin/history.py +0 -36
  171. agentpool_toolsets/builtin/integration.py +0 -85
  172. agentpool_toolsets/builtin/tool_management.py +0 -90
  173. {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/entry_points.txt +0 -0
  174. {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/licenses/LICENSE +0 -0
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import TYPE_CHECKING, Any
5
+ from typing import TYPE_CHECKING
6
6
  import uuid
7
7
 
8
8
  from exxec.events import OutputEvent, ProcessCompletedEvent, ProcessErrorEvent, ProcessStartedEvent
@@ -55,7 +55,7 @@ class ExecutionEnvironmentTools(ResourceProvider):
55
55
  return [
56
56
  # Code execution tools
57
57
  self.create_tool(self.execute_code, category="execute"),
58
- self.create_tool(self.execute_command, category="execute", open_world=True),
58
+ self.create_tool(self.bash, category="execute", open_world=True),
59
59
  # Process management tools
60
60
  self.create_tool(self.start_process, category="execute", open_world=True),
61
61
  self.create_tool(
@@ -71,7 +71,7 @@ class ExecutionEnvironmentTools(ResourceProvider):
71
71
  ),
72
72
  ]
73
73
 
74
- async def execute_code(self, agent_ctx: AgentContext, code: str) -> dict[str, Any]: # noqa: D417
74
+ async def execute_code(self, agent_ctx: AgentContext, code: str) -> str: # noqa: D417
75
75
  """Execute Python code and return the result.
76
76
 
77
77
  Args:
@@ -81,7 +81,6 @@ class ExecutionEnvironmentTools(ResourceProvider):
81
81
  output_parts: list[str] = []
82
82
  exit_code: int | None = None
83
83
  error_msg: str | None = None
84
- duration: float | None = None
85
84
  try:
86
85
  async for event in self.get_env(agent_ctx).stream_code(code):
87
86
  match event:
@@ -92,9 +91,8 @@ class ExecutionEnvironmentTools(ResourceProvider):
92
91
  output_parts.append(data)
93
92
  if process_id:
94
93
  await agent_ctx.events.process_output(process_id, data)
95
- case ProcessCompletedEvent(exit_code=code_, duration=dur):
94
+ case ProcessCompletedEvent(exit_code=code_):
96
95
  exit_code = code_
97
- duration = dur
98
96
  out = "".join(output_parts)
99
97
  if process_id:
100
98
  await agent_ctx.events.process_exit(
@@ -109,24 +107,29 @@ class ExecutionEnvironmentTools(ResourceProvider):
109
107
  )
110
108
 
111
109
  combined_output = "".join(output_parts)
110
+
111
+ # Format as plain text for LLM
112
112
  if error_msg:
113
- return {"error": error_msg, "output": combined_output, "exit_code": exit_code}
113
+ return f"{combined_output}\n\nError: {error_msg}\nExit code: {exit_code}"
114
114
 
115
115
  except Exception as e: # noqa: BLE001
116
116
  error_id = process_id or f"code_{uuid.uuid4().hex[:8]}"
117
117
  await agent_ctx.events.process_started(
118
118
  error_id, "execute_code", success=False, error=str(e)
119
119
  )
120
- return {"error": f"Error executing code: {e}"}
120
+ return f"Error executing code: {e}"
121
121
  else:
122
- return {"output": combined_output, "exit_code": exit_code, "duration": duration}
122
+ # Return just output if success, add exit code only if non-zero
123
+ if exit_code and exit_code != 0:
124
+ return f"{combined_output}\n\nExit code: {exit_code}"
125
+ return combined_output
123
126
 
124
- async def execute_command( # noqa: PLR0915, D417
127
+ async def bash( # noqa: PLR0915, D417
125
128
  self,
126
129
  agent_ctx: AgentContext,
127
130
  command: str,
128
131
  output_limit: int | None = None,
129
- ) -> dict[str, Any]:
132
+ ) -> str:
130
133
  """Execute a shell command and return the output.
131
134
 
132
135
  Args:
@@ -139,7 +142,6 @@ class ExecutionEnvironmentTools(ResourceProvider):
139
142
  stderr_parts: list[str] = []
140
143
  exit_code: int | None = None
141
144
  error_msg: str | None = None
142
- duration: float | None = None
143
145
  try:
144
146
  async for event in self.get_env(agent_ctx).stream_command(command):
145
147
  match event:
@@ -158,9 +160,8 @@ class ExecutionEnvironmentTools(ResourceProvider):
158
160
  await agent_ctx.events.process_output(pid, data)
159
161
  else:
160
162
  logger.warning("OutputEvent missing process_id", stream=stream)
161
- case ProcessCompletedEvent(process_id=pid, exit_code=code_, duration=dur):
163
+ case ProcessCompletedEvent(process_id=pid, exit_code=code_):
162
164
  exit_code = code_
163
- duration = dur
164
165
  combined = "".join(stdout_parts) + "".join(stderr_parts)
165
166
  if pid:
166
167
  await agent_ctx.events.process_exit(
@@ -175,6 +176,7 @@ class ExecutionEnvironmentTools(ResourceProvider):
175
176
 
176
177
  stdout = "".join(stdout_parts)
177
178
  stderr = "".join(stderr_parts)
179
+
178
180
  # Apply output limit if specified
179
181
  truncated = False
180
182
  if output_limit:
@@ -186,26 +188,33 @@ class ExecutionEnvironmentTools(ResourceProvider):
186
188
  out = stderr.encode()[-output_limit:].decode(errors="ignore")
187
189
  stderr = "...[truncated]\n" + out
188
190
  truncated = True
191
+
192
+ # Format as plain text for LLM
189
193
  if error_msg:
190
- return {
191
- "error": error_msg,
192
- "stdout": stdout,
193
- "stderr": stderr,
194
- "exit_code": exit_code,
195
- }
194
+ output = stdout + stderr if stdout or stderr else ""
195
+ return f"{output}\n\nError: {error_msg}\nExit code: {exit_code}"
196
+
196
197
  except Exception as e: # noqa: BLE001
197
198
  # Use process_id from events if available, otherwise generate fallback
198
199
  error_id = process_id or f"cmd_{uuid.uuid4().hex[:8]}"
199
200
  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
+ return f"Error executing command: {e}"
201
202
  else:
202
- return {
203
- "stdout": stdout,
204
- "stderr": stderr,
205
- "exit_code": exit_code,
206
- "duration": duration,
207
- "truncated": truncated,
208
- }
203
+ # Combine stdout and stderr for output
204
+ output = stdout
205
+ if stderr:
206
+ output = f"{stdout}\n\nSTDERR:\n{stderr}" if stdout else f"STDERR:\n{stderr}"
207
+
208
+ # Add metadata only when relevant
209
+ suffix_parts = []
210
+ if truncated:
211
+ suffix_parts.append("[output truncated]")
212
+ if exit_code and exit_code != 0:
213
+ suffix_parts.append(f"Exit code: {exit_code}")
214
+
215
+ if suffix_parts:
216
+ return f"{output}\n\n{' | '.join(suffix_parts)}"
217
+ return output
209
218
 
210
219
  async def start_process( # noqa: D417
211
220
  self,
@@ -215,7 +224,7 @@ class ExecutionEnvironmentTools(ResourceProvider):
215
224
  cwd: str | None = None,
216
225
  env: dict[str, str] | None = None,
217
226
  output_limit: int | None = None,
218
- ) -> dict[str, Any]:
227
+ ) -> str:
219
228
  """Start a command in the background and return process ID.
220
229
 
221
230
  Args:
@@ -238,17 +247,12 @@ class ExecutionEnvironmentTools(ResourceProvider):
238
247
 
239
248
  except Exception as e: # noqa: BLE001
240
249
  await agent_ctx.events.process_started("", command, success=False, error=str(e))
241
- return {"error": f"Failed to start process: {e}"}
250
+ return f"Failed to start process: {e}"
242
251
  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
252
+ full_cmd = f"{command} {' '.join(args)}" if args else command
253
+ return f"Started background process {process_id}\nCommand: {full_cmd}"
254
+
255
+ async def get_process_output(self, agent_ctx: AgentContext, process_id: str) -> str: # noqa: D417
252
256
  """Get current output from a background process.
253
257
 
254
258
  Args:
@@ -258,26 +262,28 @@ class ExecutionEnvironmentTools(ResourceProvider):
258
262
  try:
259
263
  output = await manager.get_output(process_id)
260
264
  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
- }
265
+
266
+ combined = output.combined or ""
267
+ status = "completed" if output.exit_code is not None else "running"
268
+
269
+ # Format as plain text
270
+ suffix_parts = [f"Status: {status}"]
268
271
  if output.exit_code is not None:
269
- result["exit_code"] = output.exit_code
270
- result["status"] = "completed"
271
- else:
272
- result["status"] = "running"
272
+ suffix_parts.append(f"Exit code: {output.exit_code}")
273
+ if output.truncated:
274
+ suffix_parts.append("[output truncated]")
275
+
276
+ return (
277
+ f"{combined}\n\n{' | '.join(suffix_parts)}"
278
+ if combined
279
+ else " | ".join(suffix_parts)
280
+ )
273
281
  except ValueError as e:
274
- return {"error": str(e)}
282
+ return f"Error: {e}"
275
283
  except Exception as e: # noqa: BLE001
276
- return {"error": f"Error getting process output: {e}"}
277
- else:
278
- return result
284
+ return f"Error getting process output: {e}"
279
285
 
280
- async def wait_for_process(self, agent_ctx: AgentContext, process_id: str) -> dict[str, Any]: # noqa: D417
286
+ async def wait_for_process(self, agent_ctx: AgentContext, process_id: str) -> str: # noqa: D417
281
287
  """Wait for background process to complete and return final output.
282
288
 
283
289
  Args:
@@ -288,23 +294,25 @@ class ExecutionEnvironmentTools(ResourceProvider):
288
294
  exit_code = await manager.wait_for_exit(process_id)
289
295
  output = await manager.get_output(process_id)
290
296
  await agent_ctx.events.process_exit(process_id, exit_code, final_output=output.combined)
291
-
292
297
  except ValueError as e:
293
- return {"error": str(e)}
298
+ return f"Error: {e}"
294
299
  except Exception as e: # noqa: BLE001
295
- return {"error": f"Error waiting for process: {e}"}
300
+ return f"Error waiting for process: {e}"
296
301
  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
302
+ combined = output.combined or ""
303
+
304
+ # Format as plain text
305
+ suffix_parts = []
306
+ if output.truncated:
307
+ suffix_parts.append("[output truncated]")
308
+ if exit_code != 0:
309
+ suffix_parts.append(f"Exit code: {exit_code}")
310
+
311
+ if suffix_parts:
312
+ return f"{combined}\n\n{' | '.join(suffix_parts)}"
313
+ return combined
314
+
315
+ async def kill_process(self, agent_ctx: AgentContext, process_id: str) -> str: # noqa: D417
308
316
  """Terminate a background process.
309
317
 
310
318
  Args:
@@ -315,18 +323,14 @@ class ExecutionEnvironmentTools(ResourceProvider):
315
323
  await agent_ctx.events.process_killed(process_id=process_id, success=True)
316
324
  except ValueError as e:
317
325
  await agent_ctx.events.process_killed(process_id, success=False, error=str(e))
318
- return {"error": str(e)}
326
+ return f"Error: {e}"
319
327
  except Exception as e: # noqa: BLE001
320
328
  await agent_ctx.events.process_killed(process_id, success=False, error=str(e))
321
- return {"error": f"Error killing process: {e}"}
329
+ return f"Error killing process: {e}"
322
330
  else:
323
- return {
324
- "process_id": process_id,
325
- "status": "killed",
326
- "message": f"Process {process_id} has been terminated",
327
- }
331
+ return f"Process {process_id} has been terminated"
328
332
 
329
- async def release_process(self, agent_ctx: AgentContext, process_id: str) -> dict[str, Any]: # noqa: D417
333
+ async def release_process(self, agent_ctx: AgentContext, process_id: str) -> str: # noqa: D417
330
334
  """Release resources for a background process.
331
335
 
332
336
  Args:
@@ -335,47 +339,39 @@ class ExecutionEnvironmentTools(ResourceProvider):
335
339
  try:
336
340
  await self.get_env(agent_ctx).process_manager.release_process(process_id)
337
341
  await agent_ctx.events.process_released(process_id=process_id, success=True)
338
-
339
342
  except ValueError as e:
340
343
  await agent_ctx.events.process_released(process_id, success=False, error=str(e))
341
- return {"error": str(e)}
344
+ return f"Error: {e}"
342
345
  except Exception as e: # noqa: BLE001
343
346
  await agent_ctx.events.process_released(process_id, success=False, error=str(e))
344
- return {"error": f"Error releasing process: {e}"}
347
+ return f"Error releasing process: {e}"
345
348
  else:
346
- return {
347
- "process_id": process_id,
348
- "status": "released",
349
- "message": f"Process {process_id} resources have been released",
350
- }
349
+ return f"Process {process_id} resources have been released"
351
350
 
352
- async def list_processes(self, agent_ctx: AgentContext) -> dict[str, Any]:
351
+ async def list_processes(self, agent_ctx: AgentContext) -> str:
353
352
  """List all active background processes."""
354
353
  env = self.get_env(agent_ctx)
355
354
  try:
356
355
  process_ids = await env.process_manager.list_processes()
357
356
  if not process_ids:
358
- return {"processes": [], "count": 0, "message": "No active processes"}
357
+ return "No active background processes"
359
358
 
360
- processes = []
359
+ lines = [f"Active processes ({len(process_ids)}):"]
361
360
  for process_id in process_ids:
362
361
  try:
363
362
  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
- })
363
+ command = info["command"]
364
+ args = info.get("args", [])
365
+ full_cmd = f"{command} {' '.join(args)}" if args else command
366
+ status = "running" if info.get("is_running", False) else "stopped"
367
+ exit_code = info.get("exit_code")
368
+ status_str = (
369
+ f"{status}" if exit_code is None else f"{status} (exit {exit_code})"
370
+ )
371
+ lines.append(f" - {process_id}: {full_cmd} [{status_str}]")
373
372
  except Exception as e: # noqa: BLE001
374
- processes.append({
375
- "process_id": process_id,
376
- "error": f"Error getting info: {e}",
377
- })
373
+ lines.append(f" - {process_id}: [error getting info: {e}]")
378
374
 
379
- return {"processes": processes, "count": len(processes)}
375
+ return "\n".join(lines)
380
376
  except Exception as e: # noqa: BLE001
381
- return {"error": f"Error listing processes: {e}"}
377
+ 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
 
@@ -59,14 +59,90 @@ async def list_skills(ctx: AgentContext) -> str:
59
59
  return "\n".join(lines)
60
60
 
61
61
 
62
+ class _StringOutputWriter:
63
+ """Output writer that captures output to a string buffer."""
64
+
65
+ def __init__(self) -> None:
66
+ from io import StringIO
67
+
68
+ self._buffer = StringIO()
69
+
70
+ async def print(self, message: str) -> None:
71
+ """Write a message to the buffer."""
72
+ self._buffer.write(message)
73
+ self._buffer.write("\n")
74
+
75
+ def getvalue(self) -> str:
76
+ """Get the captured output."""
77
+ return self._buffer.getvalue()
78
+
79
+
80
+ async def run_command(ctx: AgentContext, command: str) -> str: # noqa: D417
81
+ """Execute an internal command.
82
+
83
+ This provides access to the agent's internal CLI for management operations.
84
+
85
+ IMPORTANT: Before using any command for the first time, call "help <command>" to learn
86
+ the correct syntax and available options. Commands have specific argument orders and
87
+ flags that must be followed exactly.
88
+
89
+ Discovery commands:
90
+ - "help" - list all available commands
91
+ - "help <command>" - get detailed usage for a specific command (ALWAYS do this first!)
92
+
93
+ Command categories:
94
+ - Agent/team management: create-agent, create-team, list-agents
95
+ - Tool management: list-tools, register-tool, enable-tool, disable-tool
96
+ - MCP servers: add-mcp-server, add-remote-mcp-server, list-mcp-servers
97
+ - Connections: connect, disconnect, connections
98
+ - Workers: add-worker, remove-worker, list-workers
99
+
100
+ Args:
101
+ command: The command to execute. Leading slash is optional.
102
+
103
+ Returns:
104
+ Command output or error message
105
+ """
106
+ from slashed import CommandContext
107
+
108
+ if not ctx.agent.command_store:
109
+ return "No command store available"
110
+
111
+ # Remove leading slash if present (slashed expects command name without /)
112
+ cmd = command.lstrip("/")
113
+
114
+ # Create output capture
115
+ output = _StringOutputWriter()
116
+
117
+ # Create CommandContext with output capture and AgentContext as data
118
+ cmd_ctx = CommandContext(
119
+ output=output,
120
+ data=ctx,
121
+ command_store=ctx.agent.command_store,
122
+ )
123
+
124
+ try:
125
+ await ctx.agent.command_store.execute_command(cmd, cmd_ctx)
126
+ result = output.getvalue()
127
+ except Exception as e: # noqa: BLE001
128
+ return f"Command failed: {e}"
129
+ else:
130
+ return result if result else "Command executed successfully."
131
+
132
+
62
133
  class SkillsTools(StaticResourceProvider):
63
- """Provider for Claude Code Skills tools.
134
+ """Provider for skills and commands tools.
135
+
136
+ Provides tools to:
137
+ - Discover and load skills from the pool's skills registry
138
+ - Execute internal commands via the agent's command system
64
139
 
65
- Provides tools to discover and load skills from the pool's skills registry.
66
140
  Skills are discovered from configured directories (e.g., ~/.claude/skills/,
67
141
  .claude/skills/).
68
142
 
69
- The pool manages skill discovery; this toolset just provides access to them.
143
+ Commands provide access to management operations like creating agents,
144
+ managing tools, connecting nodes, etc. Use run_command("/help") to discover
145
+ available commands.
70
146
  """
71
147
 
72
148
  def __init__(self, name: str = "skills") -> None:
@@ -74,4 +150,10 @@ class SkillsTools(StaticResourceProvider):
74
150
  self._tools = [
75
151
  self.create_tool(load_skill, category="read", read_only=True, idempotent=True),
76
152
  self.create_tool(list_skills, category="read", read_only=True, idempotent=True),
153
+ self.create_tool(
154
+ run_command,
155
+ category="other",
156
+ read_only=False,
157
+ idempotent=False,
158
+ ),
77
159
  ]
@@ -2,6 +2,18 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from agentpool_toolsets.fsspec_toolset.diagnostics import (
6
+ DiagnosticsConfig,
7
+ DiagnosticsManager,
8
+ DiagnosticsResult,
9
+ )
10
+ from agentpool_toolsets.fsspec_toolset.image_utils import resize_image_if_needed
5
11
  from agentpool_toolsets.fsspec_toolset.toolset import FSSpecTools
6
12
 
7
- __all__ = ["FSSpecTools"]
13
+ __all__ = [
14
+ "DiagnosticsConfig",
15
+ "DiagnosticsManager",
16
+ "DiagnosticsResult",
17
+ "FSSpecTools",
18
+ "resize_image_if_needed",
19
+ ]