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
@@ -0,0 +1,242 @@
1
+ """Claude Code usage limits helper.
2
+
3
+ Fetches usage information from Anthropic's OAuth API using stored credentials.
4
+ Works on both Linux and macOS.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from datetime import datetime
10
+ from pathlib import Path
11
+ import subprocess
12
+ import sys
13
+
14
+ import anyenv
15
+ import httpx
16
+ from pydantic import BaseModel
17
+
18
+
19
+ class UsageLimit(BaseModel):
20
+ """A single usage limit with utilization percentage and reset time."""
21
+
22
+ utilization: float
23
+ """Utilization percentage (0-100)."""
24
+
25
+ resets_at: datetime | None = None
26
+ """When this limit resets, or None if not set."""
27
+
28
+
29
+ class ExtraUsage(BaseModel):
30
+ """Extra usage information for paid plans."""
31
+
32
+ is_enabled: bool = False
33
+ monthly_limit: float | None = None
34
+ used_credits: float | None = None
35
+ utilization: float | None = None
36
+
37
+
38
+ class ClaudeCodeUsage(BaseModel):
39
+ """Claude Code usage limits."""
40
+
41
+ five_hour: UsageLimit | None = None
42
+ """5-hour rolling usage limit."""
43
+
44
+ seven_day: UsageLimit | None = None
45
+ """7-day rolling usage limit."""
46
+
47
+ seven_day_opus: UsageLimit | None = None
48
+ """7-day Opus-specific limit."""
49
+
50
+ seven_day_sonnet: UsageLimit | None = None
51
+ """7-day Sonnet-specific limit."""
52
+
53
+ seven_day_oauth_apps: UsageLimit | None = None
54
+ """7-day OAuth apps limit."""
55
+
56
+ extra_usage: ExtraUsage | None = None
57
+ """Extra usage info for paid plans."""
58
+
59
+ def format_table(self) -> str:
60
+ """Format usage as a readable table."""
61
+ lines = ["Claude Code Usage Limits", "=" * 50]
62
+
63
+ def format_limit(name: str, limit: UsageLimit | None) -> str | None:
64
+ if limit is None:
65
+ return None
66
+ reset_str = ""
67
+ if limit.resets_at:
68
+ reset_str = f" (resets {limit.resets_at.strftime('%Y-%m-%d %H:%M UTC')})"
69
+ return f"{name}: {limit.utilization:.0f}%{reset_str}"
70
+
71
+ for name, limit in [
72
+ ("5-hour", self.five_hour),
73
+ ("7-day", self.seven_day),
74
+ ("7-day Opus", self.seven_day_opus),
75
+ ("7-day Sonnet", self.seven_day_sonnet),
76
+ ("7-day OAuth Apps", self.seven_day_oauth_apps),
77
+ ]:
78
+ formatted = format_limit(name, limit)
79
+ if formatted:
80
+ lines.append(formatted)
81
+
82
+ if self.extra_usage and self.extra_usage.is_enabled:
83
+ lines.append("")
84
+ lines.append("Extra Usage (paid):")
85
+ if self.extra_usage.utilization is not None:
86
+ lines.append(f" Utilization: {self.extra_usage.utilization:.0f}%")
87
+ if self.extra_usage.used_credits is not None:
88
+ lines.append(f" Used credits: {self.extra_usage.used_credits}")
89
+ if self.extra_usage.monthly_limit is not None:
90
+ lines.append(f" Monthly limit: {self.extra_usage.monthly_limit}")
91
+
92
+ return "\n".join(lines)
93
+
94
+
95
+ def _get_credentials_path() -> Path | None:
96
+ """Get the path to Claude Code credentials file.
97
+
98
+ Returns:
99
+ Path to credentials file, or None if not found.
100
+ """
101
+ # Linux: ~/.claude/.credentials.json
102
+ linux_path = Path.home() / ".claude" / ".credentials.json"
103
+ if linux_path.exists():
104
+ return linux_path
105
+
106
+ # macOS: Also check ~/.claude first (newer versions)
107
+ if linux_path.exists():
108
+ return linux_path
109
+
110
+ return None
111
+
112
+
113
+ def _get_access_token_from_file(path: Path) -> str | None:
114
+ """Read access token from credentials file."""
115
+ try:
116
+ data = anyenv.load_json(path.read_text(), return_type=dict)
117
+ val = data.get("claudeAiOauth", {}).get("accessToken")
118
+ except (anyenv.JsonLoadError, OSError):
119
+ return None
120
+ else:
121
+ assert isinstance(val, str)
122
+ return val
123
+
124
+
125
+ def _get_access_token_from_keychain() -> str | None:
126
+ """Read access token from macOS Keychain."""
127
+ if sys.platform != "darwin":
128
+ return None
129
+
130
+ try:
131
+ result = subprocess.run(
132
+ ["security", "find-generic-password", "-s", "Claude Code-credentials", "-w"],
133
+ capture_output=True,
134
+ text=True,
135
+ check=True,
136
+ )
137
+ data = anyenv.load_json(result.stdout.strip(), return_type=dict)
138
+ val = data.get("claudeAiOauth", {}).get("accessToken")
139
+ except (subprocess.CalledProcessError, anyenv.JsonLoadError, FileNotFoundError):
140
+ return None
141
+ else:
142
+ assert isinstance(val, str)
143
+ return val
144
+
145
+
146
+ def get_access_token() -> str | None:
147
+ """Get Claude Code OAuth access token.
148
+
149
+ Checks both file-based storage (Linux/newer macOS) and Keychain (macOS).
150
+
151
+ Returns:
152
+ Access token string, or None if not found.
153
+ """
154
+ # Try file-based first (works on Linux and newer macOS)
155
+ creds_path = _get_credentials_path()
156
+ if creds_path:
157
+ token = _get_access_token_from_file(creds_path)
158
+ if token:
159
+ return token
160
+ # Fall back to macOS Keychain
161
+ return _get_access_token_from_keychain()
162
+
163
+
164
+ async def get_usage_async(token: str | None = None) -> ClaudeCodeUsage:
165
+ """Fetch Claude Code usage limits asynchronously.
166
+
167
+ Args:
168
+ token: OAuth access token. If not provided, will attempt to read from
169
+ stored credentials.
170
+
171
+ Returns:
172
+ ClaudeCodeUsage with current limits.
173
+
174
+ Raises:
175
+ ValueError: If no token provided and credentials not found.
176
+ httpx.HTTPStatusError: If API request fails.
177
+ """
178
+ if token is None:
179
+ token = get_access_token()
180
+ if token is None:
181
+ msg = "No Claude Code credentials found. Please authenticate with Claude Code first."
182
+ raise ValueError(msg)
183
+
184
+ async with httpx.AsyncClient() as client:
185
+ response = await client.get(
186
+ "https://api.anthropic.com/api/oauth/usage",
187
+ headers={
188
+ "Accept": "application/json",
189
+ "Content-Type": "application/json",
190
+ "User-Agent": "claude-code/2.0.32",
191
+ "Authorization": f"Bearer {token}",
192
+ "anthropic-beta": "oauth-2025-04-20",
193
+ },
194
+ )
195
+ response.raise_for_status()
196
+ return ClaudeCodeUsage.model_validate(response.json())
197
+
198
+
199
+ def get_usage(token: str | None = None) -> ClaudeCodeUsage:
200
+ """Fetch Claude Code usage limits synchronously.
201
+
202
+ Args:
203
+ token: OAuth access token. If not provided, will attempt to read from
204
+ stored credentials.
205
+
206
+ Returns:
207
+ ClaudeCodeUsage with current limits.
208
+
209
+ Raises:
210
+ ValueError: If no token provided and credentials not found.
211
+ httpx.HTTPStatusError: If API request fails.
212
+ """
213
+ if token is None:
214
+ token = get_access_token()
215
+ if token is None:
216
+ msg = "No Claude Code credentials found. Please authenticate with Claude Code first."
217
+ raise ValueError(msg)
218
+
219
+ with httpx.Client() as client:
220
+ response = client.get(
221
+ "https://api.anthropic.com/api/oauth/usage",
222
+ headers={
223
+ "Accept": "application/json",
224
+ "Content-Type": "application/json",
225
+ "User-Agent": "claude-code/2.0.32",
226
+ "Authorization": f"Bearer {token}",
227
+ "anthropic-beta": "oauth-2025-04-20",
228
+ },
229
+ )
230
+ response.raise_for_status()
231
+ return ClaudeCodeUsage.model_validate(response.json())
232
+
233
+
234
+ if __name__ == "__main__":
235
+ # Quick test
236
+ try:
237
+ usage = get_usage()
238
+ print(usage.format_table())
239
+ except ValueError as e:
240
+ print(f"Error: {e}")
241
+ except httpx.HTTPStatusError as e:
242
+ print(f"API Error: {e.response.status_code} - {e.response.text}")
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from contextvars import ContextVar
5
6
  from dataclasses import dataclass, field
6
7
  from typing import TYPE_CHECKING, Any, Literal
7
8
 
@@ -9,6 +10,45 @@ from agentpool.log import get_logger
9
10
  from agentpool.messaging.context import NodeContext
10
11
 
11
12
 
13
+ if TYPE_CHECKING:
14
+ from contextvars import Token
15
+
16
+
17
+ # ContextVar for passing deps through async call boundaries (e.g., MCP tool bridge)
18
+ # This allows run_stream() to set deps that are accessible in tool invocations
19
+ _current_deps: ContextVar[Any] = ContextVar("current_deps", default=None)
20
+
21
+
22
+ def set_current_deps(deps: Any) -> Token[Any]:
23
+ """Set the current deps for the running context.
24
+
25
+ Args:
26
+ deps: Dependencies to set
27
+
28
+ Returns:
29
+ Token to reset the deps when done
30
+ """
31
+ return _current_deps.set(deps)
32
+
33
+
34
+ def get_current_deps() -> Any:
35
+ """Get the current deps from the running context.
36
+
37
+ Returns:
38
+ Current deps or None if not set
39
+ """
40
+ return _current_deps.get()
41
+
42
+
43
+ def reset_current_deps(token: Token[Any]) -> None:
44
+ """Reset deps to previous value.
45
+
46
+ Args:
47
+ token: Token from set_current_deps
48
+ """
49
+ _current_deps.reset(token)
50
+
51
+
12
52
  if TYPE_CHECKING:
13
53
  from mcp import types
14
54
 
@@ -3,22 +3,27 @@
3
3
  from .events import (
4
4
  CommandCompleteEvent,
5
5
  CommandOutputEvent,
6
+ CompactionEvent,
6
7
  CustomEvent,
7
8
  DiffContentItem,
8
9
  FileContentItem,
9
10
  LocationContentItem,
10
11
  PlanUpdateEvent,
12
+ PartStartEvent,
13
+ PartDeltaEvent,
11
14
  RichAgentStreamEvent,
12
15
  RunErrorEvent,
13
16
  RunStartedEvent,
14
17
  SlashedAgentStreamEvent,
15
18
  StreamCompleteEvent,
19
+ SubAgentEvent,
16
20
  TerminalContentItem,
17
21
  TextContentItem,
18
22
  ToolCallCompleteEvent,
19
23
  ToolCallContentItem,
20
24
  ToolCallProgressEvent,
21
25
  ToolCallStartEvent,
26
+ ToolResultMetadataEvent,
22
27
  )
23
28
  from .event_emitter import StreamEventEmitter
24
29
  from .builtin_handlers import (
@@ -31,17 +36,30 @@ from .tts_handlers import (
31
36
  EdgeTTSEventHandler,
32
37
  OpenAITTSEventHandler,
33
38
  )
39
+ from .processors import (
40
+ FileTracker,
41
+ FileTrackingProcessor,
42
+ StreamPipeline,
43
+ StreamProcessor,
44
+ event_handler_processor,
45
+ extract_file_path_from_tool_call,
46
+ )
34
47
 
35
48
  __all__ = [
36
49
  "BaseTTSEventHandler",
37
50
  "CommandCompleteEvent",
38
51
  "CommandOutputEvent",
52
+ "CompactionEvent",
39
53
  "CustomEvent",
40
54
  "DiffContentItem",
41
55
  "EdgeTTSEventHandler",
42
56
  "FileContentItem",
57
+ "FileTracker",
58
+ "FileTrackingProcessor",
43
59
  "LocationContentItem",
44
60
  "OpenAITTSEventHandler",
61
+ "PartDeltaEvent",
62
+ "PartStartEvent",
45
63
  "PlanUpdateEvent",
46
64
  "RichAgentStreamEvent",
47
65
  "RunErrorEvent",
@@ -49,13 +67,19 @@ __all__ = [
49
67
  "SlashedAgentStreamEvent",
50
68
  "StreamCompleteEvent",
51
69
  "StreamEventEmitter",
70
+ "StreamPipeline",
71
+ "StreamProcessor",
72
+ "SubAgentEvent",
52
73
  "TerminalContentItem",
53
74
  "TextContentItem",
54
75
  "ToolCallCompleteEvent",
55
76
  "ToolCallContentItem",
56
77
  "ToolCallProgressEvent",
57
78
  "ToolCallStartEvent",
79
+ "ToolResultMetadataEvent",
58
80
  "detailed_print_handler",
81
+ "event_handler_processor",
82
+ "extract_file_path_from_tool_call",
59
83
  "resolve_event_handlers",
60
84
  "simple_print_handler",
61
85
  ]
@@ -24,6 +24,7 @@ from agentpool.agents.events import (
24
24
  ToolCallProgressEvent,
25
25
  ToolCallStartEvent,
26
26
  )
27
+ from agentpool.utils.pydantic_ai_helpers import safe_args_as_dict
27
28
 
28
29
 
29
30
  if TYPE_CHECKING:
@@ -52,7 +53,7 @@ async def simple_print_handler(ctx: RunContext, event: RichAgentStreamEvent[Any]
52
53
  print(delta, end="", flush=True, file=sys.stderr)
53
54
 
54
55
  case FunctionToolCallEvent(part=ToolCallPart() as part):
55
- kwargs_str = ", ".join(f"{k}={v!r}" for k, v in part.args_as_dict().items())
56
+ kwargs_str = ", ".join(f"{k}={v!r}" for k, v in safe_args_as_dict(part).items())
56
57
  print(f"\n🔧 {part.tool_name}({kwargs_str})", flush=True, file=sys.stderr)
57
58
 
58
59
  case FunctionToolResultEvent(result=ToolReturnPart() as return_part):
@@ -119,6 +120,71 @@ async def detailed_print_handler(ctx: RunContext, event: RichAgentStreamEvent[An
119
120
  print(file=sys.stderr) # Final newline
120
121
 
121
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
+
122
188
  def resolve_event_handlers(
123
189
  event_handlers: Sequence[IndividualEventHandler | BuiltinEventHandlerType] | None,
124
190
  ) -> list[IndividualEventHandler]:
@@ -8,12 +8,15 @@ from agentpool.agents.events import (
8
8
  CustomEvent,
9
9
  LocationContentItem,
10
10
  PlanUpdateEvent,
11
+ TextContentItem,
11
12
  ToolCallProgressEvent,
12
13
  ToolCallStartEvent,
13
14
  )
14
15
 
15
16
 
16
17
  if TYPE_CHECKING:
18
+ from collections.abc import Sequence
19
+
17
20
  from agentpool.agents.context import AgentContext
18
21
  from agentpool.agents.events import RichAgentStreamEvent, ToolCallContentItem
19
22
  from agentpool.resource_providers.plan_provider import PlanEntry
@@ -78,7 +81,7 @@ class StreamEventEmitter:
78
81
  title: str,
79
82
  *,
80
83
  status: Literal["pending", "in_progress", "completed", "failed"] = "in_progress",
81
- items: list[ToolCallContentItem] | None = None,
84
+ items: Sequence[ToolCallContentItem | str] | None = None,
82
85
  replace_content: bool = False,
83
86
  ) -> None:
84
87
  """Emit a progress event.
@@ -89,12 +92,26 @@ class StreamEventEmitter:
89
92
  items: Rich content items (terminals, diffs, locations, text)
90
93
  replace_content: If True, items replace existing content instead of appending
91
94
  """
95
+ # Record file changes from DiffContentItem for diff/revert support
96
+ if self._context.pool and self._context.pool.file_ops and items:
97
+ from agentpool.agents.events import DiffContentItem
98
+
99
+ for item in items:
100
+ if isinstance(item, DiffContentItem):
101
+ self._context.pool.file_ops.record_change(
102
+ path=item.path,
103
+ old_content=item.old_text,
104
+ new_content=item.new_text,
105
+ operation="create" if item.old_text is None else "write",
106
+ agent_name=self._context.node_name,
107
+ )
108
+
92
109
  event = ToolCallProgressEvent(
93
110
  tool_call_id=self._context.tool_call_id or "",
94
111
  tool_name=self._context.tool_name,
95
112
  status=status,
96
113
  title=title,
97
- items=items or [],
114
+ items=[TextContentItem(text=i) if isinstance(i, str) else i for i in items or []],
98
115
  replace_content=replace_content,
99
116
  )
100
117
  await self._emit(event)
@@ -129,6 +146,7 @@ class StreamEventEmitter:
129
146
  path: str,
130
147
  success: bool,
131
148
  error: str | None = None,
149
+ line: int = 0,
132
150
  ) -> None:
133
151
  """Emit file operation event.
134
152
 
@@ -137,6 +155,7 @@ class StreamEventEmitter:
137
155
  path: The file/directory path that was operated on
138
156
  success: Whether the operation completed successfully
139
157
  error: Error message if operation failed
158
+ line: Line number for navigation (0 = beginning)
140
159
  """
141
160
  event = ToolCallProgressEvent.file_operation(
142
161
  tool_call_id=self._context.tool_call_id or "",
@@ -145,6 +164,7 @@ class StreamEventEmitter:
145
164
  path=path,
146
165
  success=success,
147
166
  error=error,
167
+ line=line,
148
168
  )
149
169
  await self._emit(event)
150
170
 
@@ -163,6 +183,16 @@ class StreamEventEmitter:
163
183
  new_text: New file content
164
184
  status: Current status of the edit operation
165
185
  """
186
+ # Record file change for diff/revert support
187
+ if self._context.pool and self._context.pool.file_ops:
188
+ self._context.pool.file_ops.record_change(
189
+ path=path,
190
+ old_content=old_text,
191
+ new_content=new_text,
192
+ operation="edit",
193
+ agent_name=self._context.node_name,
194
+ )
195
+
166
196
  event = ToolCallProgressEvent.file_edit(
167
197
  tool_call_id=self._context.tool_call_id or "",
168
198
  tool_name=self._context.tool_name,