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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. acp/__init__.py +13 -0
  2. acp/bridge/README.md +15 -2
  3. acp/bridge/__init__.py +3 -2
  4. acp/bridge/__main__.py +60 -19
  5. acp/bridge/ws_server.py +173 -0
  6. acp/bridge/ws_server_cli.py +89 -0
  7. acp/notifications.py +2 -1
  8. acp/stdio.py +39 -9
  9. acp/transports.py +362 -2
  10. acp/utils.py +15 -2
  11. agentpool/__init__.py +4 -1
  12. agentpool/agents/__init__.py +2 -0
  13. agentpool/agents/acp_agent/acp_agent.py +203 -88
  14. agentpool/agents/acp_agent/acp_converters.py +46 -21
  15. agentpool/agents/acp_agent/client_handler.py +157 -3
  16. agentpool/agents/acp_agent/session_state.py +4 -1
  17. agentpool/agents/agent.py +314 -107
  18. agentpool/agents/agui_agent/__init__.py +0 -2
  19. agentpool/agents/agui_agent/agui_agent.py +90 -21
  20. agentpool/agents/agui_agent/agui_converters.py +0 -131
  21. agentpool/agents/base_agent.py +163 -1
  22. agentpool/agents/claude_code_agent/claude_code_agent.py +626 -179
  23. agentpool/agents/claude_code_agent/converters.py +71 -3
  24. agentpool/agents/claude_code_agent/history.py +474 -0
  25. agentpool/agents/context.py +40 -0
  26. agentpool/agents/events/__init__.py +2 -0
  27. agentpool/agents/events/builtin_handlers.py +2 -1
  28. agentpool/agents/events/event_emitter.py +29 -2
  29. agentpool/agents/events/events.py +20 -0
  30. agentpool/agents/modes.py +54 -0
  31. agentpool/agents/tool_call_accumulator.py +213 -0
  32. agentpool/common_types.py +21 -0
  33. agentpool/config_resources/__init__.py +38 -1
  34. agentpool/config_resources/claude_code_agent.yml +3 -0
  35. agentpool/delegation/pool.py +37 -29
  36. agentpool/delegation/team.py +1 -0
  37. agentpool/delegation/teamrun.py +1 -0
  38. agentpool/diagnostics/__init__.py +53 -0
  39. agentpool/diagnostics/lsp_manager.py +1593 -0
  40. agentpool/diagnostics/lsp_proxy.py +41 -0
  41. agentpool/diagnostics/lsp_proxy_script.py +229 -0
  42. agentpool/diagnostics/models.py +398 -0
  43. agentpool/mcp_server/__init__.py +0 -2
  44. agentpool/mcp_server/client.py +12 -3
  45. agentpool/mcp_server/manager.py +25 -31
  46. agentpool/mcp_server/registries/official_registry_client.py +25 -0
  47. agentpool/mcp_server/tool_bridge.py +78 -66
  48. agentpool/messaging/__init__.py +0 -2
  49. agentpool/messaging/compaction.py +72 -197
  50. agentpool/messaging/message_history.py +12 -0
  51. agentpool/messaging/messages.py +52 -9
  52. agentpool/messaging/processing.py +3 -1
  53. agentpool/models/acp_agents/base.py +0 -22
  54. agentpool/models/acp_agents/mcp_capable.py +8 -148
  55. agentpool/models/acp_agents/non_mcp.py +129 -72
  56. agentpool/models/agents.py +35 -13
  57. agentpool/models/claude_code_agents.py +33 -2
  58. agentpool/models/manifest.py +43 -0
  59. agentpool/repomap.py +1 -1
  60. agentpool/resource_providers/__init__.py +9 -1
  61. agentpool/resource_providers/aggregating.py +52 -3
  62. agentpool/resource_providers/base.py +57 -1
  63. agentpool/resource_providers/mcp_provider.py +23 -0
  64. agentpool/resource_providers/plan_provider.py +130 -41
  65. agentpool/resource_providers/pool.py +2 -0
  66. agentpool/resource_providers/static.py +2 -0
  67. agentpool/sessions/__init__.py +2 -1
  68. agentpool/sessions/manager.py +31 -2
  69. agentpool/sessions/models.py +50 -0
  70. agentpool/skills/registry.py +13 -8
  71. agentpool/storage/manager.py +217 -1
  72. agentpool/testing.py +537 -19
  73. agentpool/utils/file_watcher.py +269 -0
  74. agentpool/utils/identifiers.py +121 -0
  75. agentpool/utils/pydantic_ai_helpers.py +46 -0
  76. agentpool/utils/streams.py +690 -1
  77. agentpool/utils/subprocess_utils.py +155 -0
  78. agentpool/utils/token_breakdown.py +461 -0
  79. {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/METADATA +27 -7
  80. {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/RECORD +170 -112
  81. {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/WHEEL +1 -1
  82. agentpool_cli/__main__.py +4 -0
  83. agentpool_cli/serve_acp.py +41 -20
  84. agentpool_cli/serve_agui.py +87 -0
  85. agentpool_cli/serve_opencode.py +119 -0
  86. agentpool_commands/__init__.py +30 -0
  87. agentpool_commands/agents.py +74 -1
  88. agentpool_commands/history.py +62 -0
  89. agentpool_commands/mcp.py +176 -0
  90. agentpool_commands/models.py +56 -3
  91. agentpool_commands/tools.py +57 -0
  92. agentpool_commands/utils.py +51 -0
  93. agentpool_config/builtin_tools.py +77 -22
  94. agentpool_config/commands.py +24 -1
  95. agentpool_config/compaction.py +258 -0
  96. agentpool_config/mcp_server.py +131 -1
  97. agentpool_config/storage.py +46 -1
  98. agentpool_config/tools.py +7 -1
  99. agentpool_config/toolsets.py +92 -148
  100. agentpool_server/acp_server/acp_agent.py +134 -150
  101. agentpool_server/acp_server/commands/acp_commands.py +216 -51
  102. agentpool_server/acp_server/commands/docs_commands/fetch_repo.py +10 -10
  103. agentpool_server/acp_server/server.py +23 -79
  104. agentpool_server/acp_server/session.py +181 -19
  105. agentpool_server/opencode_server/.rules +95 -0
  106. agentpool_server/opencode_server/ENDPOINTS.md +362 -0
  107. agentpool_server/opencode_server/__init__.py +27 -0
  108. agentpool_server/opencode_server/command_validation.py +172 -0
  109. agentpool_server/opencode_server/converters.py +869 -0
  110. agentpool_server/opencode_server/dependencies.py +24 -0
  111. agentpool_server/opencode_server/input_provider.py +269 -0
  112. agentpool_server/opencode_server/models/__init__.py +228 -0
  113. agentpool_server/opencode_server/models/agent.py +53 -0
  114. agentpool_server/opencode_server/models/app.py +60 -0
  115. agentpool_server/opencode_server/models/base.py +26 -0
  116. agentpool_server/opencode_server/models/common.py +23 -0
  117. agentpool_server/opencode_server/models/config.py +37 -0
  118. agentpool_server/opencode_server/models/events.py +647 -0
  119. agentpool_server/opencode_server/models/file.py +88 -0
  120. agentpool_server/opencode_server/models/mcp.py +25 -0
  121. agentpool_server/opencode_server/models/message.py +162 -0
  122. agentpool_server/opencode_server/models/parts.py +190 -0
  123. agentpool_server/opencode_server/models/provider.py +81 -0
  124. agentpool_server/opencode_server/models/pty.py +43 -0
  125. agentpool_server/opencode_server/models/session.py +99 -0
  126. agentpool_server/opencode_server/routes/__init__.py +25 -0
  127. agentpool_server/opencode_server/routes/agent_routes.py +442 -0
  128. agentpool_server/opencode_server/routes/app_routes.py +139 -0
  129. agentpool_server/opencode_server/routes/config_routes.py +241 -0
  130. agentpool_server/opencode_server/routes/file_routes.py +392 -0
  131. agentpool_server/opencode_server/routes/global_routes.py +94 -0
  132. agentpool_server/opencode_server/routes/lsp_routes.py +319 -0
  133. agentpool_server/opencode_server/routes/message_routes.py +705 -0
  134. agentpool_server/opencode_server/routes/pty_routes.py +299 -0
  135. agentpool_server/opencode_server/routes/session_routes.py +1205 -0
  136. agentpool_server/opencode_server/routes/tui_routes.py +139 -0
  137. agentpool_server/opencode_server/server.py +430 -0
  138. agentpool_server/opencode_server/state.py +121 -0
  139. agentpool_server/opencode_server/time_utils.py +8 -0
  140. agentpool_storage/__init__.py +16 -0
  141. agentpool_storage/base.py +103 -0
  142. agentpool_storage/claude_provider.py +907 -0
  143. agentpool_storage/file_provider.py +129 -0
  144. agentpool_storage/memory_provider.py +61 -0
  145. agentpool_storage/models.py +3 -0
  146. agentpool_storage/opencode_provider.py +730 -0
  147. agentpool_storage/project_store.py +325 -0
  148. agentpool_storage/session_store.py +6 -0
  149. agentpool_storage/sql_provider/__init__.py +4 -2
  150. agentpool_storage/sql_provider/models.py +48 -0
  151. agentpool_storage/sql_provider/sql_provider.py +134 -1
  152. agentpool_storage/sql_provider/utils.py +10 -1
  153. agentpool_storage/text_log_provider.py +1 -0
  154. agentpool_toolsets/builtin/__init__.py +0 -8
  155. agentpool_toolsets/builtin/code.py +95 -56
  156. agentpool_toolsets/builtin/debug.py +16 -21
  157. agentpool_toolsets/builtin/execution_environment.py +99 -103
  158. agentpool_toolsets/builtin/file_edit/file_edit.py +115 -7
  159. agentpool_toolsets/builtin/skills.py +86 -4
  160. agentpool_toolsets/fsspec_toolset/__init__.py +13 -1
  161. agentpool_toolsets/fsspec_toolset/diagnostics.py +860 -73
  162. agentpool_toolsets/fsspec_toolset/grep.py +74 -2
  163. agentpool_toolsets/fsspec_toolset/image_utils.py +161 -0
  164. agentpool_toolsets/fsspec_toolset/toolset.py +159 -38
  165. agentpool_toolsets/mcp_discovery/__init__.py +5 -0
  166. agentpool_toolsets/mcp_discovery/data/mcp_servers.parquet +0 -0
  167. agentpool_toolsets/mcp_discovery/toolset.py +454 -0
  168. agentpool_toolsets/mcp_run_toolset.py +84 -6
  169. agentpool_toolsets/builtin/agent_management.py +0 -239
  170. agentpool_toolsets/builtin/history.py +0 -36
  171. agentpool_toolsets/builtin/integration.py +0 -85
  172. agentpool_toolsets/builtin/tool_management.py +0 -90
  173. {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/entry_points.txt +0 -0
  174. {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/licenses/LICENSE +0 -0
@@ -8,7 +8,7 @@ event types as native agents.
8
8
  from __future__ import annotations
9
9
 
10
10
  from dataclasses import dataclass, field
11
- from typing import TYPE_CHECKING, Any
11
+ from typing import TYPE_CHECKING, Any, cast
12
12
 
13
13
  from pydantic_ai import PartDeltaEvent, TextPartDelta, ThinkingPartDelta
14
14
 
@@ -21,10 +21,11 @@ from agentpool.agents.events import (
21
21
 
22
22
 
23
23
  if TYPE_CHECKING:
24
- from claude_agent_sdk import ContentBlock, Message, ToolUseBlock
24
+ from claude_agent_sdk import ContentBlock, McpServerConfig, Message, ToolUseBlock
25
25
 
26
26
  from agentpool.agents.events import RichAgentStreamEvent, ToolCallContentItem
27
27
  from agentpool.tools.base import ToolKind
28
+ from agentpool_config.mcp_server import MCPServerConfig as NativeMCPServerConfig
28
29
 
29
30
 
30
31
  @dataclass
@@ -49,7 +50,7 @@ def derive_rich_tool_info(name: str, input_data: dict[str, Any]) -> RichToolInfo
49
50
  built-in tools and MCP bridge tools.
50
51
 
51
52
  Args:
52
- name: The tool name (e.g., "Read", "mcp__server__read_file")
53
+ name: The tool name (e.g., "Read", "mcp__server__read")
53
54
  input_data: The tool input arguments
54
55
 
55
56
  Returns:
@@ -241,3 +242,70 @@ def claude_message_to_events(
241
242
  pass
242
243
 
243
244
  return events
245
+
246
+
247
+ def convert_mcp_servers_to_sdk_format(
248
+ mcp_servers: list[NativeMCPServerConfig],
249
+ ) -> dict[str, McpServerConfig]:
250
+ """Convert internal MCPServerConfig to Claude SDK format.
251
+
252
+ Returns:
253
+ Dict mapping server names to SDK-compatible config dicts
254
+ """
255
+ from claude_agent_sdk import McpServerConfig
256
+
257
+ from agentpool_config.mcp_server import (
258
+ SSEMCPServerConfig,
259
+ StdioMCPServerConfig,
260
+ StreamableHTTPMCPServerConfig,
261
+ )
262
+
263
+ result: dict[str, McpServerConfig] = {}
264
+
265
+ for idx, server in enumerate(mcp_servers):
266
+ # Determine server name
267
+ if server.name:
268
+ name = server.name
269
+ elif isinstance(server, StdioMCPServerConfig) and server.args:
270
+ name = server.args[-1].split("/")[-1].split("@")[0]
271
+ elif isinstance(server, StdioMCPServerConfig):
272
+ name = server.command
273
+ elif isinstance(server, SSEMCPServerConfig | StreamableHTTPMCPServerConfig):
274
+ from urllib.parse import urlparse
275
+
276
+ name = urlparse(str(server.url)).hostname or f"server_{idx}"
277
+ else:
278
+ name = f"server_{idx}"
279
+
280
+ # Build SDK-compatible config
281
+ config: dict[str, Any]
282
+ match server:
283
+ case StdioMCPServerConfig(command=command, args=args):
284
+ config = {"type": "stdio", "command": command, "args": args}
285
+ if server.env:
286
+ config["env"] = server.get_env_vars()
287
+ case SSEMCPServerConfig(url=url):
288
+ config = {"type": "sse", "url": str(url)}
289
+ if server.headers:
290
+ config["headers"] = server.headers
291
+ case StreamableHTTPMCPServerConfig(url=url):
292
+ config = {"type": "http", "url": str(url)}
293
+ if server.headers:
294
+ config["headers"] = server.headers
295
+
296
+ result[name] = cast(McpServerConfig, config)
297
+
298
+ return result
299
+
300
+
301
+ def to_output_format(output_type: type) -> dict[str, Any] | None:
302
+ """Convert to SDK output format dict."""
303
+ from pydantic import TypeAdapter
304
+
305
+ # Build structured output format if needed
306
+ output_format: dict[str, Any] | None = None
307
+ if output_type is not str:
308
+ adapter = TypeAdapter[Any](output_type)
309
+ schema = adapter.json_schema()
310
+ output_format = {"type": "json_schema", "schema": schema}
311
+ return output_format
@@ -0,0 +1,474 @@
1
+ """Claude Code message history loader and converter.
2
+
3
+ This module provides utilities for loading Claude Code's conversation history
4
+ from ~/.claude/projects/ and converting it to pydantic-ai message format.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from datetime import datetime
10
+ import json
11
+ from typing import TYPE_CHECKING, Annotated, Any, Literal
12
+
13
+
14
+ if TYPE_CHECKING:
15
+ from pathlib import Path
16
+
17
+ from pydantic_ai import ModelRequest, ModelResponse
18
+
19
+ from pydantic import BaseModel, Field
20
+
21
+
22
+ # Claude Code history entry types
23
+
24
+
25
+ class ClaudeCodeUsage(BaseModel):
26
+ """Token usage information from Claude Code."""
27
+
28
+ input_tokens: int = 0
29
+ output_tokens: int = 0
30
+ cache_creation_input_tokens: int = 0
31
+ cache_read_input_tokens: int = 0
32
+
33
+
34
+ class ClaudeCodeTextContent(BaseModel):
35
+ """Text content block in Claude Code messages."""
36
+
37
+ type: Literal["text"]
38
+ text: str
39
+
40
+
41
+ class ClaudeCodeToolUseContent(BaseModel):
42
+ """Tool use content block in Claude Code messages."""
43
+
44
+ type: Literal["tool_use"]
45
+ id: str
46
+ name: str
47
+ input: dict[str, Any]
48
+
49
+
50
+ class ClaudeCodeToolResultContent(BaseModel):
51
+ """Tool result content block in Claude Code messages."""
52
+
53
+ type: Literal["tool_result"]
54
+ tool_use_id: str
55
+ content: list[ClaudeCodeTextContent] | str
56
+
57
+
58
+ class ClaudeCodeThinkingContent(BaseModel):
59
+ """Thinking content block in Claude Code messages."""
60
+
61
+ type: Literal["thinking"]
62
+ thinking: str
63
+
64
+
65
+ ClaudeCodeContentBlock = Annotated[
66
+ ClaudeCodeTextContent
67
+ | ClaudeCodeToolUseContent
68
+ | ClaudeCodeToolResultContent
69
+ | ClaudeCodeThinkingContent,
70
+ Field(discriminator="type"),
71
+ ]
72
+
73
+
74
+ class ClaudeCodeUserMessage(BaseModel):
75
+ """User message payload in Claude Code format."""
76
+
77
+ role: Literal["user"]
78
+ content: str | list[ClaudeCodeContentBlock]
79
+
80
+
81
+ class ClaudeCodeAssistantMessage(BaseModel):
82
+ """Assistant message payload in Claude Code format."""
83
+
84
+ model: str | None = None
85
+ id: str | None = None
86
+ type: Literal["message"] = "message"
87
+ role: Literal["assistant"]
88
+ content: list[ClaudeCodeContentBlock]
89
+ stop_reason: str | None = None
90
+ usage: ClaudeCodeUsage | None = None
91
+
92
+
93
+ class ClaudeCodeUserEntry(BaseModel):
94
+ """A user entry in Claude Code's JSONL history."""
95
+
96
+ type: Literal["user"]
97
+ message: ClaudeCodeUserMessage
98
+ uuid: str
99
+ parent_uuid: str | None = Field(default=None, alias="parentUuid")
100
+ session_id: str = Field(alias="sessionId")
101
+ timestamp: datetime
102
+ cwd: str | None = None
103
+ version: str | None = None
104
+ git_branch: str | None = Field(default=None, alias="gitBranch")
105
+ is_sidechain: bool = Field(default=False, alias="isSidechain")
106
+ user_type: str | None = Field(default=None, alias="userType")
107
+
108
+
109
+ class ClaudeCodeAssistantEntry(BaseModel):
110
+ """An assistant entry in Claude Code's JSONL history."""
111
+
112
+ type: Literal["assistant"]
113
+ message: ClaudeCodeAssistantMessage
114
+ uuid: str
115
+ parent_uuid: str | None = Field(default=None, alias="parentUuid")
116
+ session_id: str = Field(alias="sessionId")
117
+ timestamp: datetime
118
+ request_id: str | None = Field(default=None, alias="requestId")
119
+ cwd: str | None = None
120
+ version: str | None = None
121
+ git_branch: str | None = Field(default=None, alias="gitBranch")
122
+ is_sidechain: bool = Field(default=False, alias="isSidechain")
123
+ user_type: str | None = Field(default=None, alias="userType")
124
+
125
+
126
+ class ClaudeCodeQueueOperation(BaseModel):
127
+ """A queue operation entry (metadata, not a message)."""
128
+
129
+ type: Literal["queue-operation"]
130
+ operation: str
131
+ timestamp: datetime
132
+ session_id: str = Field(alias="sessionId")
133
+
134
+
135
+ class ClaudeCodeSummary(BaseModel):
136
+ """A summary entry in Claude Code's history."""
137
+
138
+ type: Literal["summary"]
139
+ summary: str
140
+ uuid: str
141
+ parent_uuid: str | None = Field(default=None, alias="parentUuid")
142
+ session_id: str = Field(alias="sessionId")
143
+ timestamp: datetime
144
+ is_sidechain: bool = Field(default=False, alias="isSidechain")
145
+
146
+
147
+ ClaudeCodeEntry = Annotated[
148
+ ClaudeCodeUserEntry | ClaudeCodeAssistantEntry | ClaudeCodeQueueOperation | ClaudeCodeSummary,
149
+ Field(discriminator="type"),
150
+ ]
151
+
152
+ # Message entries that have uuid and parent_uuid (excludes queue operations)
153
+ ClaudeCodeMessageEntry = ClaudeCodeUserEntry | ClaudeCodeAssistantEntry | ClaudeCodeSummary
154
+
155
+
156
+ def parse_entry(line: str) -> ClaudeCodeEntry | None:
157
+ """Parse a single JSONL line into a Claude Code entry.
158
+
159
+ Args:
160
+ line: A single line from the JSONL file
161
+
162
+ Returns:
163
+ Parsed entry or None if the line is empty or unparseable
164
+ """
165
+ line = line.strip()
166
+ if not line:
167
+ return None
168
+
169
+ data = json.loads(line)
170
+ entry_type = data.get("type")
171
+
172
+ match entry_type:
173
+ case "user":
174
+ return ClaudeCodeUserEntry.model_validate(data)
175
+ case "assistant":
176
+ return ClaudeCodeAssistantEntry.model_validate(data)
177
+ case "queue-operation":
178
+ return ClaudeCodeQueueOperation.model_validate(data)
179
+ case "summary":
180
+ return ClaudeCodeSummary.model_validate(data)
181
+ case _:
182
+ return None
183
+
184
+
185
+ def load_session(session_path: Path) -> list[ClaudeCodeEntry]:
186
+ """Load all entries from a Claude Code session file.
187
+
188
+ Args:
189
+ session_path: Path to the .jsonl session file
190
+
191
+ Returns:
192
+ List of parsed entries
193
+ """
194
+ with session_path.open() as f:
195
+ return [entry for line in f if (entry := parse_entry(line))]
196
+
197
+
198
+ def get_main_conversation(
199
+ entries: list[ClaudeCodeEntry],
200
+ *,
201
+ include_sidechains: bool = False,
202
+ ) -> list[ClaudeCodeMessageEntry]:
203
+ """Extract the main conversation thread from entries.
204
+
205
+ Claude Code supports forking conversations via parentUuid. This function
206
+ follows the parent chain to reconstruct the main conversation, optionally
207
+ including or excluding sidechain messages.
208
+
209
+ Args:
210
+ entries: All entries from the session
211
+ include_sidechains: If True, include sidechain entries. If False (default),
212
+ only include the main conversation thread.
213
+
214
+ Returns:
215
+ Entries in conversation order, following the parent chain
216
+ """
217
+ # Filter to message entries (not queue operations)
218
+ message_entries: list[ClaudeCodeMessageEntry] = [
219
+ e
220
+ for e in entries
221
+ if isinstance(e, ClaudeCodeUserEntry | ClaudeCodeAssistantEntry | ClaudeCodeSummary)
222
+ ]
223
+
224
+ if not message_entries:
225
+ return []
226
+
227
+ # Build children lookup
228
+ children: dict[str | None, list[ClaudeCodeMessageEntry]] = {}
229
+ for entry in message_entries:
230
+ parent = entry.parent_uuid
231
+ children.setdefault(parent, []).append(entry)
232
+
233
+ # Find root(s) - entries with no parent
234
+ roots = children.get(None, [])
235
+
236
+ if not roots:
237
+ # No roots found, fall back to file order
238
+ if include_sidechains:
239
+ return message_entries
240
+ return [e for e in message_entries if not e.is_sidechain]
241
+
242
+ # Walk the tree, preferring non-sidechain entries
243
+ result: list[ClaudeCodeMessageEntry] = []
244
+
245
+ def walk(entry: ClaudeCodeMessageEntry) -> None:
246
+ if include_sidechains or not entry.is_sidechain:
247
+ result.append(entry)
248
+
249
+ # Get children of this entry
250
+ entry_children = children.get(entry.uuid, [])
251
+
252
+ # Sort children: non-sidechains first, then by timestamp
253
+ entry_children.sort(key=lambda e: (e.is_sidechain, e.timestamp))
254
+
255
+ for child in entry_children:
256
+ walk(child)
257
+
258
+ # Start from roots (sorted by timestamp)
259
+ roots.sort(key=lambda e: e.timestamp)
260
+ for root in roots:
261
+ walk(root)
262
+
263
+ return result
264
+
265
+
266
+ def get_claude_data_dir() -> Path:
267
+ """Get the Claude Code data directory path.
268
+
269
+ Claude Code stores data in ~/.claude rather than the XDG data directory.
270
+ """
271
+ from pathlib import Path
272
+
273
+ return Path.home() / ".claude"
274
+
275
+
276
+ def get_claude_projects_dir() -> Path:
277
+ """Get the Claude Code projects directory path."""
278
+ return get_claude_data_dir() / "projects"
279
+
280
+
281
+ def path_to_claude_dir_name(project_path: str) -> str:
282
+ """Convert a filesystem path to Claude Code's directory naming format.
283
+
284
+ Claude Code replaces '/' with '-', so '/home/user/project' becomes '-home-user-project'.
285
+
286
+ Args:
287
+ project_path: The filesystem path
288
+
289
+ Returns:
290
+ The Claude Code directory name format
291
+ """
292
+ return project_path.replace("/", "-")
293
+
294
+
295
+ def list_project_sessions(project_path: str) -> list[Path]:
296
+ """List all session files for a project.
297
+
298
+ Args:
299
+ project_path: The project path (will be converted to Claude's format)
300
+
301
+ Returns:
302
+ List of session file paths, sorted by modification time (newest first)
303
+ """
304
+ projects_dir = get_claude_projects_dir()
305
+ project_dir_name = path_to_claude_dir_name(project_path)
306
+ project_dir = projects_dir / project_dir_name
307
+
308
+ if not project_dir.exists():
309
+ return []
310
+
311
+ sessions = list(project_dir.glob("*.jsonl"))
312
+ return sorted(sessions, key=lambda p: p.stat().st_mtime, reverse=True)
313
+
314
+
315
+ def convert_to_pydantic_ai(
316
+ entries: list[ClaudeCodeEntry],
317
+ *,
318
+ include_sidechains: bool = False,
319
+ follow_parent_chain: bool = True,
320
+ ) -> list[ModelRequest | ModelResponse]:
321
+ """Convert Claude Code entries to pydantic-ai message format.
322
+
323
+ Args:
324
+ entries: List of Claude Code history entries
325
+ include_sidechains: If True, include sidechain (forked) messages
326
+ follow_parent_chain: If True (default), reconstruct conversation order
327
+ by following parentUuid links. If False, use file order.
328
+
329
+ Returns:
330
+ List of ModelRequest and ModelResponse objects
331
+ """
332
+ from pydantic_ai import ModelRequest, ModelResponse
333
+
334
+ # Optionally reconstruct proper conversation order
335
+ conversation: list[ClaudeCodeEntry] | list[ClaudeCodeMessageEntry]
336
+ if follow_parent_chain:
337
+ conversation = get_main_conversation(entries, include_sidechains=include_sidechains)
338
+ else:
339
+ conversation = entries
340
+ from pydantic_ai.messages import (
341
+ TextPart,
342
+ ThinkingPart,
343
+ ToolCallPart,
344
+ ToolReturnPart,
345
+ UserPromptPart,
346
+ )
347
+
348
+ messages: list[ModelRequest | ModelResponse] = []
349
+
350
+ for entry in conversation:
351
+ match entry:
352
+ case ClaudeCodeUserEntry():
353
+ parts: list[Any] = []
354
+ metadata = {
355
+ "uuid": entry.uuid,
356
+ "timestamp": entry.timestamp.isoformat(),
357
+ "sessionId": entry.session_id,
358
+ "cwd": entry.cwd,
359
+ "gitBranch": entry.git_branch,
360
+ "isSidechain": entry.is_sidechain,
361
+ }
362
+
363
+ content = entry.message.content
364
+ if isinstance(content, str):
365
+ parts.append(UserPromptPart(content=content))
366
+ else:
367
+ for block in content:
368
+ match block:
369
+ case ClaudeCodeTextContent():
370
+ parts.append(UserPromptPart(content=block.text))
371
+ case ClaudeCodeToolResultContent():
372
+ # Extract text from tool result content
373
+ if isinstance(block.content, str):
374
+ result_content = block.content
375
+ else:
376
+ result_content = "\n".join(
377
+ c.text
378
+ for c in block.content
379
+ if isinstance(c, ClaudeCodeTextContent)
380
+ )
381
+ parts.append(
382
+ ToolReturnPart(
383
+ tool_name="", # Not available in history
384
+ content=result_content,
385
+ tool_call_id=block.tool_use_id,
386
+ )
387
+ )
388
+
389
+ if parts:
390
+ messages.append(ModelRequest(parts=parts, metadata=metadata))
391
+
392
+ case ClaudeCodeAssistantEntry():
393
+ parts = []
394
+ metadata = {
395
+ "uuid": entry.uuid,
396
+ "timestamp": entry.timestamp.isoformat(),
397
+ "sessionId": entry.session_id,
398
+ "requestId": entry.request_id,
399
+ "cwd": entry.cwd,
400
+ "gitBranch": entry.git_branch,
401
+ "isSidechain": entry.is_sidechain,
402
+ }
403
+
404
+ for block in entry.message.content:
405
+ match block:
406
+ case ClaudeCodeTextContent():
407
+ parts.append(TextPart(content=block.text))
408
+ case ClaudeCodeToolUseContent():
409
+ parts.append(
410
+ ToolCallPart(
411
+ tool_name=block.name,
412
+ args=block.input,
413
+ tool_call_id=block.id,
414
+ )
415
+ )
416
+ case ClaudeCodeThinkingContent():
417
+ parts.append(ThinkingPart(content=block.thinking))
418
+
419
+ if parts:
420
+ messages.append(
421
+ ModelResponse(
422
+ parts=parts,
423
+ model_name=entry.message.model,
424
+ provider_response_id=entry.message.id,
425
+ metadata=metadata,
426
+ )
427
+ )
428
+
429
+ case ClaudeCodeSummary():
430
+ # Summaries can be added as system context if needed
431
+ metadata = {
432
+ "uuid": entry.uuid,
433
+ "timestamp": entry.timestamp.isoformat(),
434
+ "sessionId": entry.session_id,
435
+ "type": "summary",
436
+ }
437
+ messages.append(
438
+ ModelRequest(
439
+ parts=[UserPromptPart(content=f"[Summary]: {entry.summary}")],
440
+ metadata=metadata,
441
+ )
442
+ )
443
+
444
+ case ClaudeCodeQueueOperation():
445
+ # Skip queue operations - they're metadata, not messages
446
+ pass
447
+
448
+ return messages
449
+
450
+
451
+ def load_session_as_pydantic_ai(session_path: Path) -> list[ModelRequest | ModelResponse]:
452
+ """Load a Claude Code session and convert to pydantic-ai format.
453
+
454
+ Args:
455
+ session_path: Path to the .jsonl session file
456
+
457
+ Returns:
458
+ List of ModelRequest and ModelResponse objects
459
+ """
460
+ entries = load_session(session_path)
461
+ return convert_to_pydantic_ai(entries)
462
+
463
+
464
+ def get_latest_session(project_path: str) -> Path | None:
465
+ """Get the most recent session file for a project.
466
+
467
+ Args:
468
+ project_path: The project path
469
+
470
+ Returns:
471
+ Path to the latest session file, or None if no sessions exist
472
+ """
473
+ sessions = list_project_sessions(project_path)
474
+ return sessions[0] if sessions else None
@@ -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,6 +3,7 @@
3
3
  from .events import (
4
4
  CommandCompleteEvent,
5
5
  CommandOutputEvent,
6
+ CompactionEvent,
6
7
  CustomEvent,
7
8
  DiffContentItem,
8
9
  FileContentItem,
@@ -36,6 +37,7 @@ __all__ = [
36
37
  "BaseTTSEventHandler",
37
38
  "CommandCompleteEvent",
38
39
  "CommandOutputEvent",
40
+ "CompactionEvent",
39
41
  "CustomEvent",
40
42
  "DiffContentItem",
41
43
  "EdgeTTSEventHandler",
@@ -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):