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
@@ -20,7 +20,15 @@ from __future__ import annotations
20
20
  from dataclasses import dataclass, field
21
21
  from typing import TYPE_CHECKING, Any, Literal
22
22
 
23
- from pydantic_ai import AgentStreamEvent
23
+ from pydantic_ai import (
24
+ AgentStreamEvent,
25
+ PartDeltaEvent as PyAIPartDeltaEvent,
26
+ PartStartEvent as PyAIPartStartEvent,
27
+ TextPart,
28
+ TextPartDelta,
29
+ ThinkingPart,
30
+ ThinkingPartDelta,
31
+ )
24
32
 
25
33
  from agentpool.messaging import ChatMessage # noqa: TC001
26
34
 
@@ -35,6 +43,30 @@ if TYPE_CHECKING:
35
43
  # Lifecycle events (aligned with AG-UI protocol)
36
44
 
37
45
 
46
+ class PartStartEvent(PyAIPartStartEvent):
47
+ """Part start event."""
48
+
49
+ @classmethod
50
+ def thinking(cls, index: int, content: str) -> PartStartEvent:
51
+ return cls(index=index, part=ThinkingPart(content=content))
52
+
53
+ @classmethod
54
+ def text(cls, index: int, content: str) -> PartStartEvent:
55
+ return cls(index=index, part=TextPart(content=content))
56
+
57
+
58
+ class PartDeltaEvent(PyAIPartDeltaEvent):
59
+ """Part start event."""
60
+
61
+ @classmethod
62
+ def thinking(cls, index: int, content: str) -> PartDeltaEvent:
63
+ return cls(index=index, delta=ThinkingPartDelta(content_delta=content))
64
+
65
+ @classmethod
66
+ def text(cls, index: int, content: str) -> PartDeltaEvent:
67
+ return cls(index=index, delta=TextPartDelta(content_delta=content))
68
+
69
+
38
70
  @dataclass(kw_only=True)
39
71
  class RunStartedEvent:
40
72
  """Signals the start of an agent run."""
@@ -409,6 +441,7 @@ class ToolCallProgressEvent:
409
441
  success: bool,
410
442
  error: str | None = None,
411
443
  tool_name: str | None = None,
444
+ line: int = 0,
412
445
  ) -> ToolCallProgressEvent:
413
446
  """Create event for file operation.
414
447
 
@@ -419,13 +452,14 @@ class ToolCallProgressEvent:
419
452
  success: Whether operation succeeded
420
453
  error: Error message if failed
421
454
  tool_name: Optional tool name
455
+ line: Line number for navigation (0 = beginning)
422
456
  """
423
457
  status: Literal["completed", "failed"] = "completed" if success else "failed"
424
458
  title = f"{operation.capitalize()}: {path}"
425
459
  if error:
426
460
  title = f"{title} - {error}"
427
461
 
428
- items: list[ToolCallContentItem] = [LocationContentItem(path=path)]
462
+ items: list[ToolCallContentItem] = [LocationContentItem(path=path, line=line)]
429
463
  if error:
430
464
  items.append(TextContentItem(text=f"Error: {error}"))
431
465
 
@@ -460,7 +494,6 @@ class ToolCallProgressEvent:
460
494
  """
461
495
  items: list[ToolCallContentItem] = [
462
496
  DiffContentItem(path=path, old_text=old_text, new_text=new_text),
463
- LocationContentItem(path=path),
464
497
  ]
465
498
 
466
499
  return cls(
@@ -513,10 +546,36 @@ class ToolCallCompleteEvent:
513
546
  """The name of the agent that made the tool call."""
514
547
  message_id: str
515
548
  """The message ID associated with this tool call."""
549
+ metadata: dict[str, Any] | None = None
550
+ """Optional metadata for UI/client use (diffs, diagnostics, etc.)."""
516
551
  event_kind: Literal["tool_call_complete"] = "tool_call_complete"
517
552
  """Event type identifier."""
518
553
 
519
554
 
555
+ @dataclass(kw_only=True)
556
+ class ToolResultMetadataEvent:
557
+ """Sidechannel event carrying tool result metadata stripped by Claude SDK.
558
+
559
+ The Claude SDK strips the `_meta` field from MCP CallToolResult when converting
560
+ to ToolResultBlock, losing UI-only metadata (diffs, diagnostics, etc.).
561
+
562
+ This event provides a sidechannel to preserve that metadata:
563
+ - Tool returns ToolResult with metadata
564
+ - ToolManagerBridge emits this event with metadata before converting
565
+ - ClaudeCodeAgent correlates by tool_call_id and enriches ToolCallCompleteEvent
566
+ - Downstream consumers (OpenCode, ACP) receive complete events with metadata
567
+
568
+ This avoids polluting LLM context with UI-only data while preserving it for clients.
569
+ """
570
+
571
+ tool_call_id: str
572
+ """The ID of the tool call this metadata belongs to."""
573
+ metadata: dict[str, Any]
574
+ """Metadata for UI/client use (diffs, diagnostics, etc.)."""
575
+ event_kind: Literal["tool_result_metadata"] = "tool_result_metadata"
576
+ """Event type identifier."""
577
+
578
+
520
579
  @dataclass(kw_only=True)
521
580
  class CustomEvent[T]:
522
581
  """Generic custom event that can be emitted during tool execution."""
@@ -543,6 +602,45 @@ class PlanUpdateEvent:
543
602
  """Event type identifier."""
544
603
 
545
604
 
605
+ @dataclass(kw_only=True)
606
+ class SubAgentEvent:
607
+ """Event wrapping activity from a subagent or team member.
608
+
609
+ Used to propagate events from delegated agents/teams into the parent stream,
610
+ allowing the consumer (UI/server) to decide how to render nested activity.
611
+ """
612
+
613
+ source_name: str
614
+ """Name of the agent or team that produced this event."""
615
+ source_type: Literal["agent", "team_parallel", "team_sequential"]
616
+ """Type of source: agent, parallel team, or sequential team."""
617
+ event: RichAgentStreamEvent[Any]
618
+ """The actual event from the subagent/team."""
619
+ depth: int = 1
620
+ """Nesting depth (1 = direct child, 2 = grandchild, etc.)."""
621
+ event_kind: Literal["subagent"] = "subagent"
622
+ """Event type identifier."""
623
+
624
+
625
+ @dataclass(kw_only=True)
626
+ class CompactionEvent:
627
+ """Event indicating context compaction is starting or completed.
628
+
629
+ This is a semantic event that consumers (ACP, OpenCode) handle differently:
630
+ - ACP: Converts to a text message for display
631
+ - OpenCode: Emits session.compacted SSE event
632
+ """
633
+
634
+ session_id: str
635
+ """The session ID being compacted."""
636
+ trigger: Literal["auto", "manual"] = "auto"
637
+ """What triggered the compaction (auto = context overflow, manual = slash command)."""
638
+ phase: Literal["starting", "completed"] = "starting"
639
+ """Current phase of compaction."""
640
+ event_kind: Literal["compaction"] = "compaction"
641
+ """Event type identifier."""
642
+
643
+
546
644
  type RichAgentStreamEvent[OutputDataT] = (
547
645
  AgentStreamEvent
548
646
  | StreamCompleteEvent[OutputDataT]
@@ -552,6 +650,9 @@ type RichAgentStreamEvent[OutputDataT] = (
552
650
  | ToolCallProgressEvent
553
651
  | ToolCallCompleteEvent
554
652
  | PlanUpdateEvent
653
+ | CompactionEvent
654
+ | SubAgentEvent
655
+ | ToolResultMetadataEvent
555
656
  | CustomEvent[Any]
556
657
  )
557
658
 
@@ -0,0 +1,145 @@
1
+ """AgentPool event helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import TYPE_CHECKING, Any
7
+
8
+ from agentpool.agents.events import DiffContentItem, LocationContentItem
9
+
10
+
11
+ if TYPE_CHECKING:
12
+ from agentpool.agents.events import ToolCallContentItem
13
+ from agentpool.tools.base import ToolKind
14
+
15
+
16
+ @dataclass
17
+ class RichToolInfo:
18
+ """Rich display information derived from tool name and input."""
19
+
20
+ title: str
21
+ """Human-readable title for the tool call."""
22
+ kind: ToolKind = "other"
23
+ """Category of tool operation."""
24
+ locations: list[LocationContentItem] = field(default_factory=list)
25
+ """File locations involved in the operation."""
26
+ content: list[ToolCallContentItem] = field(default_factory=list)
27
+ """Rich content items (diffs, text, etc.)."""
28
+
29
+
30
+ def derive_rich_tool_info(name: str, input_data: dict[str, Any]) -> RichToolInfo: # noqa: PLR0911, PLR0915
31
+ """Derive rich display info from tool name and input arguments.
32
+
33
+ Maps MCP tool names and their inputs to human-readable titles, kinds,
34
+ and location information for rich UI display. Handles both Claude Code
35
+ built-in tools and MCP bridge tools.
36
+
37
+ Args:
38
+ name: The tool name (e.g., "Read", "mcp__server__read")
39
+ input_data: The tool input arguments
40
+
41
+ Returns:
42
+ RichToolInfo with derived display information
43
+ """
44
+ # Extract the actual tool name if it's an MCP bridge tool
45
+ # Format: mcp__{server_name}__{tool_name}
46
+ actual_name = name
47
+ if name.startswith("mcp__") and "__" in name[5:]:
48
+ parts = name.split("__")
49
+ if len(parts) >= 3: # noqa: PLR2004
50
+ actual_name = parts[-1] # Get the last part (actual tool name)
51
+
52
+ # Normalize to lowercase for matching
53
+ tool_lower = actual_name.lower()
54
+ # Read operations
55
+ if tool_lower in ("read", "read_file"):
56
+ path = input_data.get("file_path") or input_data.get("path", "")
57
+ offset = input_data.get("offset") or input_data.get("line")
58
+ limit = input_data.get("limit")
59
+
60
+ suffix = ""
61
+ if limit:
62
+ start = (offset or 0) + 1
63
+ end = (offset or 0) + limit
64
+ suffix = f" ({start}-{end})"
65
+ elif offset:
66
+ suffix = f" (from line {offset + 1})"
67
+ title = f"Read {path}{suffix}" if path else "Read File"
68
+ locations = [LocationContentItem(path=path, line=offset or 0)] if path else []
69
+ return RichToolInfo(title=title, kind="read", locations=locations)
70
+
71
+ # Write operations
72
+ if tool_lower in ("write", "write_file"):
73
+ path = input_data.get("file_path") or input_data.get("path", "")
74
+ content = input_data.get("content", "")
75
+ return RichToolInfo(
76
+ title=f"Write {path}" if path else "Write File",
77
+ kind="edit",
78
+ locations=[LocationContentItem(path=path)] if path else [],
79
+ content=[DiffContentItem(path=path, old_text=None, new_text=content)] if path else [],
80
+ )
81
+ # Edit operations
82
+ if tool_lower in ("edit", "edit_file"):
83
+ path = input_data.get("file_path") or input_data.get("path", "")
84
+ old_string = input_data.get("old_string") or input_data.get("old_text", "")
85
+ new_string = input_data.get("new_string") or input_data.get("new_text", "")
86
+ return RichToolInfo(
87
+ title=f"Edit {path}" if path else "Edit File",
88
+ kind="edit",
89
+ locations=[LocationContentItem(path=path)] if path else [],
90
+ content=[DiffContentItem(path=path, old_text=old_string, new_text=new_string)]
91
+ if path
92
+ else [],
93
+ )
94
+ # Delete operations
95
+ if tool_lower in ("delete", "delete_path", "delete_file"):
96
+ path = input_data.get("file_path") or input_data.get("path", "")
97
+ locations = [LocationContentItem(path=path)] if path else []
98
+ title = f"Delete {path}" if path else "Delete"
99
+ return RichToolInfo(title=title, kind="delete", locations=locations)
100
+ # Bash/terminal operations
101
+ if tool_lower in ("bash", "execute", "run_command", "execute_command", "execute_code"):
102
+ command = input_data.get("command") or input_data.get("code", "")
103
+ # Escape backticks in command
104
+ escaped_cmd = command.replace("`", "\\`") if command else ""
105
+ title = f"`{escaped_cmd}`" if escaped_cmd else "Terminal"
106
+ return RichToolInfo(title=title, kind="execute")
107
+ # Search operations
108
+ if tool_lower in ("grep", "search", "glob", "find"):
109
+ pattern = input_data.get("pattern") or input_data.get("query", "")
110
+ path = input_data.get("path", "")
111
+ title = f"Search for '{pattern}'" if pattern else "Search"
112
+ if path:
113
+ title += f" in {path}"
114
+ locations = [LocationContentItem(path=path)] if path else []
115
+ return RichToolInfo(title=title, kind="search", locations=locations)
116
+ # List directory
117
+ if tool_lower in ("ls", "list", "list_directory"):
118
+ path = input_data.get("path", ".")
119
+ title = f"List {path}" if path != "." else "List current directory"
120
+ locations = [LocationContentItem(path=path)] if path else []
121
+ return RichToolInfo(title=title, kind="search", locations=locations)
122
+ # Web operations
123
+ if tool_lower in ("webfetch", "web_fetch", "fetch"):
124
+ url = input_data.get("url", "")
125
+ return RichToolInfo(title=f"Fetch {url}" if url else "Web Fetch", kind="fetch")
126
+ if tool_lower in ("websearch", "web_search", "search_web"):
127
+ query = input_data.get("query", "")
128
+ return RichToolInfo(title=f"Search: {query}" if query else "Web Search", kind="fetch")
129
+ # Task/subagent operations
130
+ if tool_lower == "task":
131
+ description = input_data.get("description", "")
132
+ return RichToolInfo(title=description if description else "Task", kind="think")
133
+ # Notebook operations
134
+ if tool_lower in ("notebookread", "notebook_read"):
135
+ path = input_data.get("notebook_path", "")
136
+ title = f"Read Notebook {path}" if path else "Read Notebook"
137
+ locations = [LocationContentItem(path=path)] if path else []
138
+ return RichToolInfo(title=title, kind="read", locations=locations)
139
+ if tool_lower in ("notebookedit", "notebook_edit"):
140
+ path = input_data.get("notebook_path", "")
141
+ title = f"Edit Notebook {path}" if path else "Edit Notebook"
142
+ locations = [LocationContentItem(path=path)] if path else []
143
+ return RichToolInfo(title=title, kind="edit", locations=locations)
144
+ # Default: use the tool name as title
145
+ return RichToolInfo(title=actual_name, kind="other")
@@ -0,0 +1,254 @@
1
+ """Stream processors for event pipelines.
2
+
3
+ This module provides composable processors that can transform, filter, or observe
4
+ event streams. Processors wrap AsyncIterators and can be chained together.
5
+
6
+ Example:
7
+ ```python
8
+ # Simple function processor
9
+ async def log_events(stream):
10
+ async for event in stream:
11
+ print(f"Event: {type(event).__name__}")
12
+ yield event
13
+
14
+ # Class-based processor with state
15
+ tracker = FileTrackingProcessor()
16
+
17
+ # Compose into pipeline
18
+ pipeline = StreamPipeline([tracker, log_events])
19
+
20
+ async for event in pipeline(raw_events):
21
+ yield event
22
+
23
+ # Access state directly
24
+ print(tracker.get_metadata())
25
+ ```
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ from collections.abc import Callable
31
+ from dataclasses import dataclass, field
32
+ from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
33
+
34
+
35
+ if TYPE_CHECKING:
36
+ from collections.abc import AsyncIterator, Coroutine
37
+
38
+ from agentpool.agents.events.events import RichAgentStreamEvent
39
+ from agentpool.common_types import SimpleJsonType
40
+
41
+
42
+ # Type alias for processor callables
43
+ type StreamProcessorCallable = Callable[
44
+ [AsyncIterator[RichAgentStreamEvent[Any]]], AsyncIterator[RichAgentStreamEvent[Any]]
45
+ ]
46
+
47
+
48
+ @runtime_checkable
49
+ class StreamProcessor(Protocol):
50
+ """Protocol for stream processors.
51
+
52
+ Processors can be:
53
+ - Callables: `(AsyncIterator[RichAgentStreamEvent]) -> AsyncIterator[RichAgentStreamEvent]`
54
+ - Classes with `__call__`: Same signature, but can hold state
55
+ """
56
+
57
+ def __call__(
58
+ self, stream: AsyncIterator[RichAgentStreamEvent[Any]]
59
+ ) -> AsyncIterator[RichAgentStreamEvent[Any]]:
60
+ """Process an event stream.
61
+
62
+ Args:
63
+ stream: Input event stream
64
+
65
+ Returns:
66
+ Transformed/filtered event stream
67
+ """
68
+ ...
69
+
70
+
71
+ @dataclass
72
+ class StreamPipeline:
73
+ """Composable pipeline for processing event streams.
74
+
75
+ Chains multiple processors together, passing the output of each
76
+ to the input of the next.
77
+
78
+ Example:
79
+ ```python
80
+ tracker = FileTrackingProcessor()
81
+ pipeline = StreamPipeline([
82
+ tracker,
83
+ event_handler_processor(handler),
84
+ ])
85
+
86
+ async for event in pipeline(raw_events):
87
+ yield event
88
+
89
+ # Access state directly from processor instances
90
+ print(tracker.get_metadata())
91
+ ```
92
+ """
93
+
94
+ processors: list[StreamProcessorCallable | StreamProcessor] = field(default_factory=list)
95
+
96
+ def __call__(
97
+ self, stream: AsyncIterator[RichAgentStreamEvent[Any]]
98
+ ) -> AsyncIterator[RichAgentStreamEvent[Any]]:
99
+ """Run events through all processors in sequence.
100
+
101
+ Args:
102
+ stream: Input event stream
103
+
104
+ Returns:
105
+ Processed event stream
106
+ """
107
+ result = stream
108
+ for processor in self.processors:
109
+ result = processor(result)
110
+ return result
111
+
112
+ def add(self, processor: StreamProcessorCallable | StreamProcessor) -> None:
113
+ """Add a processor to the pipeline.
114
+
115
+ Args:
116
+ processor: Processor to add
117
+ """
118
+ self.processors.append(processor)
119
+
120
+
121
+ def extract_file_path_from_tool_call(tool_name: str, raw_input: dict[str, Any]) -> str | None:
122
+ """Extract file path from a tool call if it's a file-writing tool.
123
+
124
+ Uses simple heuristics:
125
+ - Tool name contains 'write' or 'edit' (case-insensitive)
126
+ - Input contains 'path' or 'file_path' key
127
+
128
+ Args:
129
+ tool_name: Name of the tool being called
130
+ raw_input: Tool call arguments
131
+
132
+ Returns:
133
+ File path if this is a file-writing tool, None otherwise
134
+ """
135
+ name_lower = tool_name.lower()
136
+ if "write" not in name_lower and "edit" not in name_lower:
137
+ return None
138
+
139
+ # Try common path argument names
140
+ for key in ("file_path", "path", "filepath", "filename", "file"):
141
+ if key in raw_input and isinstance(val := raw_input[key], str):
142
+ return val
143
+
144
+ return None
145
+
146
+
147
+ @dataclass
148
+ class FileTrackingProcessor:
149
+ """Tracks files modified during a stream of events.
150
+
151
+ Observes ToolCallStartEvent and extracts file paths from write/edit operations.
152
+ Does not modify events - just passes them through while collecting metadata.
153
+
154
+ Example:
155
+ ```python
156
+ tracker = FileTrackingProcessor()
157
+
158
+ async for event in tracker(events):
159
+ yield event
160
+
161
+ print(f"Modified files: {tracker.touched_files}")
162
+ print(f"Metadata: {tracker.get_metadata()}")
163
+ ```
164
+ """
165
+
166
+ touched_files: set[str] = field(default_factory=set)
167
+ """Set of file paths that were modified by tool calls."""
168
+
169
+ extractor: Callable[[str, dict[str, Any]], str | None] = extract_file_path_from_tool_call
170
+ """Function to extract file path from tool call. Can be customized."""
171
+
172
+ def process_event(self, event: RichAgentStreamEvent[Any]) -> None:
173
+ """Process an event and track any file modifications.
174
+
175
+ Args:
176
+ event: The event to process (checks for ToolCallStartEvent)
177
+ """
178
+ from agentpool.agents.events import ToolCallStartEvent
179
+
180
+ if isinstance(event, ToolCallStartEvent) and (
181
+ file_path := self.extractor(event.tool_name or "", event.raw_input or {})
182
+ ):
183
+ self.touched_files.add(file_path)
184
+
185
+ def __call__(
186
+ self, stream: AsyncIterator[RichAgentStreamEvent[Any]]
187
+ ) -> AsyncIterator[RichAgentStreamEvent[Any]]:
188
+ """Wrap a stream to track file modifications.
189
+
190
+ Args:
191
+ stream: Input event stream
192
+
193
+ Returns:
194
+ Same events, unmodified
195
+ """
196
+ return self._process(stream)
197
+
198
+ async def _process(
199
+ self, stream: AsyncIterator[RichAgentStreamEvent[Any]]
200
+ ) -> AsyncIterator[RichAgentStreamEvent[Any]]:
201
+ """Internal async generator for processing."""
202
+ async for event in stream:
203
+ self.process_event(event)
204
+ yield event
205
+
206
+ def get_metadata(self) -> SimpleJsonType:
207
+ """Get metadata dict with touched files (if any).
208
+
209
+ Returns:
210
+ Dict with 'touched_files' key if files were modified, else empty dict
211
+ """
212
+ if self.touched_files:
213
+ return {"touched_files": sorted(self.touched_files)}
214
+ return {}
215
+
216
+ def reset(self) -> None:
217
+ """Clear tracked files for reuse."""
218
+ self.touched_files.clear()
219
+
220
+
221
+ def event_handler_processor(
222
+ handler: Callable[[Any, RichAgentStreamEvent[Any]], Coroutine[Any, Any, None]],
223
+ ) -> StreamProcessorCallable:
224
+ """Create a processor that calls an event handler for each event.
225
+
226
+ The handler is called with (None, event) to match the existing
227
+ MultiEventHandler signature.
228
+
229
+ Args:
230
+ handler: Async callable with signature (ctx, event) -> None
231
+
232
+ Returns:
233
+ Processor function that calls the handler
234
+
235
+ Example:
236
+ ```python
237
+ pipeline = StreamPipeline([
238
+ event_handler_processor(self.event_handler),
239
+ ])
240
+ ```
241
+ """
242
+
243
+ async def process(
244
+ stream: AsyncIterator[RichAgentStreamEvent[Any]],
245
+ ) -> AsyncIterator[RichAgentStreamEvent[Any]]:
246
+ async for event in stream:
247
+ await handler(None, event)
248
+ yield event
249
+
250
+ return process
251
+
252
+
253
+ # Convenience alias for backwards compatibility with existing FileTracker usage
254
+ FileTracker = FileTrackingProcessor
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from collections.abc import Callable, Mapping
6
+ from contextlib import asynccontextmanager
6
7
  from typing import TYPE_CHECKING, Any, Literal, cast, overload
7
8
 
8
9
  from schemez import Schema
@@ -12,7 +13,7 @@ from agentpool.messaging import ChatMessage
12
13
 
13
14
 
14
15
  if TYPE_CHECKING:
15
- from collections.abc import Sequence
16
+ from collections.abc import AsyncIterator, Sequence
16
17
 
17
18
  from toprompt import AnyPromptType
18
19
 
@@ -90,6 +91,33 @@ class Interactions:
90
91
  def __init__(self, agent: SupportsStructuredOutput) -> None:
91
92
  self.agent = agent
92
93
 
94
+ @asynccontextmanager
95
+ async def _with_structured_output[T](
96
+ self, output_type: type[T]
97
+ ) -> AsyncIterator[SupportsStructuredOutput]:
98
+ """Context manager to temporarily set structured output type and restore afterwards.
99
+
100
+ Args:
101
+ output_type: The type to structure output as
102
+
103
+ Yields:
104
+ The agent configured for structured output
105
+
106
+ Example:
107
+ async with interactions._with_structured_output(MyType) as agent:
108
+ result = await agent.run(prompt)
109
+ """
110
+ # Save original output type
111
+ old_output_type = getattr(self.agent, "_output_type", None)
112
+ try:
113
+ # Configure structured output
114
+ structured_agent = self.agent.to_structured(output_type)
115
+ yield structured_agent
116
+ finally:
117
+ # Restore original output type
118
+ if hasattr(self.agent, "_output_type"):
119
+ self.agent._output_type = old_output_type
120
+
93
121
  # async def conversation(
94
122
  # self,
95
123
  # other: MessageNode[Any, Any],
@@ -218,8 +246,9 @@ Available options:
218
246
 
219
247
  Select ONE option by its exact label."""
220
248
 
221
- # Get LLM's string-based decision
222
- result = await self.agent.run(prompt or default_prompt, output_type=LLMPick)
249
+ # Get LLM's string-based decision using structured output
250
+ async with self._with_structured_output(LLMPick) as structured_agent:
251
+ result = await structured_agent.run(prompt or default_prompt)
223
252
 
224
253
  # Convert to type-safe decision
225
254
  if result.content.selection not in label_map:
@@ -328,7 +357,9 @@ Available options:
328
357
  {picks_info} options by their exact labels.
329
358
  List your selections, one per line, followed by your reasoning."""
330
359
 
331
- result = await self.agent.run(prompt or default_prompt, output_type=LLMMultiPick)
360
+ # Get LLM's multi-selection using structured output
361
+ async with self._with_structured_output(LLMMultiPick) as structured_agent:
362
+ result = await structured_agent.run(prompt or default_prompt)
332
363
 
333
364
  # Validate selections
334
365
  invalid = [s for s in result.content.selections if s not in label_map]
@@ -368,7 +399,9 @@ List your selections, one per line, followed by your reasoning."""
368
399
  instance: item_model # type: ignore
369
400
  # explanation: str | None = None
370
401
 
371
- result = await self.agent.run(final_prompt, output_type=Extraction)
402
+ # Use structured output via context manager
403
+ async with self._with_structured_output(Extraction) as structured_agent:
404
+ result = await structured_agent.run(final_prompt)
372
405
  return as_type(**result.content.instance.model_dump())
373
406
 
374
407
  async def extract_multiple[T](
@@ -404,7 +437,9 @@ List your selections, one per line, followed by your reasoning."""
404
437
  instances: list[item_model] # type: ignore
405
438
  # explanation: str | None = None
406
439
 
407
- result = await self.agent.run(final_prompt, output_type=Extraction)
440
+ # Use structured output via context manager
441
+ async with self._with_structured_output(Extraction) as structured_agent:
442
+ result = await structured_agent.run(final_prompt)
408
443
  num_instances = len(result.content.instances) # Validate counts
409
444
  if len(result.content.instances) < min_items:
410
445
  msg = f"Found only {num_instances} instances, need {min_items}"