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
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  from dataclasses import dataclass
6
6
  from typing import TYPE_CHECKING, Any, Literal
7
7
 
8
- from psygnal import Signal
8
+ from anyenv.signals import Signal
9
9
 
10
10
  from agentpool.log import get_logger
11
11
  from agentpool.talk.talk import Talk
@@ -64,7 +64,7 @@ class ConnectionRegistry(BaseRegistry[str, Talk]):
64
64
  connections get registered.
65
65
  """
66
66
 
67
- message_flow = Signal(Talk.ConnectionProcessed)
67
+ message_flow = Signal[Talk.ConnectionProcessed]()
68
68
 
69
69
  def __init__(self, *args: Any, **kwargs: Any) -> None:
70
70
  """Initialize registry and connect event handlers."""
@@ -90,9 +90,9 @@ class ConnectionRegistry(BaseRegistry[str, Talk]):
90
90
  new_talk.connection_processed.connect(self._handle_message_flow)
91
91
  logger.debug("Reconnected signal for talk", name=name)
92
92
 
93
- def _handle_message_flow(self, event: Talk.ConnectionProcessed) -> None:
93
+ async def _handle_message_flow(self, event: Talk.ConnectionProcessed) -> None:
94
94
  """Forward message flow to global stream."""
95
- self.message_flow.emit(event)
95
+ await self.message_flow.emit(event)
96
96
 
97
97
  @property
98
98
  def _error_class(self) -> type[ConnectionRegistryError]:
agentpool/talk/talk.py CHANGED
@@ -8,7 +8,7 @@ from contextlib import asynccontextmanager
8
8
  from dataclasses import dataclass, field, replace
9
9
  from typing import TYPE_CHECKING, Any, Self, overload
10
10
 
11
- from psygnal import Signal
11
+ from anyenv.signals import Signal
12
12
 
13
13
  from agentpool.log import get_logger
14
14
  from agentpool.messaging import ChatMessage
@@ -53,11 +53,11 @@ class Talk[TTransmittedData = Any]:
53
53
  timestamp: datetime = field(default_factory=get_now)
54
54
 
55
55
  # Original message "coming in"
56
- message_received = Signal(ChatMessage)
56
+ message_received = Signal[ChatMessage[Any]]()
57
57
  # After any transformation (one for each message, not per target)
58
- message_forwarded = Signal(ChatMessage)
58
+ message_forwarded = Signal[ChatMessage[Any]]()
59
59
  # Comprehensive signal capturing all information about one "message handling process"
60
- connection_processed = Signal(ConnectionProcessed)
60
+ connection_processed = Signal[ConnectionProcessed]()
61
61
 
62
62
  def __init__(
63
63
  self,
@@ -270,7 +270,7 @@ class Talk[TTransmittedData = Any]:
270
270
  )
271
271
  ]
272
272
  # 7. emit connection processed event
273
- self.connection_processed.emit(
273
+ await self.connection_processed.emit(
274
274
  self.ConnectionProcessed(
275
275
  message=processed_message,
276
276
  source=self.source,
@@ -283,7 +283,7 @@ class Talk[TTransmittedData = Any]:
283
283
  if target_list:
284
284
  messages = [*self._stats.messages, processed_message]
285
285
  self._stats = replace(self._stats, messages=messages)
286
- self.message_forwarded.emit(processed_message)
286
+ await self.message_forwarded.emit(processed_message)
287
287
 
288
288
  # 9. Second pass: Actually process for each target
289
289
  responses: list[ChatMessage[Any]] = []
@@ -308,10 +308,9 @@ class Talk[TTransmittedData = Any]:
308
308
 
309
309
  match self.connection_type:
310
310
  case "run":
311
- prompts: list[PromptCompatible] = [message]
312
- if prompt:
313
- prompts.append(prompt)
314
- return await target.run(*prompts)
311
+ # Use run_message to handle ChatMessage routing
312
+ # It extracts content, preserves conversation_id, and applies forwarding
313
+ return await target.run_message(message)
315
314
 
316
315
  case "context":
317
316
  meta = {
agentpool/testing.py CHANGED
@@ -1,38 +1,41 @@
1
- """Testing utilities for end-to-end ACP testing.
1
+ """Testing utilities for end-to-end ACP testing and CI integration.
2
2
 
3
- This module provides a lightweight test harness for running end-to-end tests
4
- against the agentpool ACP server. It uses ACPAgent as the client, connecting
5
- to a agentpool serve-acp subprocess.
3
+ This module provides:
4
+ - A lightweight test harness for running end-to-end tests against the agentpool
5
+ ACP server using ACPAgent as the client
6
+ - GitHub CI integration for programmatically triggering and monitoring workflow runs
6
7
 
7
8
  Example:
8
9
  ```python
10
+ # ACP testing
9
11
  async def test_basic_prompt():
10
12
  async with acp_test_session("tests/fixtures/simple.yml") as agent:
11
13
  result = await agent.run("Say hello")
12
14
  assert result.content
13
15
 
14
- async def test_filesystem_tool():
15
- async with acp_test_session(
16
- "tests/fixtures/with_tools.yml",
17
- file_access=True,
18
- terminal_access=True,
19
- ) as agent:
20
- result = await agent.run("List files in the current directory")
21
- assert "pyproject.toml" in result.content
16
+ # CI testing
17
+ async def test_commit_in_ci():
18
+ result = await run_ci_tests("abc123") # or "HEAD"
19
+ assert result.all_passed
20
+ print(result.summary())
22
21
  ```
23
22
  """
24
23
 
25
24
  from __future__ import annotations
26
25
 
26
+ import asyncio
27
27
  from contextlib import asynccontextmanager
28
+ from dataclasses import dataclass, field
29
+ import json
28
30
  from pathlib import Path
29
- from typing import TYPE_CHECKING, Any
31
+ import subprocess
32
+ from typing import TYPE_CHECKING, Any, Literal
30
33
 
31
34
 
32
35
  if TYPE_CHECKING:
33
36
  from collections.abc import AsyncIterator, Sequence
34
37
 
35
- from evented.configs import EventConfig
38
+ from evented_config import EventConfig
36
39
 
37
40
  from agentpool.agents.acp_agent import ACPAgent
38
41
  from agentpool.common_types import BuiltinEventHandlerType, IndividualEventHandler
@@ -44,7 +47,6 @@ async def acp_test_session(
44
47
  *,
45
48
  file_access: bool = True,
46
49
  terminal_access: bool = True,
47
- providers: list[str] | None = None,
48
50
  debug_messages: bool = False,
49
51
  debug_file: str | None = None,
50
52
  debug_commands: bool = False,
@@ -63,7 +65,6 @@ async def acp_test_session(
63
65
  config: Path to agent configuration YAML file. If None, uses default config.
64
66
  file_access: Enable file system access for agents.
65
67
  terminal_access: Enable terminal access for agents.
66
- providers: Model providers to search for models.
67
68
  debug_messages: Save raw JSON-RPC messages to debug file.
68
69
  debug_file: File path for JSON-RPC debug messages.
69
70
  debug_commands: Enable debug slash commands for testing.
@@ -98,10 +99,6 @@ async def acp_test_session(
98
99
  if not terminal_access:
99
100
  args.append("--no-terminal-access")
100
101
 
101
- if providers:
102
- for provider in providers:
103
- args.extend(["--model-provider", provider])
104
-
105
102
  if debug_messages:
106
103
  args.append("--debug-messages")
107
104
 
@@ -127,3 +124,524 @@ async def acp_test_session(
127
124
  event_handlers=event_handlers,
128
125
  ) as acp_agent:
129
126
  yield acp_agent
127
+
128
+
129
+ # --- GitHub CI Testing ---
130
+
131
+ CheckResult = Literal["success", "failure", "skipped", "cancelled", "pending"]
132
+ OSChoice = Literal["ubuntu-latest", "macos-latest", "windows-latest"]
133
+
134
+
135
+ @dataclass
136
+ class CITestResult:
137
+ """Result of a CI test run."""
138
+
139
+ commit: str
140
+ """The commit SHA that was tested."""
141
+
142
+ run_id: int
143
+ """GitHub Actions run ID."""
144
+
145
+ run_url: str
146
+ """URL to the workflow run."""
147
+
148
+ lint: CheckResult = "pending"
149
+ """Result of ruff check."""
150
+
151
+ format: CheckResult = "pending"
152
+ """Result of ruff format check."""
153
+
154
+ typecheck: CheckResult = "pending"
155
+ """Result of mypy type checking."""
156
+
157
+ test: CheckResult = "pending"
158
+ """Result of pytest."""
159
+
160
+ os: str = "ubuntu-latest"
161
+ """Operating system used for the run."""
162
+
163
+ python_version: str = "3.13"
164
+ """Python version used for the run."""
165
+
166
+ duration_seconds: float = 0.0
167
+ """Total duration of the CI run."""
168
+
169
+ raw_jobs: list[dict[str, Any]] = field(default_factory=list)
170
+ """Raw job data from GitHub API."""
171
+
172
+ failed_logs: str | None = None
173
+ """Logs from failed steps (fetched on demand)."""
174
+
175
+ _repo: str | None = field(default=None, repr=False)
176
+ """Repository for fetching logs."""
177
+
178
+ @property
179
+ def all_passed(self) -> bool:
180
+ """Check if all enabled checks passed (skipped checks are ignored)."""
181
+ return all(
182
+ result in ("success", "skipped")
183
+ for result in [self.lint, self.format, self.typecheck, self.test]
184
+ )
185
+
186
+ @property
187
+ def any_failed(self) -> bool:
188
+ """Check if any check failed."""
189
+ return any(
190
+ result == "failure" for result in [self.lint, self.format, self.typecheck, self.test]
191
+ )
192
+
193
+ def summary(self) -> str:
194
+ """Generate a human-readable summary."""
195
+ status_icons = {
196
+ "success": "✓",
197
+ "failure": "✗",
198
+ "skipped": "○",
199
+ "cancelled": "⊘",
200
+ "pending": "…",
201
+ }
202
+ lines = [
203
+ f"CI Results for {self.commit[:8]}",
204
+ f"Run: {self.run_url}",
205
+ f"OS: {self.os} | Python: {self.python_version}",
206
+ "",
207
+ f" {status_icons[self.lint]} Lint (ruff check): {self.lint}",
208
+ f" {status_icons[self.format]} Format (ruff format): {self.format}",
209
+ f" {status_icons[self.typecheck]} Type check (mypy): {self.typecheck}",
210
+ f" {status_icons[self.test]} Tests (pytest): {self.test}",
211
+ "",
212
+ f"Duration: {self.duration_seconds:.1f}s",
213
+ ]
214
+ return "\n".join(lines)
215
+
216
+ def fetch_failed_logs(self, max_lines: int = 200) -> str:
217
+ """Fetch logs from failed steps.
218
+
219
+ Args:
220
+ max_lines: Maximum number of log lines to return.
221
+
222
+ Returns:
223
+ Log output from failed steps, or empty string if no failures.
224
+ """
225
+ if not self.any_failed:
226
+ return ""
227
+
228
+ repo_args = ["-R", self._repo] if self._repo else []
229
+ try:
230
+ result = subprocess.run(
231
+ ["gh", "run", "view", str(self.run_id), "--log-failed", *repo_args],
232
+ capture_output=True,
233
+ text=True,
234
+ check=True,
235
+ )
236
+ lines = result.stdout.strip().split("\n")
237
+ # Return last N lines (most relevant)
238
+ if len(lines) > max_lines:
239
+ lines = lines[-max_lines:]
240
+ self.failed_logs = "\n".join(lines)
241
+ except subprocess.CalledProcessError:
242
+ return ""
243
+ else:
244
+ return self.failed_logs
245
+
246
+ def get_failure_summary(self, max_lines: int = 50) -> str:
247
+ """Get a concise summary of failures.
248
+
249
+ Returns:
250
+ Summary including the test/check that failed and key error lines.
251
+ """
252
+ logs = self.fetch_failed_logs(max_lines=max_lines * 2)
253
+ if not logs:
254
+ return "No failure logs available."
255
+
256
+ # Extract key lines (errors, failures, assertions)
257
+ key_patterns = ["FAILED", "Error", "error:", "AssertionError", "Timeout", "Exception"]
258
+ key_lines = []
259
+ for line in logs.split("\n"):
260
+ if any(p in line for p in key_patterns):
261
+ # Clean up the line (remove timestamp prefix)
262
+ parts = line.split("\t")
263
+ if len(parts) >= 3: # noqa: PLR2004
264
+ key_lines.append(parts[-1].strip())
265
+ else:
266
+ key_lines.append(line.strip())
267
+
268
+ if key_lines:
269
+ return "\n".join(key_lines[:max_lines])
270
+ # Fall back to last N lines
271
+ return "\n".join(logs.split("\n")[-max_lines:])
272
+
273
+
274
+ def _run_gh(*args: str) -> str:
275
+ """Run a gh CLI command and return output."""
276
+ result = subprocess.run(
277
+ ["gh", *args],
278
+ capture_output=True,
279
+ text=True,
280
+ check=True,
281
+ )
282
+ return result.stdout.strip()
283
+
284
+
285
+ def _resolve_commit(commit: str) -> str:
286
+ """Resolve a commit reference to a full SHA."""
287
+ if commit.upper() == "HEAD":
288
+ result = subprocess.run(
289
+ ["git", "rev-parse", "HEAD"],
290
+ capture_output=True,
291
+ text=True,
292
+ check=True,
293
+ )
294
+ return result.stdout.strip()
295
+ return commit
296
+
297
+
298
+ async def run_ci_tests(
299
+ commit: str = "HEAD",
300
+ *,
301
+ repo: str | None = None,
302
+ poll_interval: float = 10.0,
303
+ timeout: float = 600.0,
304
+ os: OSChoice = "ubuntu-latest",
305
+ python_version: str = "3.13",
306
+ run_lint: bool = True,
307
+ run_format: bool = True,
308
+ run_typecheck: bool = True,
309
+ test_command: str | None = "pytest --tb=short",
310
+ ) -> CITestResult:
311
+ """Trigger CI tests for a commit and wait for results.
312
+
313
+ This function triggers the test-commit.yml workflow via the GitHub CLI,
314
+ polls for completion, and returns structured results.
315
+
316
+ Args:
317
+ commit: Commit SHA or "HEAD" to test. Defaults to HEAD.
318
+ repo: Repository in "owner/repo" format. Auto-detected if None.
319
+ poll_interval: Seconds between status checks. Defaults to 10.
320
+ timeout: Maximum seconds to wait for completion. Defaults to 600 (10 min).
321
+ os: Operating system to run on. Defaults to "ubuntu-latest".
322
+ python_version: Python version to use. Defaults to "3.13".
323
+ run_lint: Whether to run ruff check. Defaults to True.
324
+ run_format: Whether to run ruff format check. Defaults to True.
325
+ run_typecheck: Whether to run mypy type checking. Defaults to True.
326
+ test_command: Pytest command to run, or None to skip tests.
327
+ Defaults to "pytest --tb=short". Use "-k pattern" to filter tests.
328
+
329
+ Returns:
330
+ CITestResult with individual check results.
331
+
332
+ Raises:
333
+ TimeoutError: If the workflow doesn't complete within timeout.
334
+ subprocess.CalledProcessError: If gh CLI commands fail.
335
+
336
+ Example:
337
+ ```python
338
+ # Run all checks
339
+ result = await run_ci_tests("abc123")
340
+
341
+ # Run specific test on Windows
342
+ result = await run_ci_tests(
343
+ "abc123",
344
+ os="windows-latest",
345
+ run_lint=False,
346
+ run_format=False,
347
+ run_typecheck=False,
348
+ test_command="pytest -k test_acp_agent --tb=short",
349
+ )
350
+
351
+ if result.all_passed:
352
+ print("All checks passed!")
353
+ else:
354
+ print(result.summary())
355
+ ```
356
+ """
357
+ import time
358
+
359
+ commit_sha = _resolve_commit(commit)
360
+ start_time = time.monotonic()
361
+
362
+ # Build repo flag if specified
363
+ repo_args = ["-R", repo] if repo else []
364
+
365
+ # Trigger the workflow with parameters
366
+ workflow_args = [
367
+ "workflow",
368
+ "run",
369
+ "test-commit.yml",
370
+ "-f",
371
+ f"commit={commit_sha}",
372
+ "-f",
373
+ f"os={os}",
374
+ "-f",
375
+ f"python_version={python_version}",
376
+ "-f",
377
+ f"run_lint={str(run_lint).lower()}",
378
+ "-f",
379
+ f"run_format={str(run_format).lower()}",
380
+ "-f",
381
+ f"run_typecheck={str(run_typecheck).lower()}",
382
+ "-f",
383
+ f"test_command={test_command or ''}",
384
+ *repo_args,
385
+ ]
386
+ _run_gh(*workflow_args)
387
+
388
+ # Wait a moment for the run to be created
389
+ await asyncio.sleep(2)
390
+
391
+ # Find the run ID
392
+ runs_json = _run_gh(
393
+ "run",
394
+ "list",
395
+ "--workflow=test-commit.yml",
396
+ "--json=databaseId,headSha,status,url",
397
+ "--limit=5",
398
+ *repo_args,
399
+ )
400
+ runs = json.loads(runs_json)
401
+
402
+ # Find the run for our commit
403
+ run_id: int | None = None
404
+ run_url = ""
405
+ for run in runs:
406
+ # Match by commit SHA (workflow dispatch uses the branch HEAD, but we can match)
407
+ if run["status"] in ("queued", "in_progress", "pending"):
408
+ run_id = run["databaseId"]
409
+ run_url = run["url"]
410
+ break
411
+
412
+ if run_id is None:
413
+ msg = f"Could not find workflow run for commit {commit_sha}"
414
+ raise RuntimeError(msg)
415
+
416
+ # Poll for completion
417
+ while True:
418
+ elapsed = time.monotonic() - start_time
419
+ if elapsed > timeout:
420
+ msg = f"Workflow run {run_id} did not complete within {timeout}s"
421
+ raise TimeoutError(msg)
422
+
423
+ run_json = _run_gh(
424
+ "run",
425
+ "view",
426
+ str(run_id),
427
+ "--json=status,conclusion,jobs",
428
+ *repo_args,
429
+ )
430
+ run_data = json.loads(run_json)
431
+
432
+ if run_data["status"] == "completed":
433
+ break
434
+
435
+ await asyncio.sleep(poll_interval)
436
+
437
+ # Parse job results
438
+ duration = time.monotonic() - start_time
439
+ jobs = run_data.get("jobs", [])
440
+
441
+ run_test = test_command is not None and test_command != ""
442
+
443
+ result = CITestResult(
444
+ commit=commit_sha,
445
+ run_id=run_id,
446
+ run_url=run_url,
447
+ os=os,
448
+ python_version=python_version,
449
+ duration_seconds=duration,
450
+ raw_jobs=jobs,
451
+ _repo=repo,
452
+ # Set skipped for disabled checks
453
+ lint="skipped" if not run_lint else "pending",
454
+ format="skipped" if not run_format else "pending",
455
+ typecheck="skipped" if not run_typecheck else "pending",
456
+ test="skipped" if not run_test else "pending",
457
+ )
458
+
459
+ # Map job names to results (only for enabled checks)
460
+ for job in jobs:
461
+ name = job.get("name", "").lower()
462
+ conclusion = job.get("conclusion", "pending")
463
+
464
+ # Normalize conclusion to our type
465
+ if conclusion not in ("success", "failure", "skipped", "cancelled"):
466
+ conclusion = "pending"
467
+
468
+ if "lint" in name and "format" not in name and run_lint:
469
+ result.lint = conclusion
470
+ elif "format" in name and run_format:
471
+ result.format = conclusion
472
+ elif ("type" in name or "mypy" in name) and run_typecheck:
473
+ result.typecheck = conclusion
474
+ elif ("test" in name or "pytest" in name) and run_test:
475
+ result.test = conclusion
476
+
477
+ return result
478
+
479
+
480
+ @dataclass
481
+ class BisectResult:
482
+ """Result of a CI bisect operation."""
483
+
484
+ first_bad_commit: str
485
+ """The first commit that failed the checks."""
486
+
487
+ last_good_commit: str
488
+ """The last commit that passed the checks."""
489
+
490
+ commits_tested: list[CITestResult] = field(default_factory=list)
491
+ """Results for all commits tested during bisection."""
492
+
493
+ total_commits_in_range: int = 0
494
+ """Total number of commits in the range (good, bad]."""
495
+
496
+ steps_taken: int = 0
497
+ """Number of bisection steps performed."""
498
+
499
+ def summary(self) -> str:
500
+ """Generate a human-readable summary."""
501
+ lines = [
502
+ "Bisect Results",
503
+ "=" * 40,
504
+ f"First bad commit: {self.first_bad_commit[:12]}",
505
+ f"Last good commit: {self.last_good_commit[:12]}",
506
+ f"Commits in range: {self.total_commits_in_range}",
507
+ f"Steps taken: {self.steps_taken}",
508
+ "",
509
+ "Tested commits:",
510
+ ]
511
+ for result in self.commits_tested:
512
+ status = "✓" if result.all_passed else "✗"
513
+ lines.append(f" {status} {result.commit[:12]}")
514
+ return "\n".join(lines)
515
+
516
+
517
+ def _get_commits_between(good: str, bad: str) -> list[str]:
518
+ """Get list of commits between good and bad (exclusive of good, inclusive of bad)."""
519
+ result = subprocess.run(
520
+ ["git", "rev-list", "--ancestry-path", f"{good}..{bad}"],
521
+ capture_output=True,
522
+ text=True,
523
+ check=True,
524
+ )
525
+ # Returns newest first, we want oldest first for bisection
526
+ commits = result.stdout.strip().split("\n")
527
+ return list(reversed(commits)) if commits[0] else []
528
+
529
+
530
+ async def bisect_ci(
531
+ good_commit: str,
532
+ bad_commit: str = "HEAD",
533
+ *,
534
+ repo: str | None = None,
535
+ poll_interval: float = 10.0,
536
+ timeout: float = 600.0,
537
+ os: OSChoice = "ubuntu-latest",
538
+ python_version: str = "3.13",
539
+ run_lint: bool = True,
540
+ run_format: bool = True,
541
+ run_typecheck: bool = True,
542
+ test_command: str | None = "pytest --tb=short",
543
+ ) -> BisectResult:
544
+ """Binary search to find the first commit that broke CI.
545
+
546
+ Uses git bisect logic to efficiently find the first bad commit
547
+ between a known good commit and a known bad commit.
548
+
549
+ Args:
550
+ good_commit: A commit SHA known to pass all enabled checks.
551
+ bad_commit: A commit SHA known to fail. Defaults to HEAD.
552
+ repo: Repository in "owner/repo" format. Auto-detected if None.
553
+ poll_interval: Seconds between status checks. Defaults to 10.
554
+ timeout: Timeout per CI run in seconds. Defaults to 600.
555
+ os: Operating system to run on. Defaults to "ubuntu-latest".
556
+ python_version: Python version to use. Defaults to "3.13".
557
+ run_lint: Whether to run ruff check. Defaults to True.
558
+ run_format: Whether to run ruff format check. Defaults to True.
559
+ run_typecheck: Whether to run mypy type checking. Defaults to True.
560
+ test_command: Pytest command to run, or None to skip tests.
561
+
562
+ Returns:
563
+ BisectResult with the first bad commit and bisection details.
564
+
565
+ Example:
566
+ ```python
567
+ # Find which commit broke a specific test on Windows
568
+ result = await bisect_ci(
569
+ good_commit="abc123",
570
+ bad_commit="HEAD",
571
+ os="windows-latest",
572
+ run_lint=False,
573
+ run_format=False,
574
+ run_typecheck=False,
575
+ test_command="pytest -k test_acp_agent --tb=short",
576
+ )
577
+ print(f"Tests broke at: {result.first_bad_commit}")
578
+ ```
579
+ """
580
+ good_sha = _resolve_commit(good_commit)
581
+ bad_sha = _resolve_commit(bad_commit)
582
+
583
+ # Get all commits in range
584
+ commits = _get_commits_between(good_sha, bad_sha)
585
+ if not commits:
586
+ msg = f"No commits found between {good_sha[:12]} and {bad_sha[:12]}"
587
+ raise ValueError(msg)
588
+
589
+ tested: list[CITestResult] = []
590
+ left = 0
591
+ right = len(commits) - 1
592
+ steps = 0
593
+
594
+ # Binary search: find first bad commit
595
+ # Invariant: commits[left-1] is good (or left=0), commits[right] is bad
596
+ while left < right:
597
+ mid = (left + right) // 2
598
+ steps += 1
599
+
600
+ result = await run_ci_tests(
601
+ commits[mid],
602
+ repo=repo,
603
+ poll_interval=poll_interval,
604
+ timeout=timeout,
605
+ os=os,
606
+ python_version=python_version,
607
+ run_lint=run_lint,
608
+ run_format=run_format,
609
+ run_typecheck=run_typecheck,
610
+ test_command=test_command,
611
+ )
612
+ tested.append(result)
613
+
614
+ if result.all_passed:
615
+ # This commit is good, search in upper half
616
+ left = mid + 1
617
+ else:
618
+ # This commit is bad, search in lower half
619
+ right = mid
620
+
621
+ first_bad_sha = commits[right]
622
+
623
+ # Determine last good commit
624
+ last_good_sha = good_sha if right == 0 else commits[right - 1]
625
+
626
+ return BisectResult(
627
+ first_bad_commit=first_bad_sha,
628
+ last_good_commit=last_good_sha,
629
+ commits_tested=tested,
630
+ total_commits_in_range=len(commits),
631
+ steps_taken=steps,
632
+ )
633
+
634
+
635
+ async def quick_ci_check(commit: str = "HEAD") -> bool:
636
+ """Quick check if a commit passes all CI checks.
637
+
638
+ Convenience wrapper around run_ci_tests that returns a simple boolean.
639
+
640
+ Args:
641
+ commit: Commit SHA or "HEAD" to test.
642
+
643
+ Returns:
644
+ True if all checks passed, False otherwise.
645
+ """
646
+ result = await run_ci_tests(commit)
647
+ return result.all_passed
@@ -0,0 +1,6 @@
1
+ """Standalone tool implementations.
2
+
3
+ Each tool is a subclass of Tool that can be used independently or grouped.
4
+ """
5
+
6
+ from __future__ import annotations