agentpool 2.2.3__py3-none-any.whl → 2.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (250) hide show
  1. acp/__init__.py +0 -4
  2. acp/acp_requests.py +20 -77
  3. acp/agent/connection.py +8 -0
  4. acp/agent/implementations/debug_server/debug_server.py +6 -2
  5. acp/agent/protocol.py +6 -0
  6. acp/client/connection.py +38 -29
  7. acp/client/implementations/default_client.py +3 -2
  8. acp/client/implementations/headless_client.py +2 -2
  9. acp/connection.py +2 -2
  10. acp/notifications.py +18 -49
  11. acp/schema/__init__.py +2 -0
  12. acp/schema/agent_responses.py +21 -0
  13. acp/schema/client_requests.py +3 -3
  14. acp/schema/session_state.py +63 -29
  15. acp/task/supervisor.py +2 -2
  16. acp/utils.py +2 -2
  17. agentpool/__init__.py +2 -0
  18. agentpool/agents/acp_agent/acp_agent.py +278 -263
  19. agentpool/agents/acp_agent/acp_converters.py +150 -17
  20. agentpool/agents/acp_agent/client_handler.py +35 -24
  21. agentpool/agents/acp_agent/session_state.py +14 -6
  22. agentpool/agents/agent.py +471 -643
  23. agentpool/agents/agui_agent/agui_agent.py +104 -107
  24. agentpool/agents/agui_agent/helpers.py +3 -4
  25. agentpool/agents/base_agent.py +485 -32
  26. agentpool/agents/claude_code_agent/FORKING.md +191 -0
  27. agentpool/agents/claude_code_agent/__init__.py +13 -1
  28. agentpool/agents/claude_code_agent/claude_code_agent.py +654 -334
  29. agentpool/agents/claude_code_agent/converters.py +4 -141
  30. agentpool/agents/claude_code_agent/models.py +77 -0
  31. agentpool/agents/claude_code_agent/static_info.py +100 -0
  32. agentpool/agents/claude_code_agent/usage.py +242 -0
  33. agentpool/agents/events/__init__.py +22 -0
  34. agentpool/agents/events/builtin_handlers.py +65 -0
  35. agentpool/agents/events/event_emitter.py +3 -0
  36. agentpool/agents/events/events.py +84 -3
  37. agentpool/agents/events/infer_info.py +145 -0
  38. agentpool/agents/events/processors.py +254 -0
  39. agentpool/agents/interactions.py +41 -6
  40. agentpool/agents/modes.py +13 -0
  41. agentpool/agents/slashed_agent.py +5 -4
  42. agentpool/agents/tool_wrapping.py +18 -6
  43. agentpool/common_types.py +35 -21
  44. agentpool/config_resources/acp_assistant.yml +2 -2
  45. agentpool/config_resources/agents.yml +3 -0
  46. agentpool/config_resources/agents_template.yml +1 -0
  47. agentpool/config_resources/claude_code_agent.yml +9 -8
  48. agentpool/config_resources/external_acp_agents.yml +2 -1
  49. agentpool/delegation/base_team.py +4 -30
  50. agentpool/delegation/pool.py +104 -265
  51. agentpool/delegation/team.py +57 -57
  52. agentpool/delegation/teamrun.py +50 -55
  53. agentpool/functional/run.py +10 -4
  54. agentpool/mcp_server/client.py +73 -38
  55. agentpool/mcp_server/conversions.py +54 -13
  56. agentpool/mcp_server/manager.py +9 -23
  57. agentpool/mcp_server/registries/official_registry_client.py +10 -1
  58. agentpool/mcp_server/tool_bridge.py +114 -79
  59. agentpool/messaging/connection_manager.py +11 -10
  60. agentpool/messaging/event_manager.py +5 -5
  61. agentpool/messaging/message_container.py +6 -30
  62. agentpool/messaging/message_history.py +87 -8
  63. agentpool/messaging/messagenode.py +52 -14
  64. agentpool/messaging/messages.py +2 -26
  65. agentpool/messaging/processing.py +10 -22
  66. agentpool/models/__init__.py +1 -1
  67. agentpool/models/acp_agents/base.py +6 -2
  68. agentpool/models/acp_agents/mcp_capable.py +124 -15
  69. agentpool/models/acp_agents/non_mcp.py +0 -23
  70. agentpool/models/agents.py +66 -66
  71. agentpool/models/agui_agents.py +1 -1
  72. agentpool/models/claude_code_agents.py +111 -17
  73. agentpool/models/file_parsing.py +0 -1
  74. agentpool/models/manifest.py +70 -50
  75. agentpool/prompts/conversion_manager.py +1 -1
  76. agentpool/prompts/prompts.py +5 -2
  77. agentpool/resource_providers/__init__.py +2 -0
  78. agentpool/resource_providers/aggregating.py +4 -2
  79. agentpool/resource_providers/base.py +13 -3
  80. agentpool/resource_providers/codemode/code_executor.py +72 -5
  81. agentpool/resource_providers/codemode/helpers.py +2 -2
  82. agentpool/resource_providers/codemode/provider.py +64 -12
  83. agentpool/resource_providers/codemode/remote_mcp_execution.py +2 -2
  84. agentpool/resource_providers/codemode/remote_provider.py +9 -12
  85. agentpool/resource_providers/filtering.py +3 -1
  86. agentpool/resource_providers/mcp_provider.py +66 -12
  87. agentpool/resource_providers/plan_provider.py +111 -18
  88. agentpool/resource_providers/pool.py +5 -3
  89. agentpool/resource_providers/resource_info.py +111 -0
  90. agentpool/resource_providers/static.py +2 -2
  91. agentpool/sessions/__init__.py +2 -0
  92. agentpool/sessions/manager.py +2 -3
  93. agentpool/sessions/models.py +9 -6
  94. agentpool/sessions/protocol.py +28 -0
  95. agentpool/sessions/session.py +11 -55
  96. agentpool/storage/manager.py +361 -54
  97. agentpool/talk/registry.py +4 -4
  98. agentpool/talk/talk.py +9 -10
  99. agentpool/testing.py +1 -1
  100. agentpool/tool_impls/__init__.py +6 -0
  101. agentpool/tool_impls/agent_cli/__init__.py +42 -0
  102. agentpool/tool_impls/agent_cli/tool.py +95 -0
  103. agentpool/tool_impls/bash/__init__.py +64 -0
  104. agentpool/tool_impls/bash/helpers.py +35 -0
  105. agentpool/tool_impls/bash/tool.py +171 -0
  106. agentpool/tool_impls/delete_path/__init__.py +70 -0
  107. agentpool/tool_impls/delete_path/tool.py +142 -0
  108. agentpool/tool_impls/download_file/__init__.py +80 -0
  109. agentpool/tool_impls/download_file/tool.py +183 -0
  110. agentpool/tool_impls/execute_code/__init__.py +55 -0
  111. agentpool/tool_impls/execute_code/tool.py +163 -0
  112. agentpool/tool_impls/grep/__init__.py +80 -0
  113. agentpool/tool_impls/grep/tool.py +200 -0
  114. agentpool/tool_impls/list_directory/__init__.py +73 -0
  115. agentpool/tool_impls/list_directory/tool.py +197 -0
  116. agentpool/tool_impls/question/__init__.py +42 -0
  117. agentpool/tool_impls/question/tool.py +127 -0
  118. agentpool/tool_impls/read/__init__.py +104 -0
  119. agentpool/tool_impls/read/tool.py +305 -0
  120. agentpool/tools/__init__.py +2 -1
  121. agentpool/tools/base.py +114 -34
  122. agentpool/tools/manager.py +57 -1
  123. agentpool/ui/base.py +2 -2
  124. agentpool/ui/mock_provider.py +2 -2
  125. agentpool/ui/stdlib_provider.py +2 -2
  126. agentpool/utils/streams.py +21 -96
  127. agentpool/vfs_registry.py +7 -2
  128. {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/METADATA +16 -22
  129. {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/RECORD +242 -195
  130. {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/WHEEL +1 -1
  131. agentpool_cli/__main__.py +20 -0
  132. agentpool_cli/create.py +1 -1
  133. agentpool_cli/serve_acp.py +59 -1
  134. agentpool_cli/serve_opencode.py +1 -1
  135. agentpool_cli/ui.py +557 -0
  136. agentpool_commands/__init__.py +12 -5
  137. agentpool_commands/agents.py +1 -1
  138. agentpool_commands/pool.py +260 -0
  139. agentpool_commands/session.py +1 -1
  140. agentpool_commands/text_sharing/__init__.py +119 -0
  141. agentpool_commands/text_sharing/base.py +123 -0
  142. agentpool_commands/text_sharing/github_gist.py +80 -0
  143. agentpool_commands/text_sharing/opencode.py +462 -0
  144. agentpool_commands/text_sharing/paste_rs.py +59 -0
  145. agentpool_commands/text_sharing/pastebin.py +116 -0
  146. agentpool_commands/text_sharing/shittycodingagent.py +112 -0
  147. agentpool_commands/utils.py +31 -32
  148. agentpool_config/__init__.py +30 -2
  149. agentpool_config/agentpool_tools.py +498 -0
  150. agentpool_config/converters.py +1 -1
  151. agentpool_config/event_handlers.py +42 -0
  152. agentpool_config/events.py +1 -1
  153. agentpool_config/forward_targets.py +1 -4
  154. agentpool_config/jinja.py +3 -3
  155. agentpool_config/mcp_server.py +1 -5
  156. agentpool_config/nodes.py +1 -1
  157. agentpool_config/observability.py +44 -0
  158. agentpool_config/session.py +0 -3
  159. agentpool_config/storage.py +38 -39
  160. agentpool_config/task.py +3 -3
  161. agentpool_config/tools.py +11 -28
  162. agentpool_config/toolsets.py +22 -90
  163. agentpool_server/a2a_server/agent_worker.py +307 -0
  164. agentpool_server/a2a_server/server.py +23 -18
  165. agentpool_server/acp_server/acp_agent.py +125 -56
  166. agentpool_server/acp_server/commands/acp_commands.py +46 -216
  167. agentpool_server/acp_server/commands/docs_commands/fetch_repo.py +8 -7
  168. agentpool_server/acp_server/event_converter.py +651 -0
  169. agentpool_server/acp_server/input_provider.py +53 -10
  170. agentpool_server/acp_server/server.py +1 -11
  171. agentpool_server/acp_server/session.py +90 -410
  172. agentpool_server/acp_server/session_manager.py +8 -34
  173. agentpool_server/agui_server/server.py +3 -1
  174. agentpool_server/mcp_server/server.py +5 -2
  175. agentpool_server/opencode_server/ENDPOINTS.md +53 -14
  176. agentpool_server/opencode_server/OPENCODE_UI_TOOLS_COMPLETE.md +202 -0
  177. agentpool_server/opencode_server/__init__.py +0 -8
  178. agentpool_server/opencode_server/converters.py +132 -26
  179. agentpool_server/opencode_server/input_provider.py +160 -8
  180. agentpool_server/opencode_server/models/__init__.py +42 -20
  181. agentpool_server/opencode_server/models/app.py +12 -0
  182. agentpool_server/opencode_server/models/events.py +203 -29
  183. agentpool_server/opencode_server/models/mcp.py +19 -0
  184. agentpool_server/opencode_server/models/message.py +18 -1
  185. agentpool_server/opencode_server/models/parts.py +134 -1
  186. agentpool_server/opencode_server/models/question.py +56 -0
  187. agentpool_server/opencode_server/models/session.py +13 -1
  188. agentpool_server/opencode_server/routes/__init__.py +4 -0
  189. agentpool_server/opencode_server/routes/agent_routes.py +33 -2
  190. agentpool_server/opencode_server/routes/app_routes.py +66 -3
  191. agentpool_server/opencode_server/routes/config_routes.py +66 -5
  192. agentpool_server/opencode_server/routes/file_routes.py +184 -5
  193. agentpool_server/opencode_server/routes/global_routes.py +1 -1
  194. agentpool_server/opencode_server/routes/lsp_routes.py +1 -1
  195. agentpool_server/opencode_server/routes/message_routes.py +122 -66
  196. agentpool_server/opencode_server/routes/permission_routes.py +63 -0
  197. agentpool_server/opencode_server/routes/pty_routes.py +23 -22
  198. agentpool_server/opencode_server/routes/question_routes.py +128 -0
  199. agentpool_server/opencode_server/routes/session_routes.py +139 -68
  200. agentpool_server/opencode_server/routes/tui_routes.py +1 -1
  201. agentpool_server/opencode_server/server.py +47 -2
  202. agentpool_server/opencode_server/state.py +30 -0
  203. agentpool_storage/__init__.py +0 -4
  204. agentpool_storage/base.py +81 -2
  205. agentpool_storage/claude_provider/ARCHITECTURE.md +433 -0
  206. agentpool_storage/claude_provider/__init__.py +42 -0
  207. agentpool_storage/{claude_provider.py → claude_provider/provider.py} +190 -8
  208. agentpool_storage/file_provider.py +149 -15
  209. agentpool_storage/memory_provider.py +132 -12
  210. agentpool_storage/opencode_provider/ARCHITECTURE.md +386 -0
  211. agentpool_storage/opencode_provider/__init__.py +16 -0
  212. agentpool_storage/opencode_provider/helpers.py +414 -0
  213. agentpool_storage/opencode_provider/provider.py +895 -0
  214. agentpool_storage/session_store.py +20 -6
  215. agentpool_storage/sql_provider/sql_provider.py +135 -2
  216. agentpool_storage/sql_provider/utils.py +2 -12
  217. agentpool_storage/zed_provider/__init__.py +16 -0
  218. agentpool_storage/zed_provider/helpers.py +281 -0
  219. agentpool_storage/zed_provider/models.py +130 -0
  220. agentpool_storage/zed_provider/provider.py +442 -0
  221. agentpool_storage/zed_provider.py +803 -0
  222. agentpool_toolsets/__init__.py +0 -2
  223. agentpool_toolsets/builtin/__init__.py +2 -4
  224. agentpool_toolsets/builtin/code.py +4 -4
  225. agentpool_toolsets/builtin/debug.py +115 -40
  226. agentpool_toolsets/builtin/execution_environment.py +54 -165
  227. agentpool_toolsets/builtin/skills.py +0 -77
  228. agentpool_toolsets/builtin/subagent_tools.py +64 -51
  229. agentpool_toolsets/builtin/workers.py +4 -2
  230. agentpool_toolsets/composio_toolset.py +2 -2
  231. agentpool_toolsets/entry_points.py +3 -1
  232. agentpool_toolsets/fsspec_toolset/grep.py +25 -5
  233. agentpool_toolsets/fsspec_toolset/helpers.py +3 -2
  234. agentpool_toolsets/fsspec_toolset/toolset.py +350 -66
  235. agentpool_toolsets/mcp_discovery/data/mcp_servers.parquet +0 -0
  236. agentpool_toolsets/mcp_discovery/toolset.py +74 -17
  237. agentpool_toolsets/mcp_run_toolset.py +8 -11
  238. agentpool_toolsets/notifications.py +33 -33
  239. agentpool_toolsets/openapi.py +3 -1
  240. agentpool_toolsets/search_toolset.py +3 -1
  241. agentpool_config/resources.py +0 -33
  242. agentpool_server/acp_server/acp_tools.py +0 -43
  243. agentpool_server/acp_server/commands/spawn.py +0 -210
  244. agentpool_storage/opencode_provider.py +0 -730
  245. agentpool_storage/text_log_provider.py +0 -276
  246. agentpool_toolsets/builtin/chain.py +0 -288
  247. agentpool_toolsets/builtin/user_interaction.py +0 -52
  248. agentpool_toolsets/semantic_memory_toolset.py +0 -536
  249. {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/entry_points.txt +0 -0
  250. {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -120,6 +120,71 @@ async def detailed_print_handler(ctx: RunContext, event: RichAgentStreamEvent[An
120
120
  print(file=sys.stderr) # Final newline
121
121
 
122
122
 
123
+ def create_file_stream_handler(
124
+ path: str,
125
+ mode: str = "a",
126
+ include_tools: bool = False,
127
+ include_thinking: bool = False,
128
+ ) -> IndividualEventHandler:
129
+ """Create an event handler that streams text output to a file.
130
+
131
+ Args:
132
+ path: Path to the output file
133
+ mode: File open mode ('w' for overwrite, 'a' for append)
134
+ include_tools: Whether to include tool call/result information
135
+ include_thinking: Whether to include thinking content
136
+
137
+ Returns:
138
+ Event handler function that writes to the specified file
139
+ """
140
+ from pathlib import Path
141
+
142
+ file_path = Path(path).expanduser()
143
+ file_path.parent.mkdir(parents=True, exist_ok=True)
144
+
145
+ # Open file handle that persists across calls
146
+ file_handle = file_path.open(mode, encoding="utf-8")
147
+
148
+ async def file_stream_handler(ctx: RunContext, event: RichAgentStreamEvent[Any]) -> None:
149
+ """Stream agent output to file."""
150
+ match event:
151
+ case (
152
+ PartStartEvent(part=TextPart(content=delta))
153
+ | PartDeltaEvent(delta=TextPartDelta(content_delta=delta))
154
+ ):
155
+ file_handle.write(delta)
156
+ file_handle.flush()
157
+
158
+ case (
159
+ PartStartEvent(part=ThinkingPart(content=delta))
160
+ | PartDeltaEvent(delta=ThinkingPartDelta(content_delta=delta))
161
+ ):
162
+ if include_thinking and delta:
163
+ file_handle.write(f"\n[thinking] {delta}")
164
+ file_handle.flush()
165
+
166
+ case FunctionToolCallEvent(part=ToolCallPart() as part):
167
+ if include_tools:
168
+ kwargs_str = ", ".join(f"{k}={v!r}" for k, v in safe_args_as_dict(part).items())
169
+ file_handle.write(f"\n[tool] {part.tool_name}({kwargs_str})\n")
170
+ file_handle.flush()
171
+
172
+ case FunctionToolResultEvent(result=ToolReturnPart() as return_part):
173
+ if include_tools:
174
+ file_handle.write(f"[result] {return_part.content}\n")
175
+ file_handle.flush()
176
+
177
+ case RunErrorEvent(message=message):
178
+ file_handle.write(f"\n[error] {message}\n")
179
+ file_handle.flush()
180
+
181
+ case StreamCompleteEvent():
182
+ file_handle.write("\n")
183
+ file_handle.flush()
184
+
185
+ return file_stream_handler
186
+
187
+
123
188
  def resolve_event_handlers(
124
189
  event_handlers: Sequence[IndividualEventHandler | BuiltinEventHandlerType] | None,
125
190
  ) -> list[IndividualEventHandler]:
@@ -146,6 +146,7 @@ class StreamEventEmitter:
146
146
  path: str,
147
147
  success: bool,
148
148
  error: str | None = None,
149
+ line: int = 0,
149
150
  ) -> None:
150
151
  """Emit file operation event.
151
152
 
@@ -154,6 +155,7 @@ class StreamEventEmitter:
154
155
  path: The file/directory path that was operated on
155
156
  success: Whether the operation completed successfully
156
157
  error: Error message if operation failed
158
+ line: Line number for navigation (0 = beginning)
157
159
  """
158
160
  event = ToolCallProgressEvent.file_operation(
159
161
  tool_call_id=self._context.tool_call_id or "",
@@ -162,6 +164,7 @@ class StreamEventEmitter:
162
164
  path=path,
163
165
  success=success,
164
166
  error=error,
167
+ line=line,
165
168
  )
166
169
  await self._emit(event)
167
170
 
@@ -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,26 @@ 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
+
546
625
  @dataclass(kw_only=True)
547
626
  class CompactionEvent:
548
627
  """Event indicating context compaction is starting or completed.
@@ -572,6 +651,8 @@ type RichAgentStreamEvent[OutputDataT] = (
572
651
  | ToolCallCompleteEvent
573
652
  | PlanUpdateEvent
574
653
  | CompactionEvent
654
+ | SubAgentEvent
655
+ | ToolResultMetadataEvent
575
656
  | CustomEvent[Any]
576
657
  )
577
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