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
|
@@ -24,12 +24,12 @@ from __future__ import annotations
|
|
|
24
24
|
|
|
25
25
|
import asyncio
|
|
26
26
|
from decimal import Decimal
|
|
27
|
-
from typing import TYPE_CHECKING, Any,
|
|
27
|
+
from typing import TYPE_CHECKING, Any, Literal, Self
|
|
28
28
|
import uuid
|
|
29
29
|
|
|
30
30
|
import anyio
|
|
31
|
-
from pydantic import TypeAdapter
|
|
32
31
|
from pydantic_ai import (
|
|
32
|
+
FunctionToolResultEvent,
|
|
33
33
|
ModelRequest,
|
|
34
34
|
ModelResponse,
|
|
35
35
|
PartDeltaEvent,
|
|
@@ -41,9 +41,11 @@ from pydantic_ai import (
|
|
|
41
41
|
ThinkingPart,
|
|
42
42
|
ThinkingPartDelta,
|
|
43
43
|
ToolCallPart,
|
|
44
|
+
ToolCallPartDelta,
|
|
44
45
|
ToolReturnPart,
|
|
45
46
|
UserPromptPart,
|
|
46
47
|
)
|
|
48
|
+
from pydantic_ai.usage import RequestUsage
|
|
47
49
|
|
|
48
50
|
from agentpool.agents.base_agent import BaseAgent
|
|
49
51
|
from agentpool.agents.claude_code_agent.converters import claude_message_to_events
|
|
@@ -54,12 +56,13 @@ from agentpool.agents.events import (
|
|
|
54
56
|
ToolCallCompleteEvent,
|
|
55
57
|
ToolCallStartEvent,
|
|
56
58
|
)
|
|
59
|
+
from agentpool.agents.modes import ModeInfo
|
|
57
60
|
from agentpool.log import get_logger
|
|
58
61
|
from agentpool.messaging import ChatMessage
|
|
59
62
|
from agentpool.messaging.messages import TokenCost
|
|
60
63
|
from agentpool.messaging.processing import prepare_prompts
|
|
61
64
|
from agentpool.models.claude_code_agents import ClaudeCodeAgentConfig
|
|
62
|
-
from agentpool.utils.streams import merge_queue_into_iterator
|
|
65
|
+
from agentpool.utils.streams import FileTracker, merge_queue_into_iterator
|
|
63
66
|
|
|
64
67
|
|
|
65
68
|
if TYPE_CHECKING:
|
|
@@ -75,21 +78,25 @@ if TYPE_CHECKING:
|
|
|
75
78
|
ToolPermissionContext,
|
|
76
79
|
ToolUseBlock,
|
|
77
80
|
)
|
|
81
|
+
from claude_agent_sdk.types import HookContext, HookInput, SyncHookJSONOutput
|
|
78
82
|
from evented.configs import EventConfig
|
|
79
83
|
from exxec import ExecutionEnvironment
|
|
84
|
+
from slashed import BaseCommand, Command, CommandContext
|
|
85
|
+
from tokonomics.model_discovery.model_info import ModelInfo
|
|
80
86
|
from toprompt import AnyPromptType
|
|
81
87
|
|
|
82
88
|
from agentpool.agents.context import AgentContext
|
|
83
89
|
from agentpool.agents.events import RichAgentStreamEvent
|
|
90
|
+
from agentpool.agents.modes import ModeCategory
|
|
84
91
|
from agentpool.common_types import (
|
|
85
92
|
BuiltinEventHandlerType,
|
|
86
93
|
IndividualEventHandler,
|
|
94
|
+
MCPServerStatus,
|
|
87
95
|
PromptCompatible,
|
|
88
96
|
)
|
|
89
97
|
from agentpool.delegation import AgentPool
|
|
90
98
|
from agentpool.mcp_server.tool_bridge import ToolManagerBridge
|
|
91
99
|
from agentpool.messaging import MessageHistory
|
|
92
|
-
from agentpool.talk.stats import MessageStats
|
|
93
100
|
from agentpool.ui.base import InputProvider
|
|
94
101
|
from agentpool_config.mcp_server import MCPServerConfig
|
|
95
102
|
from agentpool_config.nodes import ToolConfirmationMode
|
|
@@ -97,6 +104,16 @@ if TYPE_CHECKING:
|
|
|
97
104
|
|
|
98
105
|
logger = get_logger(__name__)
|
|
99
106
|
|
|
107
|
+
# Prefix to strip from tool names for cleaner UI display
|
|
108
|
+
_MCP_TOOL_PREFIX = "mcp__agentpool-claude-tools__"
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _strip_mcp_prefix(tool_name: str) -> str:
|
|
112
|
+
"""Strip MCP server prefix from tool names for cleaner UI display."""
|
|
113
|
+
if tool_name.startswith(_MCP_TOOL_PREFIX):
|
|
114
|
+
return tool_name[len(_MCP_TOOL_PREFIX) :]
|
|
115
|
+
return tool_name
|
|
116
|
+
|
|
100
117
|
|
|
101
118
|
class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
102
119
|
"""Agent wrapping Claude Agent SDK's ClaudeSDKClient.
|
|
@@ -126,6 +143,7 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
126
143
|
include_builtin_system_prompt: bool = True,
|
|
127
144
|
model: str | None = None,
|
|
128
145
|
max_turns: int | None = None,
|
|
146
|
+
max_budget_usd: float | None = None,
|
|
129
147
|
max_thinking_tokens: int | None = None,
|
|
130
148
|
permission_mode: PermissionMode | None = None,
|
|
131
149
|
mcp_servers: Sequence[MCPServerConfig] | None = None,
|
|
@@ -142,6 +160,7 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
142
160
|
event_handlers: Sequence[IndividualEventHandler | BuiltinEventHandlerType] | None = None,
|
|
143
161
|
tool_confirmation_mode: ToolConfirmationMode = "always",
|
|
144
162
|
output_type: type[TResult] | None = None,
|
|
163
|
+
commands: Sequence[BaseCommand] | None = None,
|
|
145
164
|
) -> None:
|
|
146
165
|
"""Initialize ClaudeCodeAgent.
|
|
147
166
|
|
|
@@ -157,6 +176,7 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
157
176
|
include_builtin_system_prompt: If True, the builtin system prompt is included.
|
|
158
177
|
model: Model to use (e.g., "claude-sonnet-4-5")
|
|
159
178
|
max_turns: Maximum conversation turns
|
|
179
|
+
max_budget_usd: Maximum budget to consume in dollars
|
|
160
180
|
max_thinking_tokens: Max tokens for extended thinking
|
|
161
181
|
permission_mode: Permission mode ("default", "acceptEdits", "plan", "bypassPermissions")
|
|
162
182
|
mcp_servers: External MCP servers to connect to (internal format, converted at runtime)
|
|
@@ -173,6 +193,7 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
173
193
|
event_handlers: Event handlers for streaming events
|
|
174
194
|
tool_confirmation_mode: Tool confirmation behavior
|
|
175
195
|
output_type: Type for structured output (uses JSON schema)
|
|
196
|
+
commands: Slash commands
|
|
176
197
|
"""
|
|
177
198
|
from agentpool.agents.sys_prompts import SystemPrompts
|
|
178
199
|
|
|
@@ -211,6 +232,7 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
211
232
|
output_type=output_type or str, # type: ignore[arg-type]
|
|
212
233
|
tool_confirmation_mode=tool_confirmation_mode,
|
|
213
234
|
event_handlers=event_handlers,
|
|
235
|
+
commands=commands,
|
|
214
236
|
)
|
|
215
237
|
|
|
216
238
|
self._config = config
|
|
@@ -234,6 +256,7 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
234
256
|
self.sys_prompts = SystemPrompts(all_prompts, prompt_manager=prompt_manager)
|
|
235
257
|
self._model = model or config.model
|
|
236
258
|
self._max_turns = max_turns or config.max_turns
|
|
259
|
+
self._max_budget_usd = max_budget_usd or config.max_budget_usd
|
|
237
260
|
self._max_thinking_tokens = max_thinking_tokens or config.max_thinking_tokens
|
|
238
261
|
self._permission_mode: PermissionMode | None = permission_mode or config.permission_mode
|
|
239
262
|
self._external_mcp_servers = list(mcp_servers) if mcp_servers else config.get_mcp_servers()
|
|
@@ -255,6 +278,10 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
255
278
|
self._owns_bridge = False # Track if we created the bridge (for cleanup)
|
|
256
279
|
self._mcp_servers: dict[str, McpServerConfig] = {} # Claude SDK MCP server configs
|
|
257
280
|
|
|
281
|
+
# Track pending tool call for permission matching
|
|
282
|
+
# Maps tool_name to tool_call_id for matching permissions to tool call UI parts
|
|
283
|
+
self._pending_tool_call_ids: dict[str, str] = {}
|
|
284
|
+
|
|
258
285
|
def get_context(self, data: Any = None) -> AgentContext:
|
|
259
286
|
"""Create a new context for this agent.
|
|
260
287
|
|
|
@@ -272,57 +299,6 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
272
299
|
node=self, pool=self.agent_pool, config=self._config, definition=defn, data=data
|
|
273
300
|
)
|
|
274
301
|
|
|
275
|
-
def _convert_mcp_servers_to_sdk_format(self) -> dict[str, McpServerConfig]:
|
|
276
|
-
"""Convert internal MCPServerConfig to Claude SDK format.
|
|
277
|
-
|
|
278
|
-
Returns:
|
|
279
|
-
Dict mapping server names to SDK-compatible config dicts
|
|
280
|
-
"""
|
|
281
|
-
from claude_agent_sdk import McpServerConfig
|
|
282
|
-
|
|
283
|
-
from agentpool_config.mcp_server import (
|
|
284
|
-
SSEMCPServerConfig,
|
|
285
|
-
StdioMCPServerConfig,
|
|
286
|
-
StreamableHTTPMCPServerConfig,
|
|
287
|
-
)
|
|
288
|
-
|
|
289
|
-
result: dict[str, McpServerConfig] = {}
|
|
290
|
-
|
|
291
|
-
for idx, server in enumerate(self._external_mcp_servers):
|
|
292
|
-
# Determine server name
|
|
293
|
-
if server.name:
|
|
294
|
-
name = server.name
|
|
295
|
-
elif isinstance(server, StdioMCPServerConfig) and server.args:
|
|
296
|
-
name = server.args[-1].split("/")[-1].split("@")[0]
|
|
297
|
-
elif isinstance(server, StdioMCPServerConfig):
|
|
298
|
-
name = server.command
|
|
299
|
-
elif isinstance(server, SSEMCPServerConfig | StreamableHTTPMCPServerConfig):
|
|
300
|
-
from urllib.parse import urlparse
|
|
301
|
-
|
|
302
|
-
name = urlparse(str(server.url)).hostname or f"server_{idx}"
|
|
303
|
-
else:
|
|
304
|
-
name = f"server_{idx}"
|
|
305
|
-
|
|
306
|
-
# Build SDK-compatible config
|
|
307
|
-
config: dict[str, Any]
|
|
308
|
-
match server:
|
|
309
|
-
case StdioMCPServerConfig(command=command, args=args):
|
|
310
|
-
config = {"type": "stdio", "command": command, "args": args}
|
|
311
|
-
if server.env:
|
|
312
|
-
config["env"] = server.get_env_vars()
|
|
313
|
-
case SSEMCPServerConfig(url=url):
|
|
314
|
-
config = {"type": "sse", "url": str(url)}
|
|
315
|
-
if server.headers:
|
|
316
|
-
config["headers"] = server.headers
|
|
317
|
-
case StreamableHTTPMCPServerConfig(url=url):
|
|
318
|
-
config = {"type": "http", "url": str(url)}
|
|
319
|
-
if server.headers:
|
|
320
|
-
config["headers"] = server.headers
|
|
321
|
-
|
|
322
|
-
result[name] = cast(McpServerConfig, config)
|
|
323
|
-
|
|
324
|
-
return result
|
|
325
|
-
|
|
326
302
|
async def _setup_toolsets(self) -> None:
|
|
327
303
|
"""Initialize toolsets from config and create bridge if needed.
|
|
328
304
|
|
|
@@ -330,11 +306,12 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
330
306
|
and starts an MCP bridge to expose them to Claude Code via the SDK's
|
|
331
307
|
native MCP support. Also converts external MCP servers to SDK format.
|
|
332
308
|
"""
|
|
309
|
+
from agentpool.agents.claude_code_agent.converters import convert_mcp_servers_to_sdk_format
|
|
333
310
|
from agentpool.mcp_server.tool_bridge import BridgeConfig, ToolManagerBridge
|
|
334
311
|
|
|
335
312
|
# Convert external MCP servers to SDK format first
|
|
336
313
|
if self._external_mcp_servers:
|
|
337
|
-
external_configs = self.
|
|
314
|
+
external_configs = convert_mcp_servers_to_sdk_format(self._external_mcp_servers)
|
|
338
315
|
self._mcp_servers.update(external_configs)
|
|
339
316
|
self.log.info("External MCP servers configured", server_count=len(external_configs))
|
|
340
317
|
|
|
@@ -346,14 +323,11 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
346
323
|
provider = toolset_config.get_provider()
|
|
347
324
|
self.tools.add_provider(provider)
|
|
348
325
|
|
|
349
|
-
|
|
350
|
-
config = BridgeConfig(
|
|
351
|
-
transport="streamable-http", server_name=f"agentpool-{self.name}-tools"
|
|
352
|
-
)
|
|
326
|
+
server_name = f"agentpool-{self.name}-tools"
|
|
327
|
+
config = BridgeConfig(transport="streamable-http", server_name=server_name)
|
|
353
328
|
self._tool_bridge = ToolManagerBridge(node=self, config=config)
|
|
354
329
|
await self._tool_bridge.start()
|
|
355
330
|
self._owns_bridge = True
|
|
356
|
-
|
|
357
331
|
# Get Claude SDK-compatible MCP config and merge into our servers dict
|
|
358
332
|
mcp_config = self._tool_bridge.get_claude_mcp_server_config()
|
|
359
333
|
self._mcp_servers.update(mcp_config)
|
|
@@ -372,7 +346,6 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
372
346
|
"""
|
|
373
347
|
if self._tool_bridge is None: # Don't replace our own bridge
|
|
374
348
|
self._tool_bridge = bridge
|
|
375
|
-
|
|
376
349
|
# Get Claude SDK-compatible config and merge
|
|
377
350
|
mcp_config = bridge.get_claude_mcp_server_config()
|
|
378
351
|
self._mcp_servers.update(mcp_config)
|
|
@@ -391,6 +364,61 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
391
364
|
"""Get the model name."""
|
|
392
365
|
return self._current_model
|
|
393
366
|
|
|
367
|
+
def get_mcp_server_info(self) -> dict[str, MCPServerStatus]:
|
|
368
|
+
"""Get information about configured MCP servers.
|
|
369
|
+
|
|
370
|
+
Returns a dict mapping server names to their status info. This is used
|
|
371
|
+
by the OpenCode /mcp endpoint to display MCP servers in the sidebar.
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
Dict mapping server name to MCPServerStatus dataclass
|
|
375
|
+
"""
|
|
376
|
+
from agentpool.common_types import MCPServerStatus
|
|
377
|
+
|
|
378
|
+
result: dict[str, MCPServerStatus] = {}
|
|
379
|
+
for name, config in self._mcp_servers.items():
|
|
380
|
+
server_type = config.get("type", "unknown")
|
|
381
|
+
result[name] = MCPServerStatus(
|
|
382
|
+
name=name,
|
|
383
|
+
status="connected", # Claude SDK manages connections
|
|
384
|
+
server_type=server_type,
|
|
385
|
+
)
|
|
386
|
+
return result
|
|
387
|
+
|
|
388
|
+
def _build_hooks(self) -> dict[str, list[Any]]:
|
|
389
|
+
"""Build SDK hooks configuration.
|
|
390
|
+
|
|
391
|
+
Returns:
|
|
392
|
+
Dictionary mapping hook event names to HookMatcher lists
|
|
393
|
+
"""
|
|
394
|
+
from claude_agent_sdk.types import HookMatcher
|
|
395
|
+
|
|
396
|
+
async def on_pre_compact(
|
|
397
|
+
input_data: HookInput,
|
|
398
|
+
tool_use_id: str | None,
|
|
399
|
+
context: HookContext,
|
|
400
|
+
) -> SyncHookJSONOutput:
|
|
401
|
+
"""Handle PreCompact hook by emitting a CompactionEvent."""
|
|
402
|
+
from agentpool.agents.events import CompactionEvent
|
|
403
|
+
|
|
404
|
+
# input_data is PreCompactHookInput when hook_event_name == "PreCompact"
|
|
405
|
+
trigger_value = input_data.get("trigger", "auto")
|
|
406
|
+
trigger: Literal["auto", "manual"] = "manual" if trigger_value == "manual" else "auto"
|
|
407
|
+
|
|
408
|
+
# Emit semantic CompactionEvent - consumers handle display differently
|
|
409
|
+
compaction_event = CompactionEvent(
|
|
410
|
+
session_id=self.conversation_id,
|
|
411
|
+
trigger=trigger,
|
|
412
|
+
phase="starting",
|
|
413
|
+
)
|
|
414
|
+
await self._event_queue.put(compaction_event)
|
|
415
|
+
|
|
416
|
+
return {"continue_": True}
|
|
417
|
+
|
|
418
|
+
return {
|
|
419
|
+
"PreCompact": [HookMatcher(matcher=None, hooks=[on_pre_compact])],
|
|
420
|
+
}
|
|
421
|
+
|
|
394
422
|
def _build_options(self, *, formatted_system_prompt: str | None = None) -> ClaudeAgentOptions:
|
|
395
423
|
"""Build ClaudeAgentOptions from runtime state.
|
|
396
424
|
|
|
@@ -400,6 +428,8 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
400
428
|
from claude_agent_sdk import ClaudeAgentOptions
|
|
401
429
|
from claude_agent_sdk.types import SystemPromptPreset
|
|
402
430
|
|
|
431
|
+
from agentpool.agents.claude_code_agent.converters import to_output_format
|
|
432
|
+
|
|
403
433
|
# Build system prompt value
|
|
404
434
|
system_prompt: str | SystemPromptPreset | None = None
|
|
405
435
|
if formatted_system_prompt:
|
|
@@ -424,13 +454,6 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
424
454
|
self._can_use_tool if self.tool_confirmation_mode != "never" and not bypass else None
|
|
425
455
|
)
|
|
426
456
|
|
|
427
|
-
# Build structured output format if needed
|
|
428
|
-
output_format: dict[str, Any] | None = None
|
|
429
|
-
if self._output_type is not str:
|
|
430
|
-
adapter = TypeAdapter(self._output_type)
|
|
431
|
-
schema = adapter.json_schema()
|
|
432
|
-
output_format = {"type": "json_schema", "schema": schema}
|
|
433
|
-
|
|
434
457
|
return ClaudeAgentOptions(
|
|
435
458
|
cwd=self._cwd,
|
|
436
459
|
allowed_tools=self._allowed_tools or [],
|
|
@@ -438,6 +461,7 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
438
461
|
system_prompt=system_prompt,
|
|
439
462
|
model=self._model,
|
|
440
463
|
max_turns=self._max_turns,
|
|
464
|
+
max_budget_usd=self._max_budget_usd,
|
|
441
465
|
max_thinking_tokens=self._max_thinking_tokens,
|
|
442
466
|
permission_mode=permission_mode,
|
|
443
467
|
env=self._environment or {},
|
|
@@ -445,9 +469,10 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
445
469
|
tools=self._builtin_tools,
|
|
446
470
|
fallback_model=self._fallback_model,
|
|
447
471
|
can_use_tool=can_use_tool,
|
|
448
|
-
output_format=
|
|
472
|
+
output_format=to_output_format(self._output_type),
|
|
449
473
|
mcp_servers=self._mcp_servers or {},
|
|
450
474
|
include_partial_messages=True,
|
|
475
|
+
hooks=self._build_hooks(), # type: ignore[arg-type]
|
|
451
476
|
)
|
|
452
477
|
|
|
453
478
|
async def _can_use_tool( # noqa: PLR0911
|
|
@@ -481,13 +506,26 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
481
506
|
if tool_name.startswith(bridge_prefix):
|
|
482
507
|
return PermissionResultAllow()
|
|
483
508
|
|
|
509
|
+
# Auto-grant tools from configured external MCP servers
|
|
510
|
+
# These are explicitly configured by the user, so they should be trusted
|
|
511
|
+
# Tool names are like: mcp__{server_name}__{tool_name}
|
|
512
|
+
if tool_name.startswith("mcp__") and self._mcp_servers:
|
|
513
|
+
for server_name in self._mcp_servers:
|
|
514
|
+
if tool_name.startswith(f"mcp__{server_name}__"):
|
|
515
|
+
return PermissionResultAllow()
|
|
516
|
+
|
|
484
517
|
# Use input provider if available
|
|
485
518
|
if self._input_provider:
|
|
486
519
|
# Create a dummy Tool for the confirmation dialog
|
|
487
520
|
desc = f"Claude Code tool: {tool_name}"
|
|
488
521
|
tool = Tool(callable=lambda: None, name=tool_name, description=desc)
|
|
522
|
+
# Get the tool call ID from our tracking dict (set from streaming events)
|
|
523
|
+
tool_call_id = self._pending_tool_call_ids.get(tool_name)
|
|
524
|
+
ctx = self.get_context()
|
|
525
|
+
# Attach tool_call_id to context for permission event
|
|
526
|
+
ctx.tool_call_id = tool_call_id
|
|
489
527
|
result = await self._input_provider.get_tool_confirmation(
|
|
490
|
-
context=
|
|
528
|
+
context=ctx,
|
|
491
529
|
tool=tool,
|
|
492
530
|
args=input_data,
|
|
493
531
|
)
|
|
@@ -536,6 +574,122 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
536
574
|
self._client = None
|
|
537
575
|
await super().__aexit__(exc_type, exc_val, exc_tb)
|
|
538
576
|
|
|
577
|
+
async def populate_commands(self) -> None:
|
|
578
|
+
"""Populate the command store with slash commands from Claude Code.
|
|
579
|
+
|
|
580
|
+
Fetches available commands from the connected Claude Code server
|
|
581
|
+
and registers them as slashed Commands. Should be called after
|
|
582
|
+
connection is established.
|
|
583
|
+
|
|
584
|
+
Commands that are not supported or not useful for external use
|
|
585
|
+
are filtered out (e.g., login, logout, context, cost).
|
|
586
|
+
"""
|
|
587
|
+
if not self._client:
|
|
588
|
+
self.log.warning("Cannot populate commands: not connected")
|
|
589
|
+
return
|
|
590
|
+
|
|
591
|
+
server_info = await self._client.get_server_info()
|
|
592
|
+
if not server_info:
|
|
593
|
+
self.log.warning("No server info available for command population")
|
|
594
|
+
return
|
|
595
|
+
|
|
596
|
+
commands = server_info.get("commands", [])
|
|
597
|
+
if not commands:
|
|
598
|
+
self.log.debug("No commands available from Claude Code server")
|
|
599
|
+
return
|
|
600
|
+
|
|
601
|
+
# Commands to skip - not useful or problematic in this context
|
|
602
|
+
unsupported = {"context", "cost", "login", "logout", "release-notes", "todos"}
|
|
603
|
+
|
|
604
|
+
for cmd_info in commands:
|
|
605
|
+
name = cmd_info.get("name", "")
|
|
606
|
+
if not name or name in unsupported:
|
|
607
|
+
continue
|
|
608
|
+
|
|
609
|
+
command = self._create_claude_code_command(cmd_info)
|
|
610
|
+
self._command_store.register_command(command)
|
|
611
|
+
|
|
612
|
+
self.log.info(
|
|
613
|
+
"Populated command store", command_count=len(self._command_store.list_commands())
|
|
614
|
+
)
|
|
615
|
+
|
|
616
|
+
def _create_claude_code_command(self, cmd_info: dict[str, Any]) -> Command:
|
|
617
|
+
"""Create a slashed Command from Claude Code command info.
|
|
618
|
+
|
|
619
|
+
Args:
|
|
620
|
+
cmd_info: Command info dict with 'name', 'description', 'argumentHint'
|
|
621
|
+
|
|
622
|
+
Returns:
|
|
623
|
+
A slashed Command that executes via Claude Code
|
|
624
|
+
"""
|
|
625
|
+
from slashed import Command
|
|
626
|
+
|
|
627
|
+
name = cmd_info.get("name", "")
|
|
628
|
+
description = cmd_info.get("description", "")
|
|
629
|
+
argument_hint = cmd_info.get("argumentHint")
|
|
630
|
+
|
|
631
|
+
# Handle MCP commands - they have " (MCP)" suffix in Claude Code
|
|
632
|
+
category = "claude_code"
|
|
633
|
+
if name.endswith(" (MCP)"):
|
|
634
|
+
name = f"mcp:{name.replace(' (MCP)', '')}"
|
|
635
|
+
category = "mcp"
|
|
636
|
+
|
|
637
|
+
async def execute_command(
|
|
638
|
+
ctx: CommandContext[Any],
|
|
639
|
+
args: list[str],
|
|
640
|
+
kwargs: dict[str, str],
|
|
641
|
+
) -> None:
|
|
642
|
+
"""Execute the Claude Code slash command."""
|
|
643
|
+
import re
|
|
644
|
+
|
|
645
|
+
from claude_agent_sdk.types import (
|
|
646
|
+
AssistantMessage,
|
|
647
|
+
ResultMessage,
|
|
648
|
+
TextBlock,
|
|
649
|
+
UserMessage,
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
# Build command string
|
|
653
|
+
args_str = " ".join(args) if args else ""
|
|
654
|
+
if kwargs:
|
|
655
|
+
kwargs_str = " ".join(f"{k}={v}" for k, v in kwargs.items())
|
|
656
|
+
args_str = f"{args_str} {kwargs_str}".strip()
|
|
657
|
+
|
|
658
|
+
full_command = f"/{name} {args_str}".strip()
|
|
659
|
+
|
|
660
|
+
# Execute via agent run - slash commands go through as prompts
|
|
661
|
+
if self._client:
|
|
662
|
+
await self._client.query(full_command)
|
|
663
|
+
async for msg in self._client.receive_response():
|
|
664
|
+
if isinstance(msg, AssistantMessage):
|
|
665
|
+
for block in msg.content:
|
|
666
|
+
if isinstance(block, TextBlock):
|
|
667
|
+
await ctx.print(block.text)
|
|
668
|
+
elif isinstance(msg, UserMessage):
|
|
669
|
+
# Handle local command output wrapped in XML tags
|
|
670
|
+
content = msg.content if isinstance(msg.content, str) else ""
|
|
671
|
+
# Extract content from <local-command-stdout> or <local-command-stderr>
|
|
672
|
+
match = re.search(
|
|
673
|
+
r"<local-command-(?:stdout|stderr)>(.*?)</local-command-(?:stdout|stderr)>",
|
|
674
|
+
content,
|
|
675
|
+
re.DOTALL,
|
|
676
|
+
)
|
|
677
|
+
if match:
|
|
678
|
+
await ctx.print(match.group(1))
|
|
679
|
+
elif isinstance(msg, ResultMessage):
|
|
680
|
+
if msg.result:
|
|
681
|
+
await ctx.print(msg.result)
|
|
682
|
+
if msg.is_error:
|
|
683
|
+
await ctx.print(f"Error: {msg.subtype}")
|
|
684
|
+
|
|
685
|
+
return Command.from_raw(
|
|
686
|
+
execute_command,
|
|
687
|
+
name=name,
|
|
688
|
+
description=description or f"Claude Code command: {name}",
|
|
689
|
+
category=category,
|
|
690
|
+
usage=argument_hint,
|
|
691
|
+
)
|
|
692
|
+
|
|
539
693
|
async def run(
|
|
540
694
|
self,
|
|
541
695
|
*prompts: PromptCompatible,
|
|
@@ -576,6 +730,8 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
576
730
|
message_id: str | None = None,
|
|
577
731
|
input_provider: InputProvider | None = None,
|
|
578
732
|
message_history: MessageHistory | None = None,
|
|
733
|
+
deps: TDeps | None = None,
|
|
734
|
+
event_handlers: Sequence[IndividualEventHandler | BuiltinEventHandlerType] | None = None,
|
|
579
735
|
) -> AsyncIterator[RichAgentStreamEvent[TResult]]:
|
|
580
736
|
"""Stream events from Claude Code execution.
|
|
581
737
|
|
|
@@ -584,6 +740,8 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
584
740
|
message_id: Optional message ID for the final message
|
|
585
741
|
input_provider: Optional input provider for permission requests
|
|
586
742
|
message_history: Optional MessageHistory to use instead of agent's own
|
|
743
|
+
deps: Optional dependencies accessible via ctx.data in tools
|
|
744
|
+
event_handlers: Optional event handlers for this run (overrides agent's handlers)
|
|
587
745
|
|
|
588
746
|
Yields:
|
|
589
747
|
RichAgentStreamEvent instances during execution
|
|
@@ -603,18 +761,30 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
603
761
|
# Reset cancellation state
|
|
604
762
|
self._cancelled = False
|
|
605
763
|
self._current_stream_task = asyncio.current_task()
|
|
606
|
-
|
|
607
764
|
# Update input provider if provided
|
|
608
765
|
if input_provider is not None:
|
|
609
766
|
self._input_provider = input_provider
|
|
610
|
-
|
|
611
767
|
if not self._client:
|
|
612
|
-
|
|
613
|
-
raise RuntimeError(msg)
|
|
768
|
+
raise RuntimeError("Agent not initialized - use async context manager")
|
|
614
769
|
|
|
615
770
|
conversation = message_history if message_history is not None else self.conversation
|
|
771
|
+
# Use provided event handlers or fall back to agent's handlers
|
|
772
|
+
if event_handlers is not None:
|
|
773
|
+
from anyenv import MultiEventHandler
|
|
774
|
+
|
|
775
|
+
from agentpool.agents.events import resolve_event_handlers
|
|
776
|
+
|
|
777
|
+
handler: MultiEventHandler[IndividualEventHandler] = MultiEventHandler(
|
|
778
|
+
resolve_event_handlers(event_handlers)
|
|
779
|
+
)
|
|
780
|
+
else:
|
|
781
|
+
handler = self.event_handler
|
|
616
782
|
# Prepare prompts
|
|
617
|
-
|
|
783
|
+
# Get parent_id from last message in history for tree structure
|
|
784
|
+
last_msg_id = conversation.get_last_message_id()
|
|
785
|
+
user_msg, processed_prompts, _original_message = await prepare_prompts(
|
|
786
|
+
*prompts, parent_id=last_msg_id
|
|
787
|
+
)
|
|
618
788
|
# Get pending parts from conversation (staged content)
|
|
619
789
|
pending_parts = conversation.get_pending_parts()
|
|
620
790
|
# Combine pending parts with new prompts, then join into single string for Claude SDK
|
|
@@ -627,15 +797,28 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
627
797
|
run_id=run_id,
|
|
628
798
|
agent_name=self.name,
|
|
629
799
|
)
|
|
630
|
-
|
|
631
|
-
await handler(None, run_started)
|
|
800
|
+
await handler(None, run_started)
|
|
632
801
|
yield run_started
|
|
633
802
|
request = ModelRequest(parts=[UserPromptPart(content=prompt_text)])
|
|
634
803
|
model_messages: list[ModelResponse | ModelRequest] = [request]
|
|
635
804
|
current_response_parts: list[TextPart | ThinkingPart | ToolCallPart] = []
|
|
636
805
|
text_chunks: list[str] = []
|
|
637
806
|
pending_tool_calls: dict[str, ToolUseBlock] = {}
|
|
807
|
+
# Track tool calls that already had ToolCallStartEvent emitted (via StreamEvent)
|
|
808
|
+
emitted_tool_starts: set[str] = set()
|
|
638
809
|
|
|
810
|
+
# Accumulator for streaming tool arguments
|
|
811
|
+
from agentpool.agents.tool_call_accumulator import ToolCallAccumulator
|
|
812
|
+
|
|
813
|
+
tool_accumulator = ToolCallAccumulator()
|
|
814
|
+
|
|
815
|
+
# Track files modified during this run
|
|
816
|
+
file_tracker = FileTracker()
|
|
817
|
+
|
|
818
|
+
# Set deps on tool bridge for access during tool invocations
|
|
819
|
+
# (ContextVar doesn't work because MCP server runs in a separate task)
|
|
820
|
+
if self._tool_bridge:
|
|
821
|
+
self._tool_bridge.current_deps = deps
|
|
639
822
|
try:
|
|
640
823
|
await self._client.query(prompt_text)
|
|
641
824
|
# Merge SDK messages with event queue for real-time tool event streaming
|
|
@@ -646,8 +829,7 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
646
829
|
# Check if it's a queued event (from tools via EventEmitter)
|
|
647
830
|
if not isinstance(event_or_message, Message):
|
|
648
831
|
# It's an event from the queue - yield it immediately
|
|
649
|
-
|
|
650
|
-
await handler(None, event_or_message)
|
|
832
|
+
await handler(None, event_or_message)
|
|
651
833
|
yield event_or_message
|
|
652
834
|
continue
|
|
653
835
|
|
|
@@ -666,29 +848,60 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
666
848
|
current_response_parts.append(ThinkingPart(content=thinking))
|
|
667
849
|
case ToolUseBlockType(id=tc_id, name=name, input=input_data):
|
|
668
850
|
pending_tool_calls[tc_id] = block
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
)
|
|
673
|
-
)
|
|
674
|
-
# Emit ToolCallStartEvent with rich display info
|
|
675
|
-
from agentpool.agents.claude_code_agent.converters import (
|
|
676
|
-
derive_rich_tool_info,
|
|
851
|
+
display_name = _strip_mcp_prefix(name)
|
|
852
|
+
tool_call_part = ToolCallPart(
|
|
853
|
+
tool_name=display_name, args=input_data, tool_call_id=tc_id
|
|
677
854
|
)
|
|
855
|
+
current_response_parts.append(tool_call_part)
|
|
856
|
+
|
|
857
|
+
# Emit FunctionToolCallEvent (triggers UI notification)
|
|
858
|
+
# func_tool_event = FunctionToolCallEvent(part=tool_call_part)
|
|
859
|
+
# await handler(None, func_tool_event)
|
|
860
|
+
# yield func_tool_event
|
|
861
|
+
|
|
862
|
+
# Only emit ToolCallStartEvent if not already emitted
|
|
863
|
+
# via streaming (emits early with partial info)
|
|
864
|
+
if tc_id not in emitted_tool_starts:
|
|
865
|
+
from agentpool.agents.claude_code_agent.converters import (
|
|
866
|
+
derive_rich_tool_info,
|
|
867
|
+
)
|
|
678
868
|
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
869
|
+
rich_info = derive_rich_tool_info(name, input_data)
|
|
870
|
+
tool_start_event = ToolCallStartEvent(
|
|
871
|
+
tool_call_id=tc_id,
|
|
872
|
+
tool_name=display_name,
|
|
873
|
+
title=rich_info.title,
|
|
874
|
+
kind=rich_info.kind,
|
|
875
|
+
locations=rich_info.locations,
|
|
876
|
+
content=rich_info.content,
|
|
877
|
+
raw_input=input_data,
|
|
878
|
+
)
|
|
879
|
+
# Track file modifications
|
|
880
|
+
file_tracker.process_event(tool_start_event)
|
|
690
881
|
await handler(None, tool_start_event)
|
|
691
|
-
|
|
882
|
+
yield tool_start_event
|
|
883
|
+
else:
|
|
884
|
+
# Already emitted early - emit update with full args
|
|
885
|
+
from agentpool.agents.claude_code_agent.converters import (
|
|
886
|
+
derive_rich_tool_info,
|
|
887
|
+
)
|
|
888
|
+
|
|
889
|
+
rich_info = derive_rich_tool_info(name, input_data)
|
|
890
|
+
updated_event = ToolCallStartEvent(
|
|
891
|
+
tool_call_id=tc_id,
|
|
892
|
+
tool_name=display_name,
|
|
893
|
+
title=rich_info.title,
|
|
894
|
+
kind=rich_info.kind,
|
|
895
|
+
locations=rich_info.locations,
|
|
896
|
+
content=rich_info.content,
|
|
897
|
+
raw_input=input_data,
|
|
898
|
+
)
|
|
899
|
+
# Track file modifications using derived info
|
|
900
|
+
file_tracker.process_event(updated_event)
|
|
901
|
+
await handler(None, updated_event)
|
|
902
|
+
yield updated_event
|
|
903
|
+
# Clean up from accumulator
|
|
904
|
+
tool_accumulator.complete(tc_id)
|
|
692
905
|
case ToolResultBlock(tool_use_id=tc_id, content=content):
|
|
693
906
|
# Tool result received - flush response parts and add request
|
|
694
907
|
if current_response_parts:
|
|
@@ -698,8 +911,24 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
698
911
|
|
|
699
912
|
# Get tool name from pending calls
|
|
700
913
|
tool_use = pending_tool_calls.pop(tc_id, None)
|
|
701
|
-
tool_name =
|
|
914
|
+
tool_name = _strip_mcp_prefix(
|
|
915
|
+
tool_use.name if tool_use else "unknown"
|
|
916
|
+
)
|
|
702
917
|
tool_input = tool_use.input if tool_use else {}
|
|
918
|
+
|
|
919
|
+
# Create ToolReturnPart for the result
|
|
920
|
+
tool_return_part = ToolReturnPart(
|
|
921
|
+
tool_name=tool_name, content=content, tool_call_id=tc_id
|
|
922
|
+
)
|
|
923
|
+
|
|
924
|
+
# Emit FunctionToolResultEvent (for session.py to complete UI)
|
|
925
|
+
func_result_event = FunctionToolResultEvent(
|
|
926
|
+
result=tool_return_part
|
|
927
|
+
)
|
|
928
|
+
await handler(None, func_result_event)
|
|
929
|
+
yield func_result_event
|
|
930
|
+
|
|
931
|
+
# Also emit ToolCallCompleteEvent for consumers that expect it
|
|
703
932
|
tool_done_event = ToolCallCompleteEvent(
|
|
704
933
|
tool_name=tool_name,
|
|
705
934
|
tool_call_id=tc_id,
|
|
@@ -708,15 +937,11 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
708
937
|
agent_name=self.name,
|
|
709
938
|
message_id="",
|
|
710
939
|
)
|
|
711
|
-
|
|
712
|
-
await handler(None, tool_done_event)
|
|
940
|
+
await handler(None, tool_done_event)
|
|
713
941
|
yield tool_done_event
|
|
714
942
|
|
|
715
943
|
# Add tool return as ModelRequest
|
|
716
|
-
|
|
717
|
-
tool_name=tool_name, content=content, tool_call_id=tc_id
|
|
718
|
-
)
|
|
719
|
-
model_messages.append(ModelRequest(parts=[part]))
|
|
944
|
+
model_messages.append(ModelRequest(parts=[tool_return_part]))
|
|
720
945
|
|
|
721
946
|
# Process user messages - may contain tool results
|
|
722
947
|
elif isinstance(message, UserMessage):
|
|
@@ -738,9 +963,24 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
738
963
|
|
|
739
964
|
# Get tool name from pending calls
|
|
740
965
|
tool_use = pending_tool_calls.pop(tc_id, None)
|
|
741
|
-
tool_name =
|
|
966
|
+
tool_name = _strip_mcp_prefix(
|
|
967
|
+
tool_use.name if tool_use else "unknown"
|
|
968
|
+
)
|
|
742
969
|
tool_input = tool_use.input if tool_use else {}
|
|
743
|
-
|
|
970
|
+
|
|
971
|
+
# Create ToolReturnPart for the result
|
|
972
|
+
tool_return_part = ToolReturnPart(
|
|
973
|
+
tool_name=tool_name,
|
|
974
|
+
content=result_content,
|
|
975
|
+
tool_call_id=tc_id,
|
|
976
|
+
)
|
|
977
|
+
|
|
978
|
+
# Emit FunctionToolResultEvent (for session.py to complete UI)
|
|
979
|
+
func_result_event = FunctionToolResultEvent(result=tool_return_part)
|
|
980
|
+
await handler(None, func_result_event)
|
|
981
|
+
yield func_result_event
|
|
982
|
+
|
|
983
|
+
# Also emit ToolCallCompleteEvent for consumers that expect it
|
|
744
984
|
tool_complete_event = ToolCallCompleteEvent(
|
|
745
985
|
tool_name=tool_name,
|
|
746
986
|
tool_call_id=tc_id,
|
|
@@ -749,16 +989,11 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
749
989
|
agent_name=self.name,
|
|
750
990
|
message_id="",
|
|
751
991
|
)
|
|
752
|
-
|
|
753
|
-
await handler(None, tool_complete_event)
|
|
992
|
+
await handler(None, tool_complete_event)
|
|
754
993
|
yield tool_complete_event
|
|
994
|
+
|
|
755
995
|
# Add tool return as ModelRequest
|
|
756
|
-
|
|
757
|
-
tool_name=tool_name,
|
|
758
|
-
content=result_content,
|
|
759
|
-
tool_call_id=tc_id,
|
|
760
|
-
)
|
|
761
|
-
model_messages.append(ModelRequest(parts=[part]))
|
|
996
|
+
model_messages.append(ModelRequest(parts=[tool_return_part]))
|
|
762
997
|
|
|
763
998
|
# Handle StreamEvent for real-time streaming
|
|
764
999
|
elif isinstance(message, StreamEvent):
|
|
@@ -773,20 +1008,43 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
773
1008
|
|
|
774
1009
|
if block_type == "text":
|
|
775
1010
|
start_event = PartStartEvent(index=index, part=TextPart(content=""))
|
|
776
|
-
|
|
777
|
-
await handler(None, start_event)
|
|
1011
|
+
await handler(None, start_event)
|
|
778
1012
|
yield start_event
|
|
779
1013
|
|
|
780
1014
|
elif block_type == "thinking":
|
|
781
1015
|
thinking_part = ThinkingPart(content="")
|
|
782
1016
|
start_event = PartStartEvent(index=index, part=thinking_part)
|
|
783
|
-
|
|
784
|
-
await handler(None, start_event)
|
|
1017
|
+
await handler(None, start_event)
|
|
785
1018
|
yield start_event
|
|
786
1019
|
|
|
787
1020
|
elif block_type == "tool_use":
|
|
788
|
-
#
|
|
789
|
-
|
|
1021
|
+
# Emit ToolCallStartEvent early (args still streaming)
|
|
1022
|
+
tc_id = content_block.get("id", "")
|
|
1023
|
+
raw_tool_name = content_block.get("name", "")
|
|
1024
|
+
tool_name = _strip_mcp_prefix(raw_tool_name)
|
|
1025
|
+
tool_accumulator.start(tc_id, tool_name)
|
|
1026
|
+
# Track for permission matching - permission callback will use this
|
|
1027
|
+
# Use raw name since SDK uses raw names for permissions
|
|
1028
|
+
self._pending_tool_call_ids[raw_tool_name] = tc_id
|
|
1029
|
+
|
|
1030
|
+
# Derive rich info with empty args for now
|
|
1031
|
+
from agentpool.agents.claude_code_agent.converters import (
|
|
1032
|
+
derive_rich_tool_info,
|
|
1033
|
+
)
|
|
1034
|
+
|
|
1035
|
+
rich_info = derive_rich_tool_info(raw_tool_name, {})
|
|
1036
|
+
tool_start_event = ToolCallStartEvent(
|
|
1037
|
+
tool_call_id=tc_id,
|
|
1038
|
+
tool_name=tool_name,
|
|
1039
|
+
title=rich_info.title,
|
|
1040
|
+
kind=rich_info.kind,
|
|
1041
|
+
locations=[], # No locations yet, args not complete
|
|
1042
|
+
content=rich_info.content,
|
|
1043
|
+
raw_input={}, # Empty, will be filled when complete
|
|
1044
|
+
)
|
|
1045
|
+
emitted_tool_starts.add(tc_id)
|
|
1046
|
+
await handler(None, tool_start_event)
|
|
1047
|
+
yield tool_start_event
|
|
790
1048
|
|
|
791
1049
|
# Handle content_block_delta events (text streaming)
|
|
792
1050
|
elif event_type == "content_block_delta":
|
|
@@ -798,26 +1056,46 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
798
1056
|
if text_delta:
|
|
799
1057
|
text_part = TextPartDelta(content_delta=text_delta)
|
|
800
1058
|
delta_event = PartDeltaEvent(index=index, delta=text_part)
|
|
801
|
-
|
|
802
|
-
await handler(None, delta_event)
|
|
1059
|
+
await handler(None, delta_event)
|
|
803
1060
|
yield delta_event
|
|
804
1061
|
|
|
805
1062
|
elif delta_type == "thinking_delta":
|
|
806
1063
|
thinking_delta = delta.get("thinking", "")
|
|
807
1064
|
if thinking_delta:
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
1065
|
+
thinking_part_delta = ThinkingPartDelta(
|
|
1066
|
+
content_delta=thinking_delta
|
|
1067
|
+
)
|
|
1068
|
+
delta_event = PartDeltaEvent(
|
|
1069
|
+
index=index, delta=thinking_part_delta
|
|
1070
|
+
)
|
|
1071
|
+
await handler(None, delta_event)
|
|
812
1072
|
yield delta_event
|
|
813
1073
|
|
|
1074
|
+
elif delta_type == "input_json_delta":
|
|
1075
|
+
# Accumulate tool argument JSON fragments
|
|
1076
|
+
partial_json = delta.get("partial_json", "")
|
|
1077
|
+
if partial_json:
|
|
1078
|
+
# Find which tool call this belongs to by index
|
|
1079
|
+
# The index corresponds to the content block index
|
|
1080
|
+
for tc_id in tool_accumulator._calls:
|
|
1081
|
+
tool_accumulator.add_args(tc_id, partial_json)
|
|
1082
|
+
# Emit PartDeltaEvent with ToolCallPartDelta
|
|
1083
|
+
tool_delta = ToolCallPartDelta(
|
|
1084
|
+
tool_name_delta=None,
|
|
1085
|
+
args_delta=partial_json,
|
|
1086
|
+
tool_call_id=tc_id,
|
|
1087
|
+
)
|
|
1088
|
+
delta_event = PartDeltaEvent(index=index, delta=tool_delta)
|
|
1089
|
+
await handler(None, delta_event)
|
|
1090
|
+
yield delta_event
|
|
1091
|
+
break # Only one tool call streams at a time
|
|
1092
|
+
|
|
814
1093
|
# Handle content_block_stop events
|
|
815
1094
|
elif event_type == "content_block_stop":
|
|
816
1095
|
# We don't have the full part content here, emit with empty part
|
|
817
1096
|
# The actual content was accumulated via deltas
|
|
818
1097
|
end_event = PartEndEvent(index=index, part=TextPart(content=""))
|
|
819
|
-
|
|
820
|
-
await handler(None, end_event)
|
|
1098
|
+
await handler(None, end_event)
|
|
821
1099
|
yield end_event
|
|
822
1100
|
|
|
823
1101
|
# Skip further processing for StreamEvent - don't duplicate
|
|
@@ -832,8 +1110,7 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
832
1110
|
pending_tool_calls={}, # Already handled above
|
|
833
1111
|
)
|
|
834
1112
|
for event in events:
|
|
835
|
-
|
|
836
|
-
await handler(None, event)
|
|
1113
|
+
await handler(None, event)
|
|
837
1114
|
yield event
|
|
838
1115
|
|
|
839
1116
|
# Check for result (end of response) and capture usage info
|
|
@@ -841,25 +1118,13 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
841
1118
|
result_message = message
|
|
842
1119
|
break
|
|
843
1120
|
|
|
844
|
-
#
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
name=self.name,
|
|
852
|
-
message_id=message_id or str(uuid.uuid4()),
|
|
853
|
-
conversation_id=self.conversation_id,
|
|
854
|
-
model_name=self.model_name,
|
|
855
|
-
messages=model_messages,
|
|
856
|
-
finish_reason="stop",
|
|
857
|
-
)
|
|
858
|
-
complete_event = StreamCompleteEvent(message=response_msg)
|
|
859
|
-
for handler in self.event_handler._wrapped_handlers:
|
|
860
|
-
await handler(None, complete_event)
|
|
861
|
-
yield complete_event
|
|
862
|
-
return
|
|
1121
|
+
# Note: We do NOT return early on cancellation here.
|
|
1122
|
+
# The SDK docs warn against using break/return to exit receive_response()
|
|
1123
|
+
# early as it can cause asyncio cleanup issues. Instead, we let the
|
|
1124
|
+
# interrupt() call cause the SDK to send a ResultMessage that will
|
|
1125
|
+
# naturally terminate the stream via the isinstance(message, ResultMessage)
|
|
1126
|
+
# check above. The _cancelled flag is checked in process_prompt() to
|
|
1127
|
+
# return the correct stop reason.
|
|
863
1128
|
else:
|
|
864
1129
|
result_message = None
|
|
865
1130
|
|
|
@@ -872,23 +1137,31 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
872
1137
|
name=self.name,
|
|
873
1138
|
message_id=message_id or str(uuid.uuid4()),
|
|
874
1139
|
conversation_id=self.conversation_id,
|
|
1140
|
+
parent_id=user_msg.message_id,
|
|
875
1141
|
model_name=self.model_name,
|
|
876
1142
|
messages=model_messages,
|
|
877
1143
|
finish_reason="stop",
|
|
1144
|
+
metadata=file_tracker.get_metadata(),
|
|
878
1145
|
)
|
|
879
1146
|
complete_event = StreamCompleteEvent(message=response_msg)
|
|
880
|
-
|
|
881
|
-
await handler(None, complete_event)
|
|
1147
|
+
await handler(None, complete_event)
|
|
882
1148
|
yield complete_event
|
|
1149
|
+
# Record to history even on cancellation so context is preserved
|
|
1150
|
+
self.message_sent.emit(response_msg)
|
|
1151
|
+
conversation.add_chat_messages([user_msg, response_msg])
|
|
883
1152
|
return
|
|
884
1153
|
|
|
885
1154
|
except Exception as e:
|
|
886
1155
|
error_event = RunErrorEvent(message=str(e), run_id=run_id, agent_name=self.name)
|
|
887
|
-
|
|
888
|
-
await handler(None, error_event)
|
|
1156
|
+
await handler(None, error_event)
|
|
889
1157
|
yield error_event
|
|
890
1158
|
raise
|
|
891
1159
|
|
|
1160
|
+
finally:
|
|
1161
|
+
# Clear deps from tool bridge
|
|
1162
|
+
if self._tool_bridge:
|
|
1163
|
+
self._tool_bridge.current_deps = None
|
|
1164
|
+
|
|
892
1165
|
# Flush any remaining response parts
|
|
893
1166
|
if current_response_parts:
|
|
894
1167
|
model_messages.append(ModelResponse(parts=current_response_parts))
|
|
@@ -900,35 +1173,47 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
900
1173
|
else "".join(text_chunks)
|
|
901
1174
|
)
|
|
902
1175
|
|
|
903
|
-
# Build cost_info from ResultMessage if available
|
|
1176
|
+
# Build cost_info and usage from ResultMessage if available
|
|
904
1177
|
cost_info: TokenCost | None = None
|
|
1178
|
+
request_usage: RequestUsage | None = None
|
|
905
1179
|
if result_message and result_message.usage:
|
|
906
|
-
|
|
1180
|
+
usage_dict = result_message.usage
|
|
907
1181
|
run_usage = RunUsage(
|
|
908
|
-
input_tokens=
|
|
909
|
-
output_tokens=
|
|
910
|
-
cache_read_tokens=
|
|
911
|
-
cache_write_tokens=
|
|
1182
|
+
input_tokens=usage_dict.get("input_tokens", 0),
|
|
1183
|
+
output_tokens=usage_dict.get("output_tokens", 0),
|
|
1184
|
+
cache_read_tokens=usage_dict.get("cache_read_input_tokens", 0),
|
|
1185
|
+
cache_write_tokens=usage_dict.get("cache_creation_input_tokens", 0),
|
|
912
1186
|
)
|
|
913
1187
|
total_cost = Decimal(str(result_message.total_cost_usd or 0))
|
|
914
1188
|
cost_info = TokenCost(token_usage=run_usage, total_cost=total_cost)
|
|
1189
|
+
# Also set usage for OpenCode compatibility
|
|
1190
|
+
request_usage = RequestUsage(
|
|
1191
|
+
input_tokens=usage_dict.get("input_tokens", 0),
|
|
1192
|
+
output_tokens=usage_dict.get("output_tokens", 0),
|
|
1193
|
+
cache_read_tokens=usage_dict.get("cache_read_input_tokens", 0),
|
|
1194
|
+
cache_write_tokens=usage_dict.get("cache_creation_input_tokens", 0),
|
|
1195
|
+
)
|
|
915
1196
|
|
|
1197
|
+
# Determine finish reason - check if we were cancelled
|
|
916
1198
|
chat_message = ChatMessage[TResult](
|
|
917
1199
|
content=final_content,
|
|
918
1200
|
role="assistant",
|
|
919
1201
|
name=self.name,
|
|
920
1202
|
message_id=message_id or str(uuid.uuid4()),
|
|
921
1203
|
conversation_id=self.conversation_id,
|
|
1204
|
+
parent_id=user_msg.message_id,
|
|
922
1205
|
model_name=self.model_name,
|
|
923
1206
|
messages=model_messages,
|
|
924
1207
|
cost_info=cost_info,
|
|
1208
|
+
usage=request_usage or RequestUsage(),
|
|
925
1209
|
response_time=result_message.duration_ms / 1000 if result_message else None,
|
|
1210
|
+
finish_reason="stop" if self._cancelled else None,
|
|
1211
|
+
metadata=file_tracker.get_metadata(),
|
|
926
1212
|
)
|
|
927
1213
|
|
|
928
1214
|
# Emit stream complete
|
|
929
1215
|
complete_event = StreamCompleteEvent[TResult](message=chat_message)
|
|
930
|
-
|
|
931
|
-
await handler(None, complete_event)
|
|
1216
|
+
await handler(None, complete_event)
|
|
932
1217
|
yield complete_event
|
|
933
1218
|
# Record to history
|
|
934
1219
|
self.message_sent.emit(chat_message)
|
|
@@ -953,12 +1238,14 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
953
1238
|
async def interrupt(self) -> None:
|
|
954
1239
|
"""Interrupt the currently running stream.
|
|
955
1240
|
|
|
956
|
-
|
|
957
|
-
|
|
1241
|
+
Sets the cancelled flag and calls the Claude SDK's native interrupt()
|
|
1242
|
+
method to stop the query. The stream loop checks the flag and returns
|
|
1243
|
+
gracefully - we don't cancel the task ourselves to avoid CancelledError
|
|
1244
|
+
propagation issues.
|
|
958
1245
|
"""
|
|
959
1246
|
self._cancelled = True
|
|
960
1247
|
|
|
961
|
-
# Use Claude SDK's native interrupt
|
|
1248
|
+
# Use Claude SDK's native interrupt - this causes the SDK to stop yielding
|
|
962
1249
|
if self._client:
|
|
963
1250
|
try:
|
|
964
1251
|
await self._client.interrupt()
|
|
@@ -966,10 +1253,6 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
966
1253
|
except Exception:
|
|
967
1254
|
self.log.exception("Failed to interrupt Claude Code client")
|
|
968
1255
|
|
|
969
|
-
# Also cancel the current stream task
|
|
970
|
-
if self._current_stream_task and not self._current_stream_task.done():
|
|
971
|
-
self._current_stream_task.cancel()
|
|
972
|
-
|
|
973
1256
|
async def set_model(self, model: str) -> None:
|
|
974
1257
|
"""Set the model for future requests.
|
|
975
1258
|
|
|
@@ -999,11 +1282,163 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
999
1282
|
elif self._client and mode == "always":
|
|
1000
1283
|
await self._client.set_permission_mode("default")
|
|
1001
1284
|
|
|
1002
|
-
async def
|
|
1003
|
-
"""Get
|
|
1004
|
-
from agentpool.talk.stats import MessageStats
|
|
1285
|
+
async def get_available_models(self) -> list[ModelInfo] | None:
|
|
1286
|
+
"""Get available models for Claude Code agent.
|
|
1005
1287
|
|
|
1006
|
-
|
|
1288
|
+
Returns a static list of Claude models (opus, sonnet, haiku) since
|
|
1289
|
+
Claude Code SDK only supports these models with simple IDs.
|
|
1290
|
+
|
|
1291
|
+
Returns:
|
|
1292
|
+
List of tokonomics ModelInfo for Claude models
|
|
1293
|
+
"""
|
|
1294
|
+
from tokonomics.model_discovery.model_info import ModelInfo, ModelPricing
|
|
1295
|
+
|
|
1296
|
+
# Static Claude Code models - these are the simple IDs the SDK accepts
|
|
1297
|
+
# Use id_override to ensure pydantic_ai_id returns simple names like "opus"
|
|
1298
|
+
return [
|
|
1299
|
+
ModelInfo(
|
|
1300
|
+
id="claude-opus-4-20250514",
|
|
1301
|
+
name="Claude Opus",
|
|
1302
|
+
provider="anthropic",
|
|
1303
|
+
description="Claude Opus - most capable model",
|
|
1304
|
+
context_window=200000,
|
|
1305
|
+
max_output_tokens=32000,
|
|
1306
|
+
input_modalities={"text", "image"},
|
|
1307
|
+
output_modalities={"text"},
|
|
1308
|
+
pricing=ModelPricing(
|
|
1309
|
+
prompt=0.000015, # $15 per 1M tokens
|
|
1310
|
+
completion=0.000075, # $75 per 1M tokens
|
|
1311
|
+
),
|
|
1312
|
+
id_override="opus", # Claude Code SDK uses simple names
|
|
1313
|
+
),
|
|
1314
|
+
ModelInfo(
|
|
1315
|
+
id="claude-sonnet-4-20250514",
|
|
1316
|
+
name="Claude Sonnet",
|
|
1317
|
+
provider="anthropic",
|
|
1318
|
+
description="Claude Sonnet - balanced performance and speed",
|
|
1319
|
+
context_window=200000,
|
|
1320
|
+
max_output_tokens=16000,
|
|
1321
|
+
input_modalities={"text", "image"},
|
|
1322
|
+
output_modalities={"text"},
|
|
1323
|
+
pricing=ModelPricing(
|
|
1324
|
+
prompt=0.000003, # $3 per 1M tokens
|
|
1325
|
+
completion=0.000015, # $15 per 1M tokens
|
|
1326
|
+
),
|
|
1327
|
+
id_override="sonnet", # Claude Code SDK uses simple names
|
|
1328
|
+
),
|
|
1329
|
+
ModelInfo(
|
|
1330
|
+
id="claude-haiku-3-5-20241022",
|
|
1331
|
+
name="Claude Haiku",
|
|
1332
|
+
provider="anthropic",
|
|
1333
|
+
description="Claude Haiku - fast and cost-effective",
|
|
1334
|
+
context_window=200000,
|
|
1335
|
+
max_output_tokens=8000,
|
|
1336
|
+
input_modalities={"text", "image"},
|
|
1337
|
+
output_modalities={"text"},
|
|
1338
|
+
pricing=ModelPricing(
|
|
1339
|
+
prompt=0.0000008, # $0.80 per 1M tokens
|
|
1340
|
+
completion=0.000004, # $4 per 1M tokens
|
|
1341
|
+
),
|
|
1342
|
+
id_override="haiku", # Claude Code SDK uses simple names
|
|
1343
|
+
),
|
|
1344
|
+
]
|
|
1345
|
+
|
|
1346
|
+
def get_modes(self) -> list[ModeCategory]:
|
|
1347
|
+
"""Get available mode categories for Claude Code agent.
|
|
1348
|
+
|
|
1349
|
+
Claude Code exposes permission modes from the SDK.
|
|
1350
|
+
|
|
1351
|
+
Returns:
|
|
1352
|
+
List with single ModeCategory for Claude Code permission modes
|
|
1353
|
+
"""
|
|
1354
|
+
from agentpool.agents.modes import ModeCategory, ModeInfo
|
|
1355
|
+
|
|
1356
|
+
# Get current mode - map our confirmation mode to Claude's permission mode
|
|
1357
|
+
current_id = self._permission_mode or "default"
|
|
1358
|
+
if self.tool_confirmation_mode == "never":
|
|
1359
|
+
current_id = "bypassPermissions"
|
|
1360
|
+
|
|
1361
|
+
category_id = "permissions"
|
|
1362
|
+
return [
|
|
1363
|
+
ModeCategory(
|
|
1364
|
+
id=category_id,
|
|
1365
|
+
name="Mode",
|
|
1366
|
+
available_modes=[
|
|
1367
|
+
ModeInfo(
|
|
1368
|
+
id="default",
|
|
1369
|
+
name="Default",
|
|
1370
|
+
description="Require confirmation for tool usage",
|
|
1371
|
+
category_id=category_id,
|
|
1372
|
+
),
|
|
1373
|
+
ModeInfo(
|
|
1374
|
+
id="acceptEdits",
|
|
1375
|
+
name="Accept Edits",
|
|
1376
|
+
description="Auto-approve file edits without confirmation",
|
|
1377
|
+
category_id=category_id,
|
|
1378
|
+
),
|
|
1379
|
+
ModeInfo(
|
|
1380
|
+
id="plan",
|
|
1381
|
+
name="Plan",
|
|
1382
|
+
description="Planning mode - no tool execution",
|
|
1383
|
+
category_id=category_id,
|
|
1384
|
+
),
|
|
1385
|
+
ModeInfo(
|
|
1386
|
+
id="bypassPermissions",
|
|
1387
|
+
name="Bypass Permissions",
|
|
1388
|
+
description="Skip all permission checks (use with caution)",
|
|
1389
|
+
category_id=category_id,
|
|
1390
|
+
),
|
|
1391
|
+
],
|
|
1392
|
+
current_mode_id=current_id,
|
|
1393
|
+
)
|
|
1394
|
+
]
|
|
1395
|
+
|
|
1396
|
+
async def set_mode(self, mode: ModeInfo | str, category_id: str | None = None) -> None:
|
|
1397
|
+
"""Set a mode within a category.
|
|
1398
|
+
|
|
1399
|
+
For Claude Code, this handles permission modes from the SDK.
|
|
1400
|
+
|
|
1401
|
+
Args:
|
|
1402
|
+
mode: The mode to set - ModeInfo object or mode ID string
|
|
1403
|
+
category_id: Optional category ID (defaults to "permissions")
|
|
1404
|
+
|
|
1405
|
+
Raises:
|
|
1406
|
+
ValueError: If the category or mode is unknown
|
|
1407
|
+
"""
|
|
1408
|
+
# Extract mode_id and category from ModeInfo if provided
|
|
1409
|
+
if isinstance(mode, ModeInfo):
|
|
1410
|
+
mode_id = mode.id
|
|
1411
|
+
category_id = category_id or mode.category_id or None
|
|
1412
|
+
else:
|
|
1413
|
+
mode_id = mode
|
|
1414
|
+
|
|
1415
|
+
# Default to first (and only) category
|
|
1416
|
+
if category_id is None:
|
|
1417
|
+
category_id = "permissions"
|
|
1418
|
+
|
|
1419
|
+
if category_id != "permissions":
|
|
1420
|
+
msg = f"Unknown category: {category_id}. Only 'permissions' is supported."
|
|
1421
|
+
raise ValueError(msg)
|
|
1422
|
+
|
|
1423
|
+
# Map mode_id to PermissionMode
|
|
1424
|
+
valid_modes: set[PermissionMode] = {"default", "acceptEdits", "plan", "bypassPermissions"}
|
|
1425
|
+
if mode_id not in valid_modes:
|
|
1426
|
+
msg = f"Unknown mode: {mode_id}. Available: {list(valid_modes)}"
|
|
1427
|
+
raise ValueError(msg)
|
|
1428
|
+
|
|
1429
|
+
permission_mode: PermissionMode = mode_id # type: ignore[assignment]
|
|
1430
|
+
self._permission_mode = permission_mode
|
|
1431
|
+
|
|
1432
|
+
# Update tool confirmation mode based on permission mode
|
|
1433
|
+
if mode_id == "bypassPermissions":
|
|
1434
|
+
self.tool_confirmation_mode = "never"
|
|
1435
|
+
elif mode_id in ("default", "plan"):
|
|
1436
|
+
self.tool_confirmation_mode = "always"
|
|
1437
|
+
|
|
1438
|
+
# Update SDK client if connected
|
|
1439
|
+
if self._client:
|
|
1440
|
+
await self._client.set_permission_mode(permission_mode)
|
|
1441
|
+
self.log.info("Permission mode changed", mode=mode_id)
|
|
1007
1442
|
|
|
1008
1443
|
|
|
1009
1444
|
if __name__ == "__main__":
|
|
@@ -1011,11 +1446,23 @@ if __name__ == "__main__":
|
|
|
1011
1446
|
|
|
1012
1447
|
os.environ["ANTHROPIC_API_KEY"] = ""
|
|
1013
1448
|
|
|
1449
|
+
# async def main() -> None:
|
|
1450
|
+
# """Demo: Basic call to Claude Code."""
|
|
1451
|
+
# async with ClaudeCodeAgent(name="demo", event_handlers=["detailed"]) as agent:
|
|
1452
|
+
# print("Response (streaming): ", end="", flush=True)
|
|
1453
|
+
# async for _ in agent.run_stream("What files are in the current directory?"):
|
|
1454
|
+
# pass
|
|
1455
|
+
|
|
1014
1456
|
async def main() -> None:
|
|
1015
1457
|
"""Demo: Basic call to Claude Code."""
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1458
|
+
from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient
|
|
1459
|
+
|
|
1460
|
+
options = ClaudeAgentOptions(include_partial_messages=True)
|
|
1461
|
+
client = ClaudeSDKClient(options=options)
|
|
1462
|
+
await client.connect()
|
|
1463
|
+
prompt = "Do one tool call. list the cwd"
|
|
1464
|
+
await client.query(prompt)
|
|
1465
|
+
async for message in client.receive_response():
|
|
1466
|
+
print(message)
|
|
1020
1467
|
|
|
1021
1468
|
anyio.run(main)
|