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.
- acp/__init__.py +13 -0
- acp/bridge/README.md +15 -2
- acp/bridge/__init__.py +3 -2
- acp/bridge/__main__.py +60 -19
- acp/bridge/ws_server.py +173 -0
- acp/bridge/ws_server_cli.py +89 -0
- acp/notifications.py +2 -1
- acp/stdio.py +39 -9
- acp/transports.py +362 -2
- acp/utils.py +15 -2
- agentpool/__init__.py +4 -1
- agentpool/agents/__init__.py +2 -0
- agentpool/agents/acp_agent/acp_agent.py +203 -88
- agentpool/agents/acp_agent/acp_converters.py +46 -21
- agentpool/agents/acp_agent/client_handler.py +157 -3
- agentpool/agents/acp_agent/session_state.py +4 -1
- agentpool/agents/agent.py +314 -107
- agentpool/agents/agui_agent/__init__.py +0 -2
- agentpool/agents/agui_agent/agui_agent.py +90 -21
- agentpool/agents/agui_agent/agui_converters.py +0 -131
- agentpool/agents/base_agent.py +163 -1
- agentpool/agents/claude_code_agent/claude_code_agent.py +626 -179
- agentpool/agents/claude_code_agent/converters.py +71 -3
- agentpool/agents/claude_code_agent/history.py +474 -0
- agentpool/agents/context.py +40 -0
- agentpool/agents/events/__init__.py +2 -0
- agentpool/agents/events/builtin_handlers.py +2 -1
- agentpool/agents/events/event_emitter.py +29 -2
- agentpool/agents/events/events.py +20 -0
- agentpool/agents/modes.py +54 -0
- agentpool/agents/tool_call_accumulator.py +213 -0
- agentpool/common_types.py +21 -0
- agentpool/config_resources/__init__.py +38 -1
- agentpool/config_resources/claude_code_agent.yml +3 -0
- agentpool/delegation/pool.py +37 -29
- agentpool/delegation/team.py +1 -0
- agentpool/delegation/teamrun.py +1 -0
- agentpool/diagnostics/__init__.py +53 -0
- agentpool/diagnostics/lsp_manager.py +1593 -0
- agentpool/diagnostics/lsp_proxy.py +41 -0
- agentpool/diagnostics/lsp_proxy_script.py +229 -0
- agentpool/diagnostics/models.py +398 -0
- agentpool/mcp_server/__init__.py +0 -2
- agentpool/mcp_server/client.py +12 -3
- agentpool/mcp_server/manager.py +25 -31
- agentpool/mcp_server/registries/official_registry_client.py +25 -0
- agentpool/mcp_server/tool_bridge.py +78 -66
- agentpool/messaging/__init__.py +0 -2
- agentpool/messaging/compaction.py +72 -197
- agentpool/messaging/message_history.py +12 -0
- agentpool/messaging/messages.py +52 -9
- agentpool/messaging/processing.py +3 -1
- agentpool/models/acp_agents/base.py +0 -22
- agentpool/models/acp_agents/mcp_capable.py +8 -148
- agentpool/models/acp_agents/non_mcp.py +129 -72
- agentpool/models/agents.py +35 -13
- agentpool/models/claude_code_agents.py +33 -2
- agentpool/models/manifest.py +43 -0
- agentpool/repomap.py +1 -1
- agentpool/resource_providers/__init__.py +9 -1
- agentpool/resource_providers/aggregating.py +52 -3
- agentpool/resource_providers/base.py +57 -1
- agentpool/resource_providers/mcp_provider.py +23 -0
- agentpool/resource_providers/plan_provider.py +130 -41
- agentpool/resource_providers/pool.py +2 -0
- agentpool/resource_providers/static.py +2 -0
- agentpool/sessions/__init__.py +2 -1
- agentpool/sessions/manager.py +31 -2
- agentpool/sessions/models.py +50 -0
- agentpool/skills/registry.py +13 -8
- agentpool/storage/manager.py +217 -1
- agentpool/testing.py +537 -19
- agentpool/utils/file_watcher.py +269 -0
- agentpool/utils/identifiers.py +121 -0
- agentpool/utils/pydantic_ai_helpers.py +46 -0
- agentpool/utils/streams.py +690 -1
- agentpool/utils/subprocess_utils.py +155 -0
- agentpool/utils/token_breakdown.py +461 -0
- {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/METADATA +27 -7
- {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/RECORD +170 -112
- {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/WHEEL +1 -1
- agentpool_cli/__main__.py +4 -0
- agentpool_cli/serve_acp.py +41 -20
- agentpool_cli/serve_agui.py +87 -0
- agentpool_cli/serve_opencode.py +119 -0
- agentpool_commands/__init__.py +30 -0
- agentpool_commands/agents.py +74 -1
- agentpool_commands/history.py +62 -0
- agentpool_commands/mcp.py +176 -0
- agentpool_commands/models.py +56 -3
- agentpool_commands/tools.py +57 -0
- agentpool_commands/utils.py +51 -0
- agentpool_config/builtin_tools.py +77 -22
- agentpool_config/commands.py +24 -1
- agentpool_config/compaction.py +258 -0
- agentpool_config/mcp_server.py +131 -1
- agentpool_config/storage.py +46 -1
- agentpool_config/tools.py +7 -1
- agentpool_config/toolsets.py +92 -148
- agentpool_server/acp_server/acp_agent.py +134 -150
- agentpool_server/acp_server/commands/acp_commands.py +216 -51
- agentpool_server/acp_server/commands/docs_commands/fetch_repo.py +10 -10
- agentpool_server/acp_server/server.py +23 -79
- agentpool_server/acp_server/session.py +181 -19
- agentpool_server/opencode_server/.rules +95 -0
- agentpool_server/opencode_server/ENDPOINTS.md +362 -0
- agentpool_server/opencode_server/__init__.py +27 -0
- agentpool_server/opencode_server/command_validation.py +172 -0
- agentpool_server/opencode_server/converters.py +869 -0
- agentpool_server/opencode_server/dependencies.py +24 -0
- agentpool_server/opencode_server/input_provider.py +269 -0
- agentpool_server/opencode_server/models/__init__.py +228 -0
- agentpool_server/opencode_server/models/agent.py +53 -0
- agentpool_server/opencode_server/models/app.py +60 -0
- agentpool_server/opencode_server/models/base.py +26 -0
- agentpool_server/opencode_server/models/common.py +23 -0
- agentpool_server/opencode_server/models/config.py +37 -0
- agentpool_server/opencode_server/models/events.py +647 -0
- agentpool_server/opencode_server/models/file.py +88 -0
- agentpool_server/opencode_server/models/mcp.py +25 -0
- agentpool_server/opencode_server/models/message.py +162 -0
- agentpool_server/opencode_server/models/parts.py +190 -0
- agentpool_server/opencode_server/models/provider.py +81 -0
- agentpool_server/opencode_server/models/pty.py +43 -0
- agentpool_server/opencode_server/models/session.py +99 -0
- agentpool_server/opencode_server/routes/__init__.py +25 -0
- agentpool_server/opencode_server/routes/agent_routes.py +442 -0
- agentpool_server/opencode_server/routes/app_routes.py +139 -0
- agentpool_server/opencode_server/routes/config_routes.py +241 -0
- agentpool_server/opencode_server/routes/file_routes.py +392 -0
- agentpool_server/opencode_server/routes/global_routes.py +94 -0
- agentpool_server/opencode_server/routes/lsp_routes.py +319 -0
- agentpool_server/opencode_server/routes/message_routes.py +705 -0
- agentpool_server/opencode_server/routes/pty_routes.py +299 -0
- agentpool_server/opencode_server/routes/session_routes.py +1205 -0
- agentpool_server/opencode_server/routes/tui_routes.py +139 -0
- agentpool_server/opencode_server/server.py +430 -0
- agentpool_server/opencode_server/state.py +121 -0
- agentpool_server/opencode_server/time_utils.py +8 -0
- agentpool_storage/__init__.py +16 -0
- agentpool_storage/base.py +103 -0
- agentpool_storage/claude_provider.py +907 -0
- agentpool_storage/file_provider.py +129 -0
- agentpool_storage/memory_provider.py +61 -0
- agentpool_storage/models.py +3 -0
- agentpool_storage/opencode_provider.py +730 -0
- agentpool_storage/project_store.py +325 -0
- agentpool_storage/session_store.py +6 -0
- agentpool_storage/sql_provider/__init__.py +4 -2
- agentpool_storage/sql_provider/models.py +48 -0
- agentpool_storage/sql_provider/sql_provider.py +134 -1
- agentpool_storage/sql_provider/utils.py +10 -1
- agentpool_storage/text_log_provider.py +1 -0
- agentpool_toolsets/builtin/__init__.py +0 -8
- agentpool_toolsets/builtin/code.py +95 -56
- agentpool_toolsets/builtin/debug.py +16 -21
- agentpool_toolsets/builtin/execution_environment.py +99 -103
- agentpool_toolsets/builtin/file_edit/file_edit.py +115 -7
- agentpool_toolsets/builtin/skills.py +86 -4
- agentpool_toolsets/fsspec_toolset/__init__.py +13 -1
- agentpool_toolsets/fsspec_toolset/diagnostics.py +860 -73
- agentpool_toolsets/fsspec_toolset/grep.py +74 -2
- agentpool_toolsets/fsspec_toolset/image_utils.py +161 -0
- agentpool_toolsets/fsspec_toolset/toolset.py +159 -38
- agentpool_toolsets/mcp_discovery/__init__.py +5 -0
- agentpool_toolsets/mcp_discovery/data/mcp_servers.parquet +0 -0
- agentpool_toolsets/mcp_discovery/toolset.py +454 -0
- agentpool_toolsets/mcp_run_toolset.py +84 -6
- agentpool_toolsets/builtin/agent_management.py +0 -239
- agentpool_toolsets/builtin/history.py +0 -36
- agentpool_toolsets/builtin/integration.py +0 -85
- agentpool_toolsets/builtin/tool_management.py +0 -90
- {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/entry_points.txt +0 -0
- {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", "
|
|
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
|
agentpool/agents/context.py
CHANGED
|
@@ -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
|
|
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):
|