agentpool 2.1.9__py3-none-any.whl → 2.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- acp/__init__.py +13 -4
- acp/acp_requests.py +20 -77
- acp/agent/connection.py +8 -0
- acp/agent/implementations/debug_server/debug_server.py +6 -2
- acp/agent/protocol.py +6 -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/client/connection.py +38 -29
- acp/client/implementations/default_client.py +3 -2
- acp/client/implementations/headless_client.py +2 -2
- acp/connection.py +2 -2
- acp/notifications.py +20 -50
- acp/schema/__init__.py +2 -0
- acp/schema/agent_responses.py +21 -0
- acp/schema/client_requests.py +3 -3
- acp/schema/session_state.py +63 -29
- acp/stdio.py +39 -9
- acp/task/supervisor.py +2 -2
- acp/transports.py +362 -2
- acp/utils.py +17 -4
- agentpool/__init__.py +6 -1
- agentpool/agents/__init__.py +2 -0
- agentpool/agents/acp_agent/acp_agent.py +407 -277
- agentpool/agents/acp_agent/acp_converters.py +196 -38
- agentpool/agents/acp_agent/client_handler.py +191 -26
- agentpool/agents/acp_agent/session_state.py +17 -6
- agentpool/agents/agent.py +607 -572
- agentpool/agents/agui_agent/__init__.py +0 -2
- agentpool/agents/agui_agent/agui_agent.py +176 -110
- agentpool/agents/agui_agent/agui_converters.py +0 -131
- agentpool/agents/agui_agent/helpers.py +3 -4
- agentpool/agents/base_agent.py +632 -17
- agentpool/agents/claude_code_agent/FORKING.md +191 -0
- agentpool/agents/claude_code_agent/__init__.py +13 -1
- agentpool/agents/claude_code_agent/claude_code_agent.py +1058 -291
- agentpool/agents/claude_code_agent/converters.py +74 -143
- agentpool/agents/claude_code_agent/history.py +474 -0
- agentpool/agents/claude_code_agent/models.py +77 -0
- agentpool/agents/claude_code_agent/static_info.py +100 -0
- agentpool/agents/claude_code_agent/usage.py +242 -0
- agentpool/agents/context.py +40 -0
- agentpool/agents/events/__init__.py +24 -0
- agentpool/agents/events/builtin_handlers.py +67 -1
- agentpool/agents/events/event_emitter.py +32 -2
- agentpool/agents/events/events.py +104 -3
- agentpool/agents/events/infer_info.py +145 -0
- agentpool/agents/events/processors.py +254 -0
- agentpool/agents/interactions.py +41 -6
- agentpool/agents/modes.py +67 -0
- agentpool/agents/slashed_agent.py +5 -4
- agentpool/agents/tool_call_accumulator.py +213 -0
- agentpool/agents/tool_wrapping.py +18 -6
- agentpool/common_types.py +56 -21
- agentpool/config_resources/__init__.py +38 -1
- agentpool/config_resources/acp_assistant.yml +2 -2
- agentpool/config_resources/agents.yml +3 -0
- agentpool/config_resources/agents_template.yml +1 -0
- agentpool/config_resources/claude_code_agent.yml +10 -6
- agentpool/config_resources/external_acp_agents.yml +2 -1
- agentpool/delegation/base_team.py +4 -30
- agentpool/delegation/pool.py +136 -289
- agentpool/delegation/team.py +58 -57
- agentpool/delegation/teamrun.py +51 -55
- 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/functional/run.py +10 -4
- agentpool/mcp_server/__init__.py +0 -2
- agentpool/mcp_server/client.py +76 -32
- agentpool/mcp_server/conversions.py +54 -13
- agentpool/mcp_server/manager.py +34 -54
- agentpool/mcp_server/registries/official_registry_client.py +35 -1
- agentpool/mcp_server/tool_bridge.py +186 -139
- agentpool/messaging/__init__.py +0 -2
- agentpool/messaging/compaction.py +72 -197
- agentpool/messaging/connection_manager.py +11 -10
- agentpool/messaging/event_manager.py +5 -5
- agentpool/messaging/message_container.py +6 -30
- agentpool/messaging/message_history.py +99 -8
- agentpool/messaging/messagenode.py +52 -14
- agentpool/messaging/messages.py +54 -35
- agentpool/messaging/processing.py +12 -22
- agentpool/models/__init__.py +1 -1
- agentpool/models/acp_agents/base.py +6 -24
- agentpool/models/acp_agents/mcp_capable.py +126 -157
- agentpool/models/acp_agents/non_mcp.py +129 -95
- agentpool/models/agents.py +98 -76
- agentpool/models/agui_agents.py +1 -1
- agentpool/models/claude_code_agents.py +144 -19
- agentpool/models/file_parsing.py +0 -1
- agentpool/models/manifest.py +113 -50
- agentpool/prompts/conversion_manager.py +1 -1
- agentpool/prompts/prompts.py +5 -2
- agentpool/repomap.py +1 -1
- agentpool/resource_providers/__init__.py +11 -1
- agentpool/resource_providers/aggregating.py +56 -5
- agentpool/resource_providers/base.py +70 -4
- agentpool/resource_providers/codemode/code_executor.py +72 -5
- agentpool/resource_providers/codemode/helpers.py +2 -2
- agentpool/resource_providers/codemode/provider.py +64 -12
- agentpool/resource_providers/codemode/remote_mcp_execution.py +2 -2
- agentpool/resource_providers/codemode/remote_provider.py +9 -12
- agentpool/resource_providers/filtering.py +3 -1
- agentpool/resource_providers/mcp_provider.py +89 -12
- agentpool/resource_providers/plan_provider.py +228 -46
- agentpool/resource_providers/pool.py +7 -3
- agentpool/resource_providers/resource_info.py +111 -0
- agentpool/resource_providers/static.py +4 -2
- agentpool/sessions/__init__.py +4 -1
- agentpool/sessions/manager.py +33 -5
- agentpool/sessions/models.py +59 -6
- agentpool/sessions/protocol.py +28 -0
- agentpool/sessions/session.py +11 -55
- agentpool/skills/registry.py +13 -8
- agentpool/storage/manager.py +572 -49
- agentpool/talk/registry.py +4 -4
- agentpool/talk/talk.py +9 -10
- agentpool/testing.py +538 -20
- agentpool/tool_impls/__init__.py +6 -0
- agentpool/tool_impls/agent_cli/__init__.py +42 -0
- agentpool/tool_impls/agent_cli/tool.py +95 -0
- agentpool/tool_impls/bash/__init__.py +64 -0
- agentpool/tool_impls/bash/helpers.py +35 -0
- agentpool/tool_impls/bash/tool.py +171 -0
- agentpool/tool_impls/delete_path/__init__.py +70 -0
- agentpool/tool_impls/delete_path/tool.py +142 -0
- agentpool/tool_impls/download_file/__init__.py +80 -0
- agentpool/tool_impls/download_file/tool.py +183 -0
- agentpool/tool_impls/execute_code/__init__.py +55 -0
- agentpool/tool_impls/execute_code/tool.py +163 -0
- agentpool/tool_impls/grep/__init__.py +80 -0
- agentpool/tool_impls/grep/tool.py +200 -0
- agentpool/tool_impls/list_directory/__init__.py +73 -0
- agentpool/tool_impls/list_directory/tool.py +197 -0
- agentpool/tool_impls/question/__init__.py +42 -0
- agentpool/tool_impls/question/tool.py +127 -0
- agentpool/tool_impls/read/__init__.py +104 -0
- agentpool/tool_impls/read/tool.py +305 -0
- agentpool/tools/__init__.py +2 -1
- agentpool/tools/base.py +114 -34
- agentpool/tools/manager.py +57 -1
- agentpool/ui/base.py +2 -2
- agentpool/ui/mock_provider.py +2 -2
- agentpool/ui/stdlib_provider.py +2 -2
- 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 +616 -2
- agentpool/utils/subprocess_utils.py +155 -0
- agentpool/utils/token_breakdown.py +461 -0
- agentpool/vfs_registry.py +7 -2
- {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/METADATA +41 -27
- agentpool-2.5.0.dist-info/RECORD +579 -0
- {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/WHEEL +1 -1
- agentpool_cli/__main__.py +24 -0
- agentpool_cli/create.py +1 -1
- agentpool_cli/serve_acp.py +100 -21
- agentpool_cli/serve_agui.py +87 -0
- agentpool_cli/serve_opencode.py +119 -0
- agentpool_cli/ui.py +557 -0
- agentpool_commands/__init__.py +42 -5
- agentpool_commands/agents.py +75 -2
- agentpool_commands/history.py +62 -0
- agentpool_commands/mcp.py +176 -0
- agentpool_commands/models.py +56 -3
- agentpool_commands/pool.py +260 -0
- agentpool_commands/session.py +1 -1
- agentpool_commands/text_sharing/__init__.py +119 -0
- agentpool_commands/text_sharing/base.py +123 -0
- agentpool_commands/text_sharing/github_gist.py +80 -0
- agentpool_commands/text_sharing/opencode.py +462 -0
- agentpool_commands/text_sharing/paste_rs.py +59 -0
- agentpool_commands/text_sharing/pastebin.py +116 -0
- agentpool_commands/text_sharing/shittycodingagent.py +112 -0
- agentpool_commands/tools.py +57 -0
- agentpool_commands/utils.py +80 -30
- agentpool_config/__init__.py +30 -2
- agentpool_config/agentpool_tools.py +498 -0
- agentpool_config/builtin_tools.py +77 -22
- agentpool_config/commands.py +24 -1
- agentpool_config/compaction.py +258 -0
- agentpool_config/converters.py +1 -1
- agentpool_config/event_handlers.py +42 -0
- agentpool_config/events.py +1 -1
- agentpool_config/forward_targets.py +1 -4
- agentpool_config/jinja.py +3 -3
- agentpool_config/mcp_server.py +132 -6
- agentpool_config/nodes.py +1 -1
- agentpool_config/observability.py +44 -0
- agentpool_config/session.py +0 -3
- agentpool_config/storage.py +82 -38
- agentpool_config/task.py +3 -3
- agentpool_config/tools.py +11 -22
- agentpool_config/toolsets.py +109 -233
- agentpool_server/a2a_server/agent_worker.py +307 -0
- agentpool_server/a2a_server/server.py +23 -18
- agentpool_server/acp_server/acp_agent.py +234 -181
- agentpool_server/acp_server/commands/acp_commands.py +151 -156
- agentpool_server/acp_server/commands/docs_commands/fetch_repo.py +18 -17
- agentpool_server/acp_server/event_converter.py +651 -0
- agentpool_server/acp_server/input_provider.py +53 -10
- agentpool_server/acp_server/server.py +24 -90
- agentpool_server/acp_server/session.py +173 -331
- agentpool_server/acp_server/session_manager.py +8 -34
- agentpool_server/agui_server/server.py +3 -1
- agentpool_server/mcp_server/server.py +5 -2
- agentpool_server/opencode_server/.rules +95 -0
- agentpool_server/opencode_server/ENDPOINTS.md +401 -0
- agentpool_server/opencode_server/OPENCODE_UI_TOOLS_COMPLETE.md +202 -0
- agentpool_server/opencode_server/__init__.py +19 -0
- agentpool_server/opencode_server/command_validation.py +172 -0
- agentpool_server/opencode_server/converters.py +975 -0
- agentpool_server/opencode_server/dependencies.py +24 -0
- agentpool_server/opencode_server/input_provider.py +421 -0
- agentpool_server/opencode_server/models/__init__.py +250 -0
- agentpool_server/opencode_server/models/agent.py +53 -0
- agentpool_server/opencode_server/models/app.py +72 -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 +821 -0
- agentpool_server/opencode_server/models/file.py +88 -0
- agentpool_server/opencode_server/models/mcp.py +44 -0
- agentpool_server/opencode_server/models/message.py +179 -0
- agentpool_server/opencode_server/models/parts.py +323 -0
- agentpool_server/opencode_server/models/provider.py +81 -0
- agentpool_server/opencode_server/models/pty.py +43 -0
- agentpool_server/opencode_server/models/question.py +56 -0
- agentpool_server/opencode_server/models/session.py +111 -0
- agentpool_server/opencode_server/routes/__init__.py +29 -0
- agentpool_server/opencode_server/routes/agent_routes.py +473 -0
- agentpool_server/opencode_server/routes/app_routes.py +202 -0
- agentpool_server/opencode_server/routes/config_routes.py +302 -0
- agentpool_server/opencode_server/routes/file_routes.py +571 -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 +761 -0
- agentpool_server/opencode_server/routes/permission_routes.py +63 -0
- agentpool_server/opencode_server/routes/pty_routes.py +300 -0
- agentpool_server/opencode_server/routes/question_routes.py +128 -0
- agentpool_server/opencode_server/routes/session_routes.py +1276 -0
- agentpool_server/opencode_server/routes/tui_routes.py +139 -0
- agentpool_server/opencode_server/server.py +475 -0
- agentpool_server/opencode_server/state.py +151 -0
- agentpool_server/opencode_server/time_utils.py +8 -0
- agentpool_storage/__init__.py +12 -0
- agentpool_storage/base.py +184 -2
- agentpool_storage/claude_provider/ARCHITECTURE.md +433 -0
- agentpool_storage/claude_provider/__init__.py +42 -0
- agentpool_storage/claude_provider/provider.py +1089 -0
- agentpool_storage/file_provider.py +278 -15
- agentpool_storage/memory_provider.py +193 -12
- agentpool_storage/models.py +3 -0
- agentpool_storage/opencode_provider/ARCHITECTURE.md +386 -0
- agentpool_storage/opencode_provider/__init__.py +16 -0
- agentpool_storage/opencode_provider/helpers.py +414 -0
- agentpool_storage/opencode_provider/provider.py +895 -0
- agentpool_storage/project_store.py +325 -0
- agentpool_storage/session_store.py +26 -6
- agentpool_storage/sql_provider/__init__.py +4 -2
- agentpool_storage/sql_provider/models.py +48 -0
- agentpool_storage/sql_provider/sql_provider.py +269 -3
- agentpool_storage/sql_provider/utils.py +12 -13
- agentpool_storage/zed_provider/__init__.py +16 -0
- agentpool_storage/zed_provider/helpers.py +281 -0
- agentpool_storage/zed_provider/models.py +130 -0
- agentpool_storage/zed_provider/provider.py +442 -0
- agentpool_storage/zed_provider.py +803 -0
- agentpool_toolsets/__init__.py +0 -2
- agentpool_toolsets/builtin/__init__.py +2 -12
- agentpool_toolsets/builtin/code.py +96 -57
- agentpool_toolsets/builtin/debug.py +118 -48
- agentpool_toolsets/builtin/execution_environment.py +115 -230
- agentpool_toolsets/builtin/file_edit/file_edit.py +115 -7
- agentpool_toolsets/builtin/skills.py +9 -4
- agentpool_toolsets/builtin/subagent_tools.py +64 -51
- agentpool_toolsets/builtin/workers.py +4 -2
- agentpool_toolsets/composio_toolset.py +2 -2
- agentpool_toolsets/entry_points.py +3 -1
- agentpool_toolsets/fsspec_toolset/__init__.py +13 -1
- agentpool_toolsets/fsspec_toolset/diagnostics.py +860 -73
- agentpool_toolsets/fsspec_toolset/grep.py +99 -7
- agentpool_toolsets/fsspec_toolset/helpers.py +3 -2
- agentpool_toolsets/fsspec_toolset/image_utils.py +161 -0
- agentpool_toolsets/fsspec_toolset/toolset.py +500 -95
- agentpool_toolsets/mcp_discovery/__init__.py +5 -0
- agentpool_toolsets/mcp_discovery/data/mcp_servers.parquet +0 -0
- agentpool_toolsets/mcp_discovery/toolset.py +511 -0
- agentpool_toolsets/mcp_run_toolset.py +87 -12
- agentpool_toolsets/notifications.py +33 -33
- agentpool_toolsets/openapi.py +3 -1
- agentpool_toolsets/search_toolset.py +3 -1
- agentpool-2.1.9.dist-info/RECORD +0 -474
- agentpool_config/resources.py +0 -33
- agentpool_server/acp_server/acp_tools.py +0 -43
- agentpool_server/acp_server/commands/spawn.py +0 -210
- agentpool_storage/text_log_provider.py +0 -275
- agentpool_toolsets/builtin/agent_management.py +0 -239
- agentpool_toolsets/builtin/chain.py +0 -288
- agentpool_toolsets/builtin/history.py +0 -36
- agentpool_toolsets/builtin/integration.py +0 -85
- agentpool_toolsets/builtin/tool_management.py +0 -90
- agentpool_toolsets/builtin/user_interaction.py +0 -52
- agentpool_toolsets/semantic_memory_toolset.py +0 -536
- {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/entry_points.txt +0 -0
- {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -8,6 +8,39 @@ The ClaudeCodeAgent acts as a client to the Claude Code CLI, enabling:
|
|
|
8
8
|
- Tool permission handling via callbacks
|
|
9
9
|
- Integration with agentpool's event system
|
|
10
10
|
|
|
11
|
+
Tool Call Event Flow
|
|
12
|
+
--------------------
|
|
13
|
+
The SDK streams events in a specific order. Understanding this is critical for
|
|
14
|
+
avoiding race conditions with permission dialogs:
|
|
15
|
+
|
|
16
|
+
1. **content_block_start** (StreamEvent)
|
|
17
|
+
- Contains tool_use_id, tool name
|
|
18
|
+
- We emit ToolCallStartEvent here (early, with empty args)
|
|
19
|
+
- ACP converter sends `tool_call` notification to client
|
|
20
|
+
|
|
21
|
+
2. **content_block_delta** (StreamEvent, multiple)
|
|
22
|
+
- Contains input_json_delta with partial JSON args
|
|
23
|
+
- We emit PartDeltaEvent(ToolCallPartDelta) for streaming
|
|
24
|
+
- ACP converter accumulates args, doesn't send notifications
|
|
25
|
+
|
|
26
|
+
3. **AssistantMessage** with ToolUseBlock
|
|
27
|
+
- Contains complete tool call info (id, name, full args)
|
|
28
|
+
- We do NOT emit events here (would race with permission)
|
|
29
|
+
- Just track file modifications silently
|
|
30
|
+
|
|
31
|
+
4. **content_block_stop**, **message_delta**, **message_stop** (StreamEvent)
|
|
32
|
+
- Signal completion of the message
|
|
33
|
+
|
|
34
|
+
5. **can_use_tool callback** (~100ms after message_stop)
|
|
35
|
+
- SDK calls our permission callback
|
|
36
|
+
- We send permission request to ACP client
|
|
37
|
+
- Client shows permission dialog to user
|
|
38
|
+
- IMPORTANT: No notifications should be sent while dialog is open!
|
|
39
|
+
|
|
40
|
+
6. **Tool execution or denial**
|
|
41
|
+
- If allowed: tool runs, emits ToolCallCompleteEvent
|
|
42
|
+
- If denied: SDK receives denial, continues with next turn
|
|
43
|
+
|
|
11
44
|
Example:
|
|
12
45
|
```python
|
|
13
46
|
async with ClaudeCodeAgent(
|
|
@@ -23,41 +56,46 @@ Example:
|
|
|
23
56
|
from __future__ import annotations
|
|
24
57
|
|
|
25
58
|
import asyncio
|
|
59
|
+
import contextlib
|
|
26
60
|
from decimal import Decimal
|
|
27
|
-
|
|
61
|
+
import re
|
|
62
|
+
from typing import TYPE_CHECKING, Any, Literal, Self
|
|
28
63
|
import uuid
|
|
29
64
|
|
|
30
65
|
import anyio
|
|
31
|
-
from pydantic import TypeAdapter
|
|
32
66
|
from pydantic_ai import (
|
|
67
|
+
FunctionToolResultEvent,
|
|
33
68
|
ModelRequest,
|
|
34
69
|
ModelResponse,
|
|
35
70
|
PartDeltaEvent,
|
|
36
71
|
PartEndEvent,
|
|
37
|
-
PartStartEvent,
|
|
38
72
|
RunUsage,
|
|
39
73
|
TextPart,
|
|
40
74
|
TextPartDelta,
|
|
41
75
|
ThinkingPart,
|
|
42
76
|
ThinkingPartDelta,
|
|
43
77
|
ToolCallPart,
|
|
78
|
+
ToolCallPartDelta,
|
|
44
79
|
ToolReturnPart,
|
|
45
80
|
UserPromptPart,
|
|
46
81
|
)
|
|
82
|
+
from pydantic_ai.usage import RequestUsage
|
|
47
83
|
|
|
48
84
|
from agentpool.agents.base_agent import BaseAgent
|
|
49
85
|
from agentpool.agents.claude_code_agent.converters import claude_message_to_events
|
|
50
86
|
from agentpool.agents.events import (
|
|
87
|
+
PartStartEvent,
|
|
51
88
|
RunErrorEvent,
|
|
52
89
|
RunStartedEvent,
|
|
53
90
|
StreamCompleteEvent,
|
|
54
91
|
ToolCallCompleteEvent,
|
|
55
92
|
ToolCallStartEvent,
|
|
56
93
|
)
|
|
94
|
+
from agentpool.agents.events.processors import FileTracker
|
|
95
|
+
from agentpool.agents.modes import ModeInfo
|
|
57
96
|
from agentpool.log import get_logger
|
|
58
97
|
from agentpool.messaging import ChatMessage
|
|
59
98
|
from agentpool.messaging.messages import TokenCost
|
|
60
|
-
from agentpool.messaging.processing import prepare_prompts
|
|
61
99
|
from agentpool.models.claude_code_agents import ClaudeCodeAgentConfig
|
|
62
100
|
from agentpool.utils.streams import merge_queue_into_iterator
|
|
63
101
|
|
|
@@ -75,21 +113,31 @@ if TYPE_CHECKING:
|
|
|
75
113
|
ToolPermissionContext,
|
|
76
114
|
ToolUseBlock,
|
|
77
115
|
)
|
|
78
|
-
from
|
|
116
|
+
from claude_agent_sdk.types import HookContext, HookInput, SyncHookJSONOutput
|
|
117
|
+
from evented_config import EventConfig
|
|
79
118
|
from exxec import ExecutionEnvironment
|
|
119
|
+
from pydantic_ai import UserContent
|
|
120
|
+
from slashed import BaseCommand, Command, CommandContext
|
|
121
|
+
from tokonomics.model_discovery.model_info import ModelInfo
|
|
122
|
+
from tokonomics.model_names import AnthropicMaxModelName
|
|
80
123
|
from toprompt import AnyPromptType
|
|
81
124
|
|
|
125
|
+
from agentpool.agents.claude_code_agent.models import (
|
|
126
|
+
ClaudeCodeCommandInfo,
|
|
127
|
+
ClaudeCodeServerInfo,
|
|
128
|
+
)
|
|
82
129
|
from agentpool.agents.context import AgentContext
|
|
83
130
|
from agentpool.agents.events import RichAgentStreamEvent
|
|
131
|
+
from agentpool.agents.modes import ModeCategory
|
|
84
132
|
from agentpool.common_types import (
|
|
85
133
|
BuiltinEventHandlerType,
|
|
86
134
|
IndividualEventHandler,
|
|
87
|
-
|
|
135
|
+
MCPServerStatus,
|
|
88
136
|
)
|
|
89
137
|
from agentpool.delegation import AgentPool
|
|
90
138
|
from agentpool.mcp_server.tool_bridge import ToolManagerBridge
|
|
91
139
|
from agentpool.messaging import MessageHistory
|
|
92
|
-
from agentpool.
|
|
140
|
+
from agentpool.models.claude_code_agents import SettingSource
|
|
93
141
|
from agentpool.ui.base import InputProvider
|
|
94
142
|
from agentpool_config.mcp_server import MCPServerConfig
|
|
95
143
|
from agentpool_config.nodes import ToolConfirmationMode
|
|
@@ -97,6 +145,30 @@ if TYPE_CHECKING:
|
|
|
97
145
|
|
|
98
146
|
logger = get_logger(__name__)
|
|
99
147
|
|
|
148
|
+
# Pattern to strip MCP server prefix from tool names
|
|
149
|
+
# Format: mcp__agentpool-{agent_name}-tools__{tool_name}
|
|
150
|
+
_MCP_TOOL_PATTERN = re.compile(r"^mcp__agentpool-(.+)-tools__(.+)$")
|
|
151
|
+
|
|
152
|
+
# Thinking modes for extended thinking budget allocation
|
|
153
|
+
ThinkingMode = Literal["off", "on"]
|
|
154
|
+
|
|
155
|
+
# Map thinking mode to prompt instruction
|
|
156
|
+
# "ultrathink" triggers ~32k token thinking budget in Claude Code
|
|
157
|
+
THINKING_MODE_PROMPTS: dict[ThinkingMode, str] = {
|
|
158
|
+
"off": "",
|
|
159
|
+
"on": "ultrathink",
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _strip_mcp_prefix(tool_name: str) -> str:
|
|
164
|
+
"""Strip MCP server prefix from tool names for cleaner UI display.
|
|
165
|
+
|
|
166
|
+
Handles dynamic prefixes like mcp__agentpool-{agent_name}-tools__{tool}
|
|
167
|
+
"""
|
|
168
|
+
if match := _MCP_TOOL_PATTERN.match(tool_name):
|
|
169
|
+
return match.group(2) # group(1) is agent name, group(2) is tool name
|
|
170
|
+
return tool_name
|
|
171
|
+
|
|
100
172
|
|
|
101
173
|
class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
102
174
|
"""Agent wrapping Claude Agent SDK's ClaudeSDKClient.
|
|
@@ -124,16 +196,19 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
124
196
|
disallowed_tools: list[str] | None = None,
|
|
125
197
|
system_prompt: str | Sequence[str] | None = None,
|
|
126
198
|
include_builtin_system_prompt: bool = True,
|
|
127
|
-
model: str | None = None,
|
|
199
|
+
model: AnthropicMaxModelName | str | None = None,
|
|
128
200
|
max_turns: int | None = None,
|
|
201
|
+
max_budget_usd: float | None = None,
|
|
129
202
|
max_thinking_tokens: int | None = None,
|
|
130
203
|
permission_mode: PermissionMode | None = None,
|
|
131
204
|
mcp_servers: Sequence[MCPServerConfig] | None = None,
|
|
132
205
|
environment: dict[str, str] | None = None,
|
|
133
206
|
add_dir: list[str] | None = None,
|
|
134
207
|
builtin_tools: list[str] | None = None,
|
|
135
|
-
fallback_model: str | None = None,
|
|
208
|
+
fallback_model: AnthropicMaxModelName | str | None = None,
|
|
136
209
|
dangerously_skip_permissions: bool = False,
|
|
210
|
+
setting_sources: list[SettingSource] | None = None,
|
|
211
|
+
use_subscription: bool = False,
|
|
137
212
|
env: ExecutionEnvironment | None = None,
|
|
138
213
|
input_provider: InputProvider | None = None,
|
|
139
214
|
agent_pool: AgentPool[Any] | None = None,
|
|
@@ -142,6 +217,7 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
142
217
|
event_handlers: Sequence[IndividualEventHandler | BuiltinEventHandlerType] | None = None,
|
|
143
218
|
tool_confirmation_mode: ToolConfirmationMode = "always",
|
|
144
219
|
output_type: type[TResult] | None = None,
|
|
220
|
+
commands: Sequence[BaseCommand] | None = None,
|
|
145
221
|
) -> None:
|
|
146
222
|
"""Initialize ClaudeCodeAgent.
|
|
147
223
|
|
|
@@ -157,14 +233,18 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
157
233
|
include_builtin_system_prompt: If True, the builtin system prompt is included.
|
|
158
234
|
model: Model to use (e.g., "claude-sonnet-4-5")
|
|
159
235
|
max_turns: Maximum conversation turns
|
|
236
|
+
max_budget_usd: Maximum budget to consume in dollars
|
|
160
237
|
max_thinking_tokens: Max tokens for extended thinking
|
|
161
238
|
permission_mode: Permission mode ("default", "acceptEdits", "plan", "bypassPermissions")
|
|
162
239
|
mcp_servers: External MCP servers to connect to (internal format, converted at runtime)
|
|
163
240
|
environment: Environment variables for the agent process
|
|
164
241
|
add_dir: Additional directories to allow tool access to
|
|
165
|
-
builtin_tools: Available tools from
|
|
242
|
+
builtin_tools: Available tools from built-in set. Special: "LSP" for code intelligence,
|
|
243
|
+
"Chrome" for browser control
|
|
166
244
|
fallback_model: Fallback model when default is overloaded
|
|
167
245
|
dangerously_skip_permissions: Bypass all permission checks (sandboxed only)
|
|
246
|
+
setting_sources: Setting sources to load ("user", "project", "local")
|
|
247
|
+
use_subscription: Force Claude subscription usage instead of API key
|
|
168
248
|
env: Execution environment
|
|
169
249
|
input_provider: Provider for user input/confirmations
|
|
170
250
|
agent_pool: Agent pool for multi-agent coordination
|
|
@@ -173,6 +253,7 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
173
253
|
event_handlers: Event handlers for streaming events
|
|
174
254
|
tool_confirmation_mode: Tool confirmation behavior
|
|
175
255
|
output_type: Type for structured output (uses JSON schema)
|
|
256
|
+
commands: Slash commands
|
|
176
257
|
"""
|
|
177
258
|
from agentpool.agents.sys_prompts import SystemPrompts
|
|
178
259
|
|
|
@@ -197,6 +278,8 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
197
278
|
builtin_tools=builtin_tools,
|
|
198
279
|
fallback_model=fallback_model,
|
|
199
280
|
dangerously_skip_permissions=dangerously_skip_permissions,
|
|
281
|
+
setting_sources=setting_sources,
|
|
282
|
+
use_subscription=use_subscription,
|
|
200
283
|
)
|
|
201
284
|
|
|
202
285
|
super().__init__(
|
|
@@ -211,6 +294,7 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
211
294
|
output_type=output_type or str, # type: ignore[arg-type]
|
|
212
295
|
tool_confirmation_mode=tool_confirmation_mode,
|
|
213
296
|
event_handlers=event_handlers,
|
|
297
|
+
commands=commands,
|
|
214
298
|
)
|
|
215
299
|
|
|
216
300
|
self._config = config
|
|
@@ -234,8 +318,10 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
234
318
|
self.sys_prompts = SystemPrompts(all_prompts, prompt_manager=prompt_manager)
|
|
235
319
|
self._model = model or config.model
|
|
236
320
|
self._max_turns = max_turns or config.max_turns
|
|
321
|
+
self._max_budget_usd = max_budget_usd or config.max_budget_usd
|
|
237
322
|
self._max_thinking_tokens = max_thinking_tokens or config.max_thinking_tokens
|
|
238
323
|
self._permission_mode: PermissionMode | None = permission_mode or config.permission_mode
|
|
324
|
+
self._thinking_mode: ThinkingMode = "off"
|
|
239
325
|
self._external_mcp_servers = list(mcp_servers) if mcp_servers else config.get_mcp_servers()
|
|
240
326
|
self._environment = environment or config.env
|
|
241
327
|
self._add_dir = add_dir or config.add_dir
|
|
@@ -244,10 +330,14 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
244
330
|
self._dangerously_skip_permissions = (
|
|
245
331
|
dangerously_skip_permissions or config.dangerously_skip_permissions
|
|
246
332
|
)
|
|
333
|
+
self._setting_sources = setting_sources or config.setting_sources
|
|
334
|
+
self._use_subscription = use_subscription or config.use_subscription
|
|
247
335
|
|
|
248
336
|
# Client state
|
|
249
337
|
self._client: ClaudeSDKClient | None = None
|
|
250
|
-
self.
|
|
338
|
+
self._connection_task: asyncio.Task[None] | None = None
|
|
339
|
+
self._current_model: AnthropicMaxModelName | str | None = self._model
|
|
340
|
+
self._sdk_session_id: str | None = None # Session ID from Claude SDK init message
|
|
251
341
|
self.deps_type = type(None)
|
|
252
342
|
|
|
253
343
|
# ToolBridge state for exposing toolsets via MCP
|
|
@@ -255,6 +345,49 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
255
345
|
self._owns_bridge = False # Track if we created the bridge (for cleanup)
|
|
256
346
|
self._mcp_servers: dict[str, McpServerConfig] = {} # Claude SDK MCP server configs
|
|
257
347
|
|
|
348
|
+
# Track pending tool call for permission matching
|
|
349
|
+
# Maps tool_name to tool_call_id for matching permissions to tool call UI parts
|
|
350
|
+
self._pending_tool_call_ids: dict[str, str] = {}
|
|
351
|
+
|
|
352
|
+
@classmethod
|
|
353
|
+
def from_config(
|
|
354
|
+
cls,
|
|
355
|
+
config: ClaudeCodeAgentConfig,
|
|
356
|
+
*,
|
|
357
|
+
event_handlers: Sequence[IndividualEventHandler | BuiltinEventHandlerType] | None = None,
|
|
358
|
+
input_provider: InputProvider | None = None,
|
|
359
|
+
agent_pool: AgentPool[Any] | None = None,
|
|
360
|
+
output_type: type[TResult] | None = None,
|
|
361
|
+
) -> Self:
|
|
362
|
+
"""Create a ClaudeCodeAgent from a config object.
|
|
363
|
+
|
|
364
|
+
This is the preferred way to instantiate a ClaudeCodeAgent from configuration.
|
|
365
|
+
|
|
366
|
+
Args:
|
|
367
|
+
config: Claude Code agent configuration
|
|
368
|
+
event_handlers: Optional event handlers (merged with config handlers)
|
|
369
|
+
input_provider: Optional input provider for user interactions
|
|
370
|
+
agent_pool: Optional agent pool for coordination
|
|
371
|
+
output_type: Optional output type for structured output
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
Configured ClaudeCodeAgent instance
|
|
375
|
+
"""
|
|
376
|
+
# Merge config-level handlers with provided handlers
|
|
377
|
+
config_handlers = config.get_event_handlers()
|
|
378
|
+
merged_handlers: list[IndividualEventHandler | BuiltinEventHandlerType] = [
|
|
379
|
+
*config_handlers,
|
|
380
|
+
*(event_handlers or []),
|
|
381
|
+
]
|
|
382
|
+
return cls(
|
|
383
|
+
config=config,
|
|
384
|
+
event_handlers=merged_handlers or None,
|
|
385
|
+
input_provider=input_provider,
|
|
386
|
+
agent_pool=agent_pool,
|
|
387
|
+
tool_confirmation_mode=config.requires_tool_confirmation,
|
|
388
|
+
output_type=output_type,
|
|
389
|
+
)
|
|
390
|
+
|
|
258
391
|
def get_context(self, data: Any = None) -> AgentContext:
|
|
259
392
|
"""Create a new context for this agent.
|
|
260
393
|
|
|
@@ -269,60 +402,14 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
269
402
|
|
|
270
403
|
defn = self.agent_pool.manifest if self.agent_pool else AgentsManifest()
|
|
271
404
|
return AgentContext(
|
|
272
|
-
node=self,
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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,
|
|
405
|
+
node=self,
|
|
406
|
+
pool=self.agent_pool,
|
|
407
|
+
config=self._config,
|
|
408
|
+
definition=defn,
|
|
409
|
+
input_provider=self._input_provider,
|
|
410
|
+
data=data,
|
|
287
411
|
)
|
|
288
412
|
|
|
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
413
|
async def _setup_toolsets(self) -> None:
|
|
327
414
|
"""Initialize toolsets from config and create bridge if needed.
|
|
328
415
|
|
|
@@ -330,34 +417,30 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
330
417
|
and starts an MCP bridge to expose them to Claude Code via the SDK's
|
|
331
418
|
native MCP support. Also converts external MCP servers to SDK format.
|
|
332
419
|
"""
|
|
420
|
+
from agentpool.agents.claude_code_agent.converters import convert_mcp_servers_to_sdk_format
|
|
333
421
|
from agentpool.mcp_server.tool_bridge import BridgeConfig, ToolManagerBridge
|
|
334
422
|
|
|
335
423
|
# Convert external MCP servers to SDK format first
|
|
336
424
|
if self._external_mcp_servers:
|
|
337
|
-
external_configs = self.
|
|
425
|
+
external_configs = convert_mcp_servers_to_sdk_format(self._external_mcp_servers)
|
|
338
426
|
self._mcp_servers.update(external_configs)
|
|
339
427
|
self.log.info("External MCP servers configured", server_count=len(external_configs))
|
|
340
428
|
|
|
341
|
-
if not self._config.
|
|
429
|
+
if not self._config.tools:
|
|
342
430
|
return
|
|
343
431
|
|
|
344
|
-
# Create providers from
|
|
345
|
-
for
|
|
346
|
-
provider = toolset_config.get_provider()
|
|
432
|
+
# Create providers from tool configs and add to tool manager
|
|
433
|
+
for provider in self._config.get_tool_providers():
|
|
347
434
|
self.tools.add_provider(provider)
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
config = BridgeConfig(
|
|
351
|
-
transport="streamable-http", server_name=f"agentpool-{self.name}-tools"
|
|
352
|
-
)
|
|
435
|
+
server_name = f"agentpool-{self.name}-tools"
|
|
436
|
+
config = BridgeConfig(server_name=server_name)
|
|
353
437
|
self._tool_bridge = ToolManagerBridge(node=self, config=config)
|
|
354
438
|
await self._tool_bridge.start()
|
|
355
439
|
self._owns_bridge = True
|
|
356
|
-
|
|
357
440
|
# Get Claude SDK-compatible MCP config and merge into our servers dict
|
|
358
441
|
mcp_config = self._tool_bridge.get_claude_mcp_server_config()
|
|
359
442
|
self._mcp_servers.update(mcp_config)
|
|
360
|
-
self.log.info("
|
|
443
|
+
self.log.info("Tools initialized", tool_count=len(self._config.tools))
|
|
361
444
|
|
|
362
445
|
async def add_tool_bridge(self, bridge: ToolManagerBridge) -> None:
|
|
363
446
|
"""Add an external tool bridge to expose its tools via MCP.
|
|
@@ -372,25 +455,64 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
372
455
|
"""
|
|
373
456
|
if self._tool_bridge is None: # Don't replace our own bridge
|
|
374
457
|
self._tool_bridge = bridge
|
|
375
|
-
|
|
376
458
|
# Get Claude SDK-compatible config and merge
|
|
377
459
|
mcp_config = bridge.get_claude_mcp_server_config()
|
|
378
460
|
self._mcp_servers.update(mcp_config)
|
|
379
461
|
self.log.info("Added external tool bridge", server_name=bridge.config.server_name)
|
|
380
462
|
|
|
381
|
-
async def _cleanup_bridge(self) -> None:
|
|
382
|
-
"""Clean up tool bridge resources."""
|
|
383
|
-
if self._tool_bridge and self._owns_bridge:
|
|
384
|
-
await self._tool_bridge.stop()
|
|
385
|
-
self._tool_bridge = None
|
|
386
|
-
self._owns_bridge = False
|
|
387
|
-
self._mcp_servers.clear()
|
|
388
|
-
|
|
389
463
|
@property
|
|
390
464
|
def model_name(self) -> str | None:
|
|
391
465
|
"""Get the model name."""
|
|
392
466
|
return self._current_model
|
|
393
467
|
|
|
468
|
+
def get_mcp_server_info(self) -> dict[str, MCPServerStatus]:
|
|
469
|
+
"""Get information about configured MCP servers.
|
|
470
|
+
|
|
471
|
+
Returns a dict mapping server names to their status info. This is used
|
|
472
|
+
by the OpenCode /mcp endpoint to display MCP servers in the sidebar.
|
|
473
|
+
|
|
474
|
+
Returns:
|
|
475
|
+
Dict mapping server name to MCPServerStatus dataclass
|
|
476
|
+
"""
|
|
477
|
+
from agentpool.common_types import MCPServerStatus
|
|
478
|
+
|
|
479
|
+
result: dict[str, MCPServerStatus] = {}
|
|
480
|
+
for name, config in self._mcp_servers.items():
|
|
481
|
+
server_type = config.get("type", "unknown")
|
|
482
|
+
result[name] = MCPServerStatus(
|
|
483
|
+
name=name,
|
|
484
|
+
status="connected", # Claude SDK manages connections
|
|
485
|
+
server_type=server_type,
|
|
486
|
+
)
|
|
487
|
+
return result
|
|
488
|
+
|
|
489
|
+
def _build_hooks(self) -> dict[str, list[Any]]:
|
|
490
|
+
"""Build SDK hooks configuration.
|
|
491
|
+
|
|
492
|
+
Returns:
|
|
493
|
+
Dictionary mapping hook event names to HookMatcher lists
|
|
494
|
+
"""
|
|
495
|
+
from claude_agent_sdk.types import HookMatcher
|
|
496
|
+
|
|
497
|
+
async def on_pre_compact(
|
|
498
|
+
input_data: HookInput,
|
|
499
|
+
tool_use_id: str | None,
|
|
500
|
+
context: HookContext,
|
|
501
|
+
) -> SyncHookJSONOutput:
|
|
502
|
+
"""Handle PreCompact hook by emitting a CompactionEvent."""
|
|
503
|
+
from agentpool.agents.events import CompactionEvent
|
|
504
|
+
|
|
505
|
+
# input_data is PreCompactHookInput when hook_event_name == "PreCompact"
|
|
506
|
+
trigger_value = input_data.get("trigger", "auto")
|
|
507
|
+
trigger: Literal["auto", "manual"] = "manual" if trigger_value == "manual" else "auto"
|
|
508
|
+
# Emit semantic CompactionEvent - consumers handle display differently
|
|
509
|
+
ses_id = self.conversation_id or "unknown"
|
|
510
|
+
compaction_event = CompactionEvent(session_id=ses_id, trigger=trigger, phase="starting")
|
|
511
|
+
await self._event_queue.put(compaction_event)
|
|
512
|
+
return {"continue_": True}
|
|
513
|
+
|
|
514
|
+
return {"PreCompact": [HookMatcher(matcher=None, hooks=[on_pre_compact])]}
|
|
515
|
+
|
|
394
516
|
def _build_options(self, *, formatted_system_prompt: str | None = None) -> ClaudeAgentOptions:
|
|
395
517
|
"""Build ClaudeAgentOptions from runtime state.
|
|
396
518
|
|
|
@@ -400,6 +522,8 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
400
522
|
from claude_agent_sdk import ClaudeAgentOptions
|
|
401
523
|
from claude_agent_sdk.types import SystemPromptPreset
|
|
402
524
|
|
|
525
|
+
from agentpool.agents.claude_code_agent.converters import to_output_format
|
|
526
|
+
|
|
403
527
|
# Build system prompt value
|
|
404
528
|
system_prompt: str | SystemPromptPreset | None = None
|
|
405
529
|
if formatted_system_prompt:
|
|
@@ -424,12 +548,24 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
424
548
|
self._can_use_tool if self.tool_confirmation_mode != "never" and not bypass else None
|
|
425
549
|
)
|
|
426
550
|
|
|
427
|
-
#
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
551
|
+
# Check builtin_tools for special tools that need extra handling
|
|
552
|
+
builtin_tools = self._builtin_tools or []
|
|
553
|
+
|
|
554
|
+
# Build extra_args for CLI flags not directly exposed
|
|
555
|
+
extra_args: dict[str, str | None] = {}
|
|
556
|
+
if "Chrome" in builtin_tools:
|
|
557
|
+
extra_args["chrome"] = None
|
|
558
|
+
|
|
559
|
+
# Build environment variables
|
|
560
|
+
env = dict(self._environment or {})
|
|
561
|
+
env["CLAUDE_AGENT_SDK_SKIP_VERSION_CHECK"] = "1"
|
|
562
|
+
env["CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC"] = "1"
|
|
563
|
+
if "LSP" in builtin_tools:
|
|
564
|
+
# Enable LSP tool support
|
|
565
|
+
env["ENABLE_LSP_TOOL"] = "1"
|
|
566
|
+
if self._use_subscription:
|
|
567
|
+
# Force subscription usage by clearing API key
|
|
568
|
+
env["ANTHROPIC_API_KEY"] = ""
|
|
433
569
|
|
|
434
570
|
return ClaudeAgentOptions(
|
|
435
571
|
cwd=self._cwd,
|
|
@@ -438,16 +574,20 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
438
574
|
system_prompt=system_prompt,
|
|
439
575
|
model=self._model,
|
|
440
576
|
max_turns=self._max_turns,
|
|
577
|
+
max_budget_usd=self._max_budget_usd,
|
|
441
578
|
max_thinking_tokens=self._max_thinking_tokens,
|
|
442
579
|
permission_mode=permission_mode,
|
|
443
|
-
env=
|
|
580
|
+
env=env,
|
|
444
581
|
add_dirs=self._add_dir or [], # type: ignore[arg-type] # SDK uses list not Sequence
|
|
445
582
|
tools=self._builtin_tools,
|
|
446
583
|
fallback_model=self._fallback_model,
|
|
447
584
|
can_use_tool=can_use_tool,
|
|
448
|
-
output_format=
|
|
585
|
+
output_format=to_output_format(self._output_type),
|
|
449
586
|
mcp_servers=self._mcp_servers or {},
|
|
450
587
|
include_partial_messages=True,
|
|
588
|
+
hooks=self._build_hooks(), # type: ignore[arg-type]
|
|
589
|
+
setting_sources=self._setting_sources,
|
|
590
|
+
extra_args=extra_args,
|
|
451
591
|
)
|
|
452
592
|
|
|
453
593
|
async def _can_use_tool( # noqa: PLR0911
|
|
@@ -458,36 +598,70 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
458
598
|
) -> PermissionResult:
|
|
459
599
|
"""Handle tool permission requests.
|
|
460
600
|
|
|
601
|
+
This callback fires in two cases:
|
|
602
|
+
1. Tool needs approval: Claude wants to use a tool that isn't auto-approved
|
|
603
|
+
2. Claude asks a question: Claude calls the AskUserQuestion tool for clarification
|
|
604
|
+
|
|
461
605
|
Args:
|
|
462
|
-
tool_name: Name of the tool being called
|
|
606
|
+
tool_name: Name of the tool being called (e.g., "Bash", "Write", "AskUserQuestion")
|
|
463
607
|
input_data: Tool input arguments
|
|
464
608
|
context: Permission context with suggestions
|
|
465
609
|
|
|
466
610
|
Returns:
|
|
467
611
|
PermissionResult indicating allow or deny
|
|
468
612
|
"""
|
|
613
|
+
import uuid
|
|
614
|
+
|
|
469
615
|
from claude_agent_sdk import PermissionResultAllow, PermissionResultDeny
|
|
470
616
|
|
|
471
|
-
from agentpool.tools
|
|
617
|
+
from agentpool.tools import FunctionTool
|
|
472
618
|
|
|
473
|
-
#
|
|
619
|
+
# Handle AskUserQuestion specially - this is Claude asking for clarification
|
|
620
|
+
if tool_name == "AskUserQuestion":
|
|
621
|
+
return await self._handle_clarifying_questions(input_data, context)
|
|
622
|
+
|
|
623
|
+
# Auto-grant if confirmation mode is "never" (bypassPermissions)
|
|
474
624
|
if self.tool_confirmation_mode == "never":
|
|
475
625
|
return PermissionResultAllow()
|
|
476
626
|
|
|
477
|
-
#
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
627
|
+
# For "acceptEdits" mode: auto-allow edit/write tools only
|
|
628
|
+
if self._permission_mode == "acceptEdits":
|
|
629
|
+
# Extract the actual tool name from MCP-style names
|
|
630
|
+
# e.g., "mcp__agentpool-claude-tools__edit" -> "edit"
|
|
631
|
+
actual_tool_name = tool_name
|
|
632
|
+
if "__" in tool_name:
|
|
633
|
+
actual_tool_name = tool_name.rsplit("__", 1)[-1]
|
|
634
|
+
# Auto-allow file editing tools
|
|
635
|
+
if actual_tool_name.lower() in ("edit", "write", "edit_file", "write_file"):
|
|
482
636
|
return PermissionResultAllow()
|
|
483
637
|
|
|
484
|
-
#
|
|
638
|
+
# For "default" mode and non-edit tools in "acceptEdits" mode:
|
|
639
|
+
# Ask for confirmation via input provider
|
|
485
640
|
if self._input_provider:
|
|
641
|
+
# Get tool_use_id from SDK context if available (requires SDK >= 0.1.19)
|
|
642
|
+
# TODO: Remove fallback once claude-agent-sdk with tool_use_id is released
|
|
643
|
+
if hasattr(context, "tool_use_id") and (tc_id := context.tool_use_id): # pyright: ignore[reportAttributeAccessIssue]
|
|
644
|
+
tool_call_id = tc_id
|
|
645
|
+
else:
|
|
646
|
+
# Fallback: look up from streaming events or generate our own
|
|
647
|
+
tool_call_id = self._pending_tool_call_ids.get(tool_name)
|
|
648
|
+
if not tool_call_id:
|
|
649
|
+
tool_call_id = f"perm_{uuid.uuid4().hex[:12]}"
|
|
650
|
+
self._pending_tool_call_ids[tool_name] = tool_call_id
|
|
651
|
+
|
|
652
|
+
display_name = _strip_mcp_prefix(tool_name)
|
|
653
|
+
self.log.debug("Permission request", tool_name=display_name, tool_call_id=tool_call_id)
|
|
486
654
|
# Create a dummy Tool for the confirmation dialog
|
|
487
655
|
desc = f"Claude Code tool: {tool_name}"
|
|
488
|
-
tool =
|
|
656
|
+
tool = FunctionTool(callable=lambda: None, name=display_name, description=desc)
|
|
657
|
+
ctx = self.get_context()
|
|
658
|
+
# Attach tool_call_id to context for permission event
|
|
659
|
+
ctx.tool_call_id = tool_call_id
|
|
660
|
+
# Also pass tool input for ACPInputProvider to generate proper title
|
|
661
|
+
ctx.tool_input = input_data
|
|
662
|
+
ctx.tool_name = tool_name
|
|
489
663
|
result = await self._input_provider.get_tool_confirmation(
|
|
490
|
-
context=
|
|
664
|
+
context=ctx,
|
|
491
665
|
tool=tool,
|
|
492
666
|
args=input_data,
|
|
493
667
|
)
|
|
@@ -505,8 +679,157 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
505
679
|
# Default: deny if no input provider
|
|
506
680
|
return PermissionResultDeny(message="No input provider configured")
|
|
507
681
|
|
|
682
|
+
async def _handle_clarifying_questions(
|
|
683
|
+
self,
|
|
684
|
+
input_data: dict[str, Any],
|
|
685
|
+
context: ToolPermissionContext,
|
|
686
|
+
) -> PermissionResult:
|
|
687
|
+
"""Handle AskUserQuestion tool - Claude asking for clarification.
|
|
688
|
+
|
|
689
|
+
The input contains Claude's questions with multiple-choice options.
|
|
690
|
+
We present these to the user and return their selections.
|
|
691
|
+
|
|
692
|
+
Users can respond with:
|
|
693
|
+
- A number (1-based index): "2" selects the second option
|
|
694
|
+
- A label: "Summary" (case-insensitive)
|
|
695
|
+
- Free text: "jquery" or "I don't know" (used directly as the answer)
|
|
696
|
+
- Multiple selections (for multi-select): "1, 3" or "Summary, Conclusion"
|
|
697
|
+
|
|
698
|
+
Question format from Claude:
|
|
699
|
+
{
|
|
700
|
+
"questions": [
|
|
701
|
+
{
|
|
702
|
+
"question": "How should I format the output?",
|
|
703
|
+
"header": "Format",
|
|
704
|
+
"options": [
|
|
705
|
+
{"label": "Summary", "description": "Brief overview"},
|
|
706
|
+
{"label": "Detailed", "description": "Full explanation"}
|
|
707
|
+
],
|
|
708
|
+
"multiSelect": false
|
|
709
|
+
}
|
|
710
|
+
]
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
Response format:
|
|
714
|
+
{
|
|
715
|
+
"questions": [...], # Original questions passed through
|
|
716
|
+
"answers": {
|
|
717
|
+
"How should I format the output?": "Summary",
|
|
718
|
+
"Which sections?": "Introduction, Conclusion" # Multi-select joined with ", "
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
Args:
|
|
723
|
+
input_data: Contains 'questions' array with question objects
|
|
724
|
+
context: Permission context
|
|
725
|
+
|
|
726
|
+
Returns:
|
|
727
|
+
PermissionResult with updated input containing user's answers
|
|
728
|
+
"""
|
|
729
|
+
from claude_agent_sdk import PermissionResultAllow, PermissionResultDeny
|
|
730
|
+
|
|
731
|
+
if not self._input_provider:
|
|
732
|
+
return PermissionResultDeny(message="No input provider configured for questions")
|
|
733
|
+
|
|
734
|
+
questions = input_data.get("questions", [])
|
|
735
|
+
if not questions:
|
|
736
|
+
return PermissionResultDeny(message="No questions provided")
|
|
737
|
+
|
|
738
|
+
# Collect answers from the user
|
|
739
|
+
answers: dict[str, str] = {}
|
|
740
|
+
|
|
741
|
+
for question_obj in questions:
|
|
742
|
+
question_text = question_obj.get("question", "")
|
|
743
|
+
header = question_obj.get("header", "")
|
|
744
|
+
options = question_obj.get("options", [])
|
|
745
|
+
multi_select = question_obj.get("multiSelect", False)
|
|
746
|
+
|
|
747
|
+
if not question_text or not options:
|
|
748
|
+
continue
|
|
749
|
+
|
|
750
|
+
# Format the question for display
|
|
751
|
+
formatted_question = f"{header}: {question_text}" if header else question_text
|
|
752
|
+
option_labels = [opt.get("label", "") for opt in options]
|
|
753
|
+
option_descriptions = {
|
|
754
|
+
opt.get("label", ""): opt.get("description", "") for opt in options
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
# Get user's answer via input provider
|
|
758
|
+
try:
|
|
759
|
+
# Build a display string showing the options
|
|
760
|
+
options_display = "\n".join(
|
|
761
|
+
f" {i + 1}. {label}"
|
|
762
|
+
+ (f" - {option_descriptions[label]}" if option_descriptions[label] else "")
|
|
763
|
+
for i, label in enumerate(option_labels)
|
|
764
|
+
)
|
|
765
|
+
full_prompt = f"{formatted_question}\n\nOptions:\n{options_display}\n\n"
|
|
766
|
+
if multi_select:
|
|
767
|
+
full_prompt += (
|
|
768
|
+
"(Enter numbers separated by commas, or type your own answer)\n"
|
|
769
|
+
"Your choice: "
|
|
770
|
+
)
|
|
771
|
+
else:
|
|
772
|
+
full_prompt += "(Enter a number, or type your own answer)\nYour choice: "
|
|
773
|
+
|
|
774
|
+
# Use input provider to get user response
|
|
775
|
+
ctx = self.get_context()
|
|
776
|
+
user_input = await self._input_provider.get_input(
|
|
777
|
+
context=ctx,
|
|
778
|
+
prompt=full_prompt,
|
|
779
|
+
)
|
|
780
|
+
|
|
781
|
+
if user_input is None:
|
|
782
|
+
return PermissionResultDeny(message="User cancelled question", interrupt=True)
|
|
783
|
+
|
|
784
|
+
# Parse user input - handle numbers, labels, or free text
|
|
785
|
+
# This follows the SDK pattern: try numeric -> try label -> use free text
|
|
786
|
+
if multi_select:
|
|
787
|
+
# Split by comma for multi-select
|
|
788
|
+
selections = [s.strip() for s in user_input.split(",")]
|
|
789
|
+
else:
|
|
790
|
+
selections = [user_input.strip()]
|
|
791
|
+
|
|
792
|
+
selected_values: list[str] = []
|
|
793
|
+
for selection in selections:
|
|
794
|
+
# Try to parse as number first
|
|
795
|
+
if selection.isdigit():
|
|
796
|
+
idx = int(selection) - 1
|
|
797
|
+
if 0 <= idx < len(option_labels):
|
|
798
|
+
# Valid number - use the option's label
|
|
799
|
+
selected_values.append(option_labels[idx])
|
|
800
|
+
else:
|
|
801
|
+
# Invalid number - treat as free text
|
|
802
|
+
selected_values.append(selection)
|
|
803
|
+
else:
|
|
804
|
+
# Try to match label (case-insensitive)
|
|
805
|
+
matching = [
|
|
806
|
+
lbl for lbl in option_labels if lbl.lower() == selection.lower()
|
|
807
|
+
]
|
|
808
|
+
if matching:
|
|
809
|
+
# Matched a label - use it
|
|
810
|
+
selected_values.append(matching[0])
|
|
811
|
+
else:
|
|
812
|
+
# No match - use as free text
|
|
813
|
+
selected_values.append(selection)
|
|
814
|
+
|
|
815
|
+
# Store answer - join multiple selections with ", "
|
|
816
|
+
# Use free text directly if provided (not "Other")
|
|
817
|
+
answers[question_text] = ", ".join(selected_values)
|
|
818
|
+
|
|
819
|
+
except Exception as e:
|
|
820
|
+
self.log.exception("Error getting clarifying question answer")
|
|
821
|
+
return PermissionResultDeny(message=f"Error collecting answer: {e}", interrupt=True)
|
|
822
|
+
|
|
823
|
+
# Return the answers to Claude
|
|
824
|
+
return PermissionResultAllow(
|
|
825
|
+
updated_input={
|
|
826
|
+
"questions": questions,
|
|
827
|
+
"answers": answers,
|
|
828
|
+
}
|
|
829
|
+
)
|
|
830
|
+
|
|
508
831
|
async def __aenter__(self) -> Self:
|
|
509
|
-
"""Connect to Claude Code."""
|
|
832
|
+
"""Connect to Claude Code with deferred client connection."""
|
|
510
833
|
from claude_agent_sdk import ClaudeSDKClient
|
|
511
834
|
|
|
512
835
|
await super().__aenter__()
|
|
@@ -514,10 +837,31 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
514
837
|
formatted_prompt = await self.sys_prompts.format_system_prompt(self)
|
|
515
838
|
options = self._build_options(formatted_system_prompt=formatted_prompt)
|
|
516
839
|
self._client = ClaudeSDKClient(options=options)
|
|
517
|
-
|
|
518
|
-
|
|
840
|
+
# Start connection in background task to reduce first-prompt latency
|
|
841
|
+
# The task owns the anyio context, we just await it when needed
|
|
842
|
+
self._connection_task = asyncio.create_task(self._do_connect())
|
|
519
843
|
return self
|
|
520
844
|
|
|
845
|
+
async def _do_connect(self) -> None:
|
|
846
|
+
"""Actually connect the client. Runs in background task."""
|
|
847
|
+
if not self._client:
|
|
848
|
+
msg = "Client not created - call __aenter__ first"
|
|
849
|
+
raise RuntimeError(msg)
|
|
850
|
+
|
|
851
|
+
try:
|
|
852
|
+
await self._client.connect()
|
|
853
|
+
await self.populate_commands()
|
|
854
|
+
self.log.info("Claude Code client connected")
|
|
855
|
+
except Exception:
|
|
856
|
+
self.log.exception("Failed to connect Claude Code client")
|
|
857
|
+
raise
|
|
858
|
+
|
|
859
|
+
async def ensure_initialized(self) -> None:
|
|
860
|
+
"""Wait for background connection task to complete."""
|
|
861
|
+
if self._connection_task:
|
|
862
|
+
await self._connection_task
|
|
863
|
+
self._connection_task = None
|
|
864
|
+
|
|
521
865
|
async def __aexit__(
|
|
522
866
|
self,
|
|
523
867
|
exc_type: type[BaseException] | None,
|
|
@@ -525,8 +869,19 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
525
869
|
exc_tb: TracebackType | None,
|
|
526
870
|
) -> None:
|
|
527
871
|
"""Disconnect from Claude Code."""
|
|
872
|
+
# Cancel connection task if still running
|
|
873
|
+
if self._connection_task and not self._connection_task.done():
|
|
874
|
+
self._connection_task.cancel()
|
|
875
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
876
|
+
await self._connection_task
|
|
877
|
+
self._connection_task = None
|
|
878
|
+
|
|
528
879
|
# Clean up tool bridge first
|
|
529
|
-
|
|
880
|
+
if self._tool_bridge and self._owns_bridge:
|
|
881
|
+
await self._tool_bridge.stop()
|
|
882
|
+
self._tool_bridge = None
|
|
883
|
+
self._owns_bridge = False
|
|
884
|
+
self._mcp_servers.clear()
|
|
530
885
|
if self._client:
|
|
531
886
|
try:
|
|
532
887
|
await self._client.disconnect()
|
|
@@ -536,62 +891,129 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
536
891
|
self._client = None
|
|
537
892
|
await super().__aexit__(exc_type, exc_val, exc_tb)
|
|
538
893
|
|
|
539
|
-
async def
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
894
|
+
async def populate_commands(self) -> None:
|
|
895
|
+
"""Populate the command store with slash commands from Claude Code.
|
|
896
|
+
|
|
897
|
+
Fetches available commands from the connected Claude Code server
|
|
898
|
+
and registers them as slashed Commands. Should be called after
|
|
899
|
+
connection is established.
|
|
900
|
+
|
|
901
|
+
Commands that are not supported or not useful for external use
|
|
902
|
+
are filtered out (e.g., login, logout, context, cost).
|
|
903
|
+
"""
|
|
904
|
+
server_info = await self.get_server_info()
|
|
905
|
+
if not server_info:
|
|
906
|
+
self.log.warning("No server info available for command population")
|
|
907
|
+
return
|
|
908
|
+
if not server_info.commands:
|
|
909
|
+
self.log.debug("No commands available from Claude Code server")
|
|
910
|
+
return
|
|
911
|
+
# Commands to skip - not useful or problematic in this context
|
|
912
|
+
unsupported = {"login", "logout", "release-notes", "todos"}
|
|
913
|
+
for cmd_info in server_info.commands:
|
|
914
|
+
name = cmd_info.name
|
|
915
|
+
if not name or name in unsupported:
|
|
916
|
+
continue
|
|
917
|
+
|
|
918
|
+
command = self._create_claude_code_command(cmd_info)
|
|
919
|
+
self._command_store.register_command(command)
|
|
920
|
+
command_count = len(self._command_store.list_commands())
|
|
921
|
+
self.log.info("Populated command store", command_count=command_count)
|
|
922
|
+
|
|
923
|
+
def _create_claude_code_command(self, cmd_info: ClaudeCodeCommandInfo) -> Command:
|
|
924
|
+
"""Create a slashed Command from Claude Code command info.
|
|
547
925
|
|
|
548
926
|
Args:
|
|
549
|
-
|
|
550
|
-
message_id: Optional message ID for the returned message
|
|
551
|
-
input_provider: Optional input provider for permission requests
|
|
552
|
-
message_history: Optional MessageHistory to use instead of agent's own
|
|
927
|
+
cmd_info: Command info dict with 'name', 'description', 'argumentHint'
|
|
553
928
|
|
|
554
929
|
Returns:
|
|
555
|
-
|
|
930
|
+
A slashed Command that executes via Claude Code
|
|
556
931
|
"""
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
932
|
+
from slashed import Command
|
|
933
|
+
|
|
934
|
+
name = cmd_info.name
|
|
935
|
+
# Handle MCP commands - they have " (MCP)" suffix in Claude Code
|
|
936
|
+
category = "claude_code"
|
|
937
|
+
if name.endswith(" (MCP)"):
|
|
938
|
+
name = f"mcp:{name.replace(' (MCP)', '')}"
|
|
939
|
+
category = "mcp"
|
|
940
|
+
|
|
941
|
+
async def execute_command(
|
|
942
|
+
ctx: CommandContext[Any],
|
|
943
|
+
args: list[str],
|
|
944
|
+
kwargs: dict[str, str],
|
|
945
|
+
) -> None:
|
|
946
|
+
"""Execute the Claude Code slash command."""
|
|
947
|
+
from claude_agent_sdk.types import (
|
|
948
|
+
AssistantMessage,
|
|
949
|
+
ResultMessage,
|
|
950
|
+
TextBlock,
|
|
951
|
+
UserMessage,
|
|
952
|
+
)
|
|
570
953
|
|
|
571
|
-
|
|
954
|
+
# Build command string
|
|
955
|
+
args_str = " ".join(args) if args else ""
|
|
956
|
+
if kwargs:
|
|
957
|
+
kwargs_str = " ".join(f"{k}={v}" for k, v in kwargs.items())
|
|
958
|
+
args_str = f"{args_str} {kwargs_str}".strip()
|
|
959
|
+
|
|
960
|
+
full_command = f"/{name} {args_str}".strip()
|
|
961
|
+
|
|
962
|
+
# Execute via agent run - slash commands go through as prompts
|
|
963
|
+
if self._client:
|
|
964
|
+
await self._client.query(full_command)
|
|
965
|
+
async for msg in self._client.receive_response():
|
|
966
|
+
if isinstance(msg, AssistantMessage):
|
|
967
|
+
for block in msg.content:
|
|
968
|
+
if isinstance(block, TextBlock):
|
|
969
|
+
await ctx.print(block.text)
|
|
970
|
+
elif isinstance(msg, UserMessage):
|
|
971
|
+
# Handle local command output wrapped in XML tags
|
|
972
|
+
content = msg.content if isinstance(msg.content, str) else ""
|
|
973
|
+
# Extract content from <local-command-stdout> or <local-command-stderr>
|
|
974
|
+
match = re.search(
|
|
975
|
+
r"<local-command-(?:stdout|stderr)>(.*?)</local-command-(?:stdout|stderr)>",
|
|
976
|
+
content,
|
|
977
|
+
re.DOTALL,
|
|
978
|
+
)
|
|
979
|
+
if match:
|
|
980
|
+
await ctx.print(match.group(1))
|
|
981
|
+
elif isinstance(msg, ResultMessage):
|
|
982
|
+
if msg.result:
|
|
983
|
+
await ctx.print(msg.result)
|
|
984
|
+
if msg.is_error:
|
|
985
|
+
await ctx.print(f"Error: {msg.subtype}")
|
|
986
|
+
|
|
987
|
+
return Command.from_raw(
|
|
988
|
+
execute_command,
|
|
989
|
+
name=name,
|
|
990
|
+
description=cmd_info.description or f"Claude Code command: {name}",
|
|
991
|
+
category=category,
|
|
992
|
+
usage=cmd_info.argument_hint,
|
|
993
|
+
)
|
|
572
994
|
|
|
573
|
-
async def
|
|
995
|
+
async def _stream_events( # noqa: PLR0915
|
|
574
996
|
self,
|
|
575
|
-
|
|
997
|
+
prompts: list[UserContent],
|
|
998
|
+
*,
|
|
999
|
+
user_msg: ChatMessage[Any],
|
|
1000
|
+
effective_parent_id: str | None,
|
|
576
1001
|
message_id: str | None = None,
|
|
1002
|
+
conversation_id: str | None = None,
|
|
1003
|
+
parent_id: str | None = None,
|
|
577
1004
|
input_provider: InputProvider | None = None,
|
|
578
1005
|
message_history: MessageHistory | None = None,
|
|
1006
|
+
deps: TDeps | None = None,
|
|
1007
|
+
event_handlers: Sequence[IndividualEventHandler | BuiltinEventHandlerType] | None = None,
|
|
1008
|
+
wait_for_connections: bool | None = None,
|
|
1009
|
+
store_history: bool = True,
|
|
579
1010
|
) -> AsyncIterator[RichAgentStreamEvent[TResult]]:
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
Args:
|
|
583
|
-
prompts: Prompts to send
|
|
584
|
-
message_id: Optional message ID for the final message
|
|
585
|
-
input_provider: Optional input provider for permission requests
|
|
586
|
-
message_history: Optional MessageHistory to use instead of agent's own
|
|
587
|
-
|
|
588
|
-
Yields:
|
|
589
|
-
RichAgentStreamEvent instances during execution
|
|
590
|
-
"""
|
|
1011
|
+
from anyenv import MultiEventHandler
|
|
591
1012
|
from claude_agent_sdk import (
|
|
592
1013
|
AssistantMessage,
|
|
593
1014
|
Message,
|
|
594
1015
|
ResultMessage,
|
|
1016
|
+
SystemMessage,
|
|
595
1017
|
TextBlock,
|
|
596
1018
|
ThinkingBlock,
|
|
597
1019
|
ToolResultBlock,
|
|
@@ -600,58 +1022,122 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
600
1022
|
)
|
|
601
1023
|
from claude_agent_sdk.types import StreamEvent
|
|
602
1024
|
|
|
1025
|
+
from agentpool.agents.events import resolve_event_handlers
|
|
1026
|
+
from agentpool.agents.events.infer_info import derive_rich_tool_info
|
|
1027
|
+
from agentpool.agents.tool_call_accumulator import ToolCallAccumulator
|
|
1028
|
+
|
|
1029
|
+
# Ensure client is connected (waits for deferred init if needed)
|
|
1030
|
+
await self.ensure_initialized()
|
|
603
1031
|
# Reset cancellation state
|
|
604
1032
|
self._cancelled = False
|
|
605
|
-
|
|
1033
|
+
# Initialize conversation_id on first run and log to storage
|
|
1034
|
+
# Use passed conversation_id if provided (e.g., from chained agents)
|
|
1035
|
+
# TODO: decide whether we should store CC sessions ourselves
|
|
1036
|
+
# For Claude Code, session_id comes from the SDK's init message:
|
|
1037
|
+
# if hasattr(message, 'subtype') and message.subtype == 'init':
|
|
1038
|
+
# session_id = message.data.get('session_id')
|
|
1039
|
+
# The SDK manages its own session persistence. To resume, pass:
|
|
1040
|
+
# ClaudeAgentOptions(resume=session_id)
|
|
1041
|
+
# Conversation ID initialization handled by BaseAgent
|
|
606
1042
|
|
|
607
1043
|
# Update input provider if provided
|
|
608
1044
|
if input_provider is not None:
|
|
609
1045
|
self._input_provider = input_provider
|
|
610
|
-
|
|
611
1046
|
if not self._client:
|
|
612
|
-
|
|
613
|
-
raise RuntimeError(msg)
|
|
1047
|
+
raise RuntimeError("Agent not initialized - use async context manager")
|
|
614
1048
|
|
|
615
1049
|
conversation = message_history if message_history is not None else self.conversation
|
|
616
|
-
#
|
|
617
|
-
|
|
1050
|
+
# Use provided event handlers or fall back to agent's handlers
|
|
1051
|
+
if event_handlers is not None:
|
|
1052
|
+
handler: MultiEventHandler[IndividualEventHandler] = MultiEventHandler(
|
|
1053
|
+
resolve_event_handlers(event_handlers)
|
|
1054
|
+
)
|
|
1055
|
+
else:
|
|
1056
|
+
handler = self.event_handler
|
|
618
1057
|
# Get pending parts from conversation (staged content)
|
|
619
1058
|
pending_parts = conversation.get_pending_parts()
|
|
620
1059
|
# Combine pending parts with new prompts, then join into single string for Claude SDK
|
|
621
|
-
all_parts = [*pending_parts, *
|
|
1060
|
+
all_parts = [*pending_parts, *prompts]
|
|
622
1061
|
prompt_text = " ".join(str(p) for p in all_parts)
|
|
1062
|
+
|
|
1063
|
+
# Inject thinking instruction if enabled
|
|
1064
|
+
if self._thinking_mode == "on":
|
|
1065
|
+
thinking_instruction = THINKING_MODE_PROMPTS[self._thinking_mode]
|
|
1066
|
+
prompt_text = f"{prompt_text}\n\n{thinking_instruction}"
|
|
623
1067
|
run_id = str(uuid.uuid4())
|
|
624
1068
|
# Emit run started
|
|
1069
|
+
assert self.conversation_id is not None # Initialized by BaseAgent.run_stream()
|
|
625
1070
|
run_started = RunStartedEvent(
|
|
626
1071
|
thread_id=self.conversation_id,
|
|
627
1072
|
run_id=run_id,
|
|
628
1073
|
agent_name=self.name,
|
|
629
1074
|
)
|
|
630
|
-
|
|
631
|
-
await handler(None, run_started)
|
|
1075
|
+
await handler(None, run_started)
|
|
632
1076
|
yield run_started
|
|
633
1077
|
request = ModelRequest(parts=[UserPromptPart(content=prompt_text)])
|
|
634
1078
|
model_messages: list[ModelResponse | ModelRequest] = [request]
|
|
635
1079
|
current_response_parts: list[TextPart | ThinkingPart | ToolCallPart] = []
|
|
636
1080
|
text_chunks: list[str] = []
|
|
637
1081
|
pending_tool_calls: dict[str, ToolUseBlock] = {}
|
|
1082
|
+
# Track tool calls that already had ToolCallStartEvent emitted (via StreamEvent)
|
|
1083
|
+
emitted_tool_starts: set[str] = set()
|
|
1084
|
+
tool_accumulator = ToolCallAccumulator()
|
|
1085
|
+
# Track files modified during this run
|
|
1086
|
+
file_tracker = FileTracker()
|
|
1087
|
+
# Accumulate metadata events by tool_call_id (workaround for SDK stripping _meta)
|
|
1088
|
+
tool_metadata: dict[str, dict[str, Any]] = {}
|
|
1089
|
+
# Set deps on tool bridge for access during tool invocations
|
|
1090
|
+
# (ContextVar doesn't work because MCP server runs in a separate task)
|
|
1091
|
+
if self._tool_bridge:
|
|
1092
|
+
self._tool_bridge.current_deps = deps
|
|
1093
|
+
|
|
1094
|
+
# Handle ephemeral execution (fork session if store_history=False)
|
|
1095
|
+
fork_client = None
|
|
1096
|
+
active_client = self._client
|
|
1097
|
+
|
|
1098
|
+
if not store_history and self._sdk_session_id:
|
|
1099
|
+
# Create fork client that shares parent's context but has separate session ID
|
|
1100
|
+
# See: src/agentpool/agents/claude_code_agent/FORKING.md
|
|
1101
|
+
from claude_agent_sdk import ClaudeSDKClient
|
|
1102
|
+
|
|
1103
|
+
# Build options using same method as main client
|
|
1104
|
+
fork_options = self._build_options()
|
|
1105
|
+
# Add fork-specific parameters
|
|
1106
|
+
fork_options.resume = self._sdk_session_id # Fork from current session
|
|
1107
|
+
fork_options.fork_session = True # Create new session ID
|
|
1108
|
+
|
|
1109
|
+
fork_client = ClaudeSDKClient(options=fork_options)
|
|
1110
|
+
await fork_client.connect()
|
|
1111
|
+
active_client = fork_client
|
|
638
1112
|
|
|
639
1113
|
try:
|
|
640
|
-
await
|
|
1114
|
+
await active_client.query(prompt_text)
|
|
641
1115
|
# Merge SDK messages with event queue for real-time tool event streaming
|
|
642
1116
|
async with merge_queue_into_iterator(
|
|
643
|
-
|
|
1117
|
+
active_client.receive_response(), self._event_queue
|
|
644
1118
|
) as merged_events:
|
|
645
1119
|
async for event_or_message in merged_events:
|
|
646
1120
|
# Check if it's a queued event (from tools via EventEmitter)
|
|
647
1121
|
if not isinstance(event_or_message, Message):
|
|
1122
|
+
# Capture metadata events for correlation with tool results
|
|
1123
|
+
from agentpool.agents.events import ToolResultMetadataEvent
|
|
1124
|
+
|
|
1125
|
+
if isinstance(event_or_message, ToolResultMetadataEvent):
|
|
1126
|
+
tool_metadata[event_or_message.tool_call_id] = event_or_message.metadata
|
|
1127
|
+
# Don't yield metadata events - they're internal correlation only
|
|
1128
|
+
continue
|
|
648
1129
|
# It's an event from the queue - yield it immediately
|
|
649
|
-
|
|
650
|
-
await handler(None, event_or_message)
|
|
1130
|
+
await handler(None, event_or_message)
|
|
651
1131
|
yield event_or_message
|
|
652
1132
|
continue
|
|
653
1133
|
|
|
654
1134
|
message = event_or_message
|
|
1135
|
+
# Capture SDK session ID from init message
|
|
1136
|
+
if isinstance(message, SystemMessage):
|
|
1137
|
+
if message.subtype == "init" and "session_id" in message.data:
|
|
1138
|
+
self._sdk_session_id = message.data["session_id"]
|
|
1139
|
+
continue
|
|
1140
|
+
|
|
655
1141
|
# Process assistant messages - extract parts incrementally
|
|
656
1142
|
if isinstance(message, AssistantMessage):
|
|
657
1143
|
# Update model name from first assistant message
|
|
@@ -666,29 +1152,44 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
666
1152
|
current_response_parts.append(ThinkingPart(content=thinking))
|
|
667
1153
|
case ToolUseBlockType(id=tc_id, name=name, input=input_data):
|
|
668
1154
|
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,
|
|
1155
|
+
display_name = _strip_mcp_prefix(name)
|
|
1156
|
+
tool_call_part = ToolCallPart(
|
|
1157
|
+
tool_name=display_name, args=input_data, tool_call_id=tc_id
|
|
677
1158
|
)
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
1159
|
+
current_response_parts.append(tool_call_part)
|
|
1160
|
+
|
|
1161
|
+
# Emit FunctionToolCallEvent (triggers UI notification)
|
|
1162
|
+
# func_tool_event = FunctionToolCallEvent(part=tool_call_part)
|
|
1163
|
+
# await handler(None, func_tool_event)
|
|
1164
|
+
# yield func_tool_event
|
|
1165
|
+
|
|
1166
|
+
# Only emit ToolCallStartEvent if not already emitted
|
|
1167
|
+
# via streaming (emits early with partial info)
|
|
1168
|
+
if tc_id not in emitted_tool_starts:
|
|
1169
|
+
rich_info = derive_rich_tool_info(name, input_data)
|
|
1170
|
+
tool_start_event = ToolCallStartEvent(
|
|
1171
|
+
tool_call_id=tc_id,
|
|
1172
|
+
tool_name=display_name,
|
|
1173
|
+
title=rich_info.title,
|
|
1174
|
+
kind=rich_info.kind,
|
|
1175
|
+
locations=rich_info.locations,
|
|
1176
|
+
content=rich_info.content,
|
|
1177
|
+
raw_input=input_data,
|
|
1178
|
+
)
|
|
1179
|
+
# Track file modifications
|
|
1180
|
+
file_tracker.process_event(tool_start_event)
|
|
690
1181
|
await handler(None, tool_start_event)
|
|
691
|
-
|
|
1182
|
+
yield tool_start_event
|
|
1183
|
+
# Already emitted ToolCallStartEvent early via streaming.
|
|
1184
|
+
# Dont emit a progress update here - it races with
|
|
1185
|
+
# permission requests and causes Zed to cancel the dialog.
|
|
1186
|
+
# Just track file modifications.
|
|
1187
|
+
elif file_path := file_tracker.extractor(
|
|
1188
|
+
display_name, input_data
|
|
1189
|
+
):
|
|
1190
|
+
file_tracker.touched_files.add(file_path)
|
|
1191
|
+
# Clean up from accumulator (always, both branches)
|
|
1192
|
+
tool_accumulator.complete(tc_id)
|
|
692
1193
|
case ToolResultBlock(tool_use_id=tc_id, content=content):
|
|
693
1194
|
# Tool result received - flush response parts and add request
|
|
694
1195
|
if current_response_parts:
|
|
@@ -698,8 +1199,24 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
698
1199
|
|
|
699
1200
|
# Get tool name from pending calls
|
|
700
1201
|
tool_use = pending_tool_calls.pop(tc_id, None)
|
|
701
|
-
tool_name =
|
|
1202
|
+
tool_name = _strip_mcp_prefix(
|
|
1203
|
+
tool_use.name if tool_use else "unknown"
|
|
1204
|
+
)
|
|
702
1205
|
tool_input = tool_use.input if tool_use else {}
|
|
1206
|
+
|
|
1207
|
+
# Create ToolReturnPart for the result
|
|
1208
|
+
tool_return_part = ToolReturnPart(
|
|
1209
|
+
tool_name=tool_name, content=content, tool_call_id=tc_id
|
|
1210
|
+
)
|
|
1211
|
+
|
|
1212
|
+
# Emit FunctionToolResultEvent (for session.py to complete UI)
|
|
1213
|
+
func_result_event = FunctionToolResultEvent(
|
|
1214
|
+
result=tool_return_part
|
|
1215
|
+
)
|
|
1216
|
+
await handler(None, func_result_event)
|
|
1217
|
+
yield func_result_event
|
|
1218
|
+
|
|
1219
|
+
# Also emit ToolCallCompleteEvent for consumers that expect it
|
|
703
1220
|
tool_done_event = ToolCallCompleteEvent(
|
|
704
1221
|
tool_name=tool_name,
|
|
705
1222
|
tool_call_id=tc_id,
|
|
@@ -707,16 +1224,13 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
707
1224
|
tool_result=content,
|
|
708
1225
|
agent_name=self.name,
|
|
709
1226
|
message_id="",
|
|
1227
|
+
metadata=tool_metadata.get(tc_id),
|
|
710
1228
|
)
|
|
711
|
-
|
|
712
|
-
await handler(None, tool_done_event)
|
|
1229
|
+
await handler(None, tool_done_event)
|
|
713
1230
|
yield tool_done_event
|
|
714
1231
|
|
|
715
1232
|
# 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]))
|
|
1233
|
+
model_messages.append(ModelRequest(parts=[tool_return_part]))
|
|
720
1234
|
|
|
721
1235
|
# Process user messages - may contain tool results
|
|
722
1236
|
elif isinstance(message, UserMessage):
|
|
@@ -738,9 +1252,24 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
738
1252
|
|
|
739
1253
|
# Get tool name from pending calls
|
|
740
1254
|
tool_use = pending_tool_calls.pop(tc_id, None)
|
|
741
|
-
tool_name =
|
|
1255
|
+
tool_name = _strip_mcp_prefix(
|
|
1256
|
+
tool_use.name if tool_use else "unknown"
|
|
1257
|
+
)
|
|
742
1258
|
tool_input = tool_use.input if tool_use else {}
|
|
743
|
-
|
|
1259
|
+
|
|
1260
|
+
# Create ToolReturnPart for the result
|
|
1261
|
+
tool_return_part = ToolReturnPart(
|
|
1262
|
+
tool_name=tool_name,
|
|
1263
|
+
content=result_content,
|
|
1264
|
+
tool_call_id=tc_id,
|
|
1265
|
+
)
|
|
1266
|
+
|
|
1267
|
+
# Emit FunctionToolResultEvent (for session.py to complete UI)
|
|
1268
|
+
func_result_event = FunctionToolResultEvent(result=tool_return_part)
|
|
1269
|
+
await handler(None, func_result_event)
|
|
1270
|
+
yield func_result_event
|
|
1271
|
+
|
|
1272
|
+
# Also emit ToolCallCompleteEvent for consumers that expect it
|
|
744
1273
|
tool_complete_event = ToolCallCompleteEvent(
|
|
745
1274
|
tool_name=tool_name,
|
|
746
1275
|
tool_call_id=tc_id,
|
|
@@ -748,17 +1277,13 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
748
1277
|
tool_result=result_content,
|
|
749
1278
|
agent_name=self.name,
|
|
750
1279
|
message_id="",
|
|
1280
|
+
metadata=tool_metadata.get(tc_id),
|
|
751
1281
|
)
|
|
752
|
-
|
|
753
|
-
await handler(None, tool_complete_event)
|
|
1282
|
+
await handler(None, tool_complete_event)
|
|
754
1283
|
yield tool_complete_event
|
|
1284
|
+
|
|
755
1285
|
# 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]))
|
|
1286
|
+
model_messages.append(ModelRequest(parts=[tool_return_part]))
|
|
762
1287
|
|
|
763
1288
|
# Handle StreamEvent for real-time streaming
|
|
764
1289
|
elif isinstance(message, StreamEvent):
|
|
@@ -772,21 +1297,38 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
772
1297
|
block_type = content_block.get("type")
|
|
773
1298
|
|
|
774
1299
|
if block_type == "text":
|
|
775
|
-
start_event = PartStartEvent(index=index,
|
|
776
|
-
|
|
777
|
-
await handler(None, start_event)
|
|
1300
|
+
start_event = PartStartEvent.text(index=index, content="")
|
|
1301
|
+
await handler(None, start_event)
|
|
778
1302
|
yield start_event
|
|
779
1303
|
|
|
780
1304
|
elif block_type == "thinking":
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
for handler in self.event_handler._wrapped_handlers:
|
|
784
|
-
await handler(None, start_event)
|
|
1305
|
+
start_event = PartStartEvent.thinking(index=index, content="")
|
|
1306
|
+
await handler(None, start_event)
|
|
785
1307
|
yield start_event
|
|
786
1308
|
|
|
787
1309
|
elif block_type == "tool_use":
|
|
788
|
-
#
|
|
789
|
-
|
|
1310
|
+
# Emit ToolCallStartEvent early (args still streaming)
|
|
1311
|
+
tc_id = content_block.get("id", "")
|
|
1312
|
+
raw_tool_name = content_block.get("name", "")
|
|
1313
|
+
tool_name = _strip_mcp_prefix(raw_tool_name)
|
|
1314
|
+
tool_accumulator.start(tc_id, tool_name)
|
|
1315
|
+
# Track for permission matching - permission callback will use this
|
|
1316
|
+
# Use raw name since SDK uses raw names for permissions
|
|
1317
|
+
self._pending_tool_call_ids[raw_tool_name] = tc_id
|
|
1318
|
+
# Derive rich info with empty args for now
|
|
1319
|
+
rich_info = derive_rich_tool_info(raw_tool_name, {})
|
|
1320
|
+
tool_start_event = ToolCallStartEvent(
|
|
1321
|
+
tool_call_id=tc_id,
|
|
1322
|
+
tool_name=tool_name,
|
|
1323
|
+
title=rich_info.title,
|
|
1324
|
+
kind=rich_info.kind,
|
|
1325
|
+
locations=[], # No locations yet, args not complete
|
|
1326
|
+
content=rich_info.content,
|
|
1327
|
+
raw_input={}, # Empty, will be filled when complete
|
|
1328
|
+
)
|
|
1329
|
+
emitted_tool_starts.add(tc_id)
|
|
1330
|
+
await handler(None, tool_start_event)
|
|
1331
|
+
yield tool_start_event
|
|
790
1332
|
|
|
791
1333
|
# Handle content_block_delta events (text streaming)
|
|
792
1334
|
elif event_type == "content_block_delta":
|
|
@@ -798,26 +1340,46 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
798
1340
|
if text_delta:
|
|
799
1341
|
text_part = TextPartDelta(content_delta=text_delta)
|
|
800
1342
|
delta_event = PartDeltaEvent(index=index, delta=text_part)
|
|
801
|
-
|
|
802
|
-
await handler(None, delta_event)
|
|
1343
|
+
await handler(None, delta_event)
|
|
803
1344
|
yield delta_event
|
|
804
1345
|
|
|
805
1346
|
elif delta_type == "thinking_delta":
|
|
806
1347
|
thinking_delta = delta.get("thinking", "")
|
|
807
1348
|
if thinking_delta:
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
1349
|
+
thinking_part_delta = ThinkingPartDelta(
|
|
1350
|
+
content_delta=thinking_delta
|
|
1351
|
+
)
|
|
1352
|
+
delta_event = PartDeltaEvent(
|
|
1353
|
+
index=index, delta=thinking_part_delta
|
|
1354
|
+
)
|
|
1355
|
+
await handler(None, delta_event)
|
|
812
1356
|
yield delta_event
|
|
813
1357
|
|
|
1358
|
+
elif delta_type == "input_json_delta":
|
|
1359
|
+
# Accumulate tool argument JSON fragments
|
|
1360
|
+
partial_json = delta.get("partial_json", "")
|
|
1361
|
+
if partial_json:
|
|
1362
|
+
# Find which tool call this belongs to by index
|
|
1363
|
+
# The index corresponds to the content block index
|
|
1364
|
+
for tc_id in tool_accumulator._calls:
|
|
1365
|
+
tool_accumulator.add_args(tc_id, partial_json)
|
|
1366
|
+
# Emit PartDeltaEvent with ToolCallPartDelta
|
|
1367
|
+
tool_delta = ToolCallPartDelta(
|
|
1368
|
+
tool_name_delta=None,
|
|
1369
|
+
args_delta=partial_json,
|
|
1370
|
+
tool_call_id=tc_id,
|
|
1371
|
+
)
|
|
1372
|
+
delta_event = PartDeltaEvent(index=index, delta=tool_delta)
|
|
1373
|
+
await handler(None, delta_event)
|
|
1374
|
+
yield delta_event
|
|
1375
|
+
break # Only one tool call streams at a time
|
|
1376
|
+
|
|
814
1377
|
# Handle content_block_stop events
|
|
815
1378
|
elif event_type == "content_block_stop":
|
|
816
1379
|
# We don't have the full part content here, emit with empty part
|
|
817
1380
|
# The actual content was accumulated via deltas
|
|
818
1381
|
end_event = PartEndEvent(index=index, part=TextPart(content=""))
|
|
819
|
-
|
|
820
|
-
await handler(None, end_event)
|
|
1382
|
+
await handler(None, end_event)
|
|
821
1383
|
yield end_event
|
|
822
1384
|
|
|
823
1385
|
# Skip further processing for StreamEvent - don't duplicate
|
|
@@ -832,8 +1394,7 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
832
1394
|
pending_tool_calls={}, # Already handled above
|
|
833
1395
|
)
|
|
834
1396
|
for event in events:
|
|
835
|
-
|
|
836
|
-
await handler(None, event)
|
|
1397
|
+
await handler(None, event)
|
|
837
1398
|
yield event
|
|
838
1399
|
|
|
839
1400
|
# Check for result (end of response) and capture usage info
|
|
@@ -841,54 +1402,60 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
841
1402
|
result_message = message
|
|
842
1403
|
break
|
|
843
1404
|
|
|
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
|
|
1405
|
+
# Note: We do NOT return early on cancellation here.
|
|
1406
|
+
# The SDK docs warn against using break/return to exit receive_response()
|
|
1407
|
+
# early as it can cause asyncio cleanup issues. Instead, we let the
|
|
1408
|
+
# interrupt() call cause the SDK to send a ResultMessage that will
|
|
1409
|
+
# naturally terminate the stream via the isinstance(message, ResultMessage)
|
|
1410
|
+
# check above. The _cancelled flag is checked in process_prompt() to
|
|
1411
|
+
# return the correct stop reason.
|
|
863
1412
|
else:
|
|
864
1413
|
result_message = None
|
|
865
1414
|
|
|
866
1415
|
except asyncio.CancelledError:
|
|
867
1416
|
self.log.info("Stream cancelled via CancelledError")
|
|
868
1417
|
# Emit partial response on cancellation
|
|
1418
|
+
# Build metadata with file tracking and SDK session ID
|
|
1419
|
+
metadata = file_tracker.get_metadata()
|
|
1420
|
+
if self._sdk_session_id:
|
|
1421
|
+
metadata["sdk_session_id"] = self._sdk_session_id
|
|
1422
|
+
|
|
869
1423
|
response_msg = ChatMessage[TResult](
|
|
870
1424
|
content="".join(text_chunks), # type: ignore[arg-type]
|
|
871
1425
|
role="assistant",
|
|
872
1426
|
name=self.name,
|
|
873
1427
|
message_id=message_id or str(uuid.uuid4()),
|
|
874
1428
|
conversation_id=self.conversation_id,
|
|
1429
|
+
parent_id=user_msg.message_id,
|
|
875
1430
|
model_name=self.model_name,
|
|
876
1431
|
messages=model_messages,
|
|
877
1432
|
finish_reason="stop",
|
|
1433
|
+
metadata=metadata,
|
|
878
1434
|
)
|
|
879
1435
|
complete_event = StreamCompleteEvent(message=response_msg)
|
|
880
|
-
|
|
881
|
-
await handler(None, complete_event)
|
|
1436
|
+
await handler(None, complete_event)
|
|
882
1437
|
yield complete_event
|
|
1438
|
+
# Post-processing handled by base class
|
|
883
1439
|
return
|
|
884
1440
|
|
|
885
1441
|
except Exception as e:
|
|
886
1442
|
error_event = RunErrorEvent(message=str(e), run_id=run_id, agent_name=self.name)
|
|
887
|
-
|
|
888
|
-
await handler(None, error_event)
|
|
1443
|
+
await handler(None, error_event)
|
|
889
1444
|
yield error_event
|
|
890
1445
|
raise
|
|
891
1446
|
|
|
1447
|
+
finally:
|
|
1448
|
+
# Disconnect fork client if we created one
|
|
1449
|
+
if fork_client:
|
|
1450
|
+
try:
|
|
1451
|
+
await fork_client.disconnect()
|
|
1452
|
+
except Exception as e: # noqa: BLE001
|
|
1453
|
+
get_logger(__name__).warning(f"Error disconnecting fork client: {e}")
|
|
1454
|
+
|
|
1455
|
+
# Clear deps from tool bridge
|
|
1456
|
+
if self._tool_bridge:
|
|
1457
|
+
self._tool_bridge.current_deps = None
|
|
1458
|
+
|
|
892
1459
|
# Flush any remaining response parts
|
|
893
1460
|
if current_response_parts:
|
|
894
1461
|
model_messages.append(ModelResponse(parts=current_response_parts))
|
|
@@ -900,18 +1467,32 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
900
1467
|
else "".join(text_chunks)
|
|
901
1468
|
)
|
|
902
1469
|
|
|
903
|
-
# Build cost_info from ResultMessage if available
|
|
1470
|
+
# Build cost_info and usage from ResultMessage if available
|
|
904
1471
|
cost_info: TokenCost | None = None
|
|
1472
|
+
request_usage: RequestUsage | None = None
|
|
905
1473
|
if result_message and result_message.usage:
|
|
906
|
-
|
|
1474
|
+
usage_dict = result_message.usage
|
|
907
1475
|
run_usage = RunUsage(
|
|
908
|
-
input_tokens=
|
|
909
|
-
output_tokens=
|
|
910
|
-
cache_read_tokens=
|
|
911
|
-
cache_write_tokens=
|
|
1476
|
+
input_tokens=usage_dict.get("input_tokens", 0),
|
|
1477
|
+
output_tokens=usage_dict.get("output_tokens", 0),
|
|
1478
|
+
cache_read_tokens=usage_dict.get("cache_read_input_tokens", 0),
|
|
1479
|
+
cache_write_tokens=usage_dict.get("cache_creation_input_tokens", 0),
|
|
912
1480
|
)
|
|
913
1481
|
total_cost = Decimal(str(result_message.total_cost_usd or 0))
|
|
914
1482
|
cost_info = TokenCost(token_usage=run_usage, total_cost=total_cost)
|
|
1483
|
+
# Also set usage for OpenCode compatibility
|
|
1484
|
+
request_usage = RequestUsage(
|
|
1485
|
+
input_tokens=usage_dict.get("input_tokens", 0),
|
|
1486
|
+
output_tokens=usage_dict.get("output_tokens", 0),
|
|
1487
|
+
cache_read_tokens=usage_dict.get("cache_read_input_tokens", 0),
|
|
1488
|
+
cache_write_tokens=usage_dict.get("cache_creation_input_tokens", 0),
|
|
1489
|
+
)
|
|
1490
|
+
|
|
1491
|
+
# Determine finish reason - check if we were cancelled
|
|
1492
|
+
# Build metadata with file tracking and SDK session ID
|
|
1493
|
+
metadata = file_tracker.get_metadata()
|
|
1494
|
+
if self._sdk_session_id:
|
|
1495
|
+
metadata["sdk_session_id"] = self._sdk_session_id
|
|
915
1496
|
|
|
916
1497
|
chat_message = ChatMessage[TResult](
|
|
917
1498
|
content=final_content,
|
|
@@ -919,46 +1500,32 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
919
1500
|
name=self.name,
|
|
920
1501
|
message_id=message_id or str(uuid.uuid4()),
|
|
921
1502
|
conversation_id=self.conversation_id,
|
|
1503
|
+
parent_id=user_msg.message_id,
|
|
922
1504
|
model_name=self.model_name,
|
|
923
1505
|
messages=model_messages,
|
|
924
1506
|
cost_info=cost_info,
|
|
1507
|
+
usage=request_usage or RequestUsage(),
|
|
925
1508
|
response_time=result_message.duration_ms / 1000 if result_message else None,
|
|
1509
|
+
finish_reason="stop" if self._cancelled else None,
|
|
1510
|
+
metadata=metadata,
|
|
926
1511
|
)
|
|
927
1512
|
|
|
928
|
-
# Emit stream complete
|
|
1513
|
+
# Emit stream complete - post-processing handled by base class
|
|
929
1514
|
complete_event = StreamCompleteEvent[TResult](message=chat_message)
|
|
930
|
-
|
|
931
|
-
await handler(None, complete_event)
|
|
1515
|
+
await handler(None, complete_event)
|
|
932
1516
|
yield complete_event
|
|
933
|
-
# Record to history
|
|
934
|
-
self.message_sent.emit(chat_message)
|
|
935
|
-
conversation.add_chat_messages([user_msg, chat_message])
|
|
936
|
-
|
|
937
|
-
async def run_iter(
|
|
938
|
-
self,
|
|
939
|
-
*prompt_groups: Sequence[PromptCompatible],
|
|
940
|
-
) -> AsyncIterator[ChatMessage[TResult]]:
|
|
941
|
-
"""Run agent sequentially on multiple prompt groups.
|
|
942
|
-
|
|
943
|
-
Args:
|
|
944
|
-
prompt_groups: Groups of prompts to process sequentially
|
|
945
|
-
|
|
946
|
-
Yields:
|
|
947
|
-
Response messages in sequence
|
|
948
|
-
"""
|
|
949
|
-
for prompts in prompt_groups:
|
|
950
|
-
response = await self.run(*prompts)
|
|
951
|
-
yield response
|
|
952
1517
|
|
|
953
1518
|
async def interrupt(self) -> None:
|
|
954
1519
|
"""Interrupt the currently running stream.
|
|
955
1520
|
|
|
956
|
-
|
|
957
|
-
|
|
1521
|
+
Sets the cancelled flag and calls the Claude SDK's native interrupt()
|
|
1522
|
+
method to stop the query. The stream loop checks the flag and returns
|
|
1523
|
+
gracefully - we don't cancel the task ourselves to avoid CancelledError
|
|
1524
|
+
propagation issues.
|
|
958
1525
|
"""
|
|
959
1526
|
self._cancelled = True
|
|
960
1527
|
|
|
961
|
-
# Use Claude SDK's native interrupt
|
|
1528
|
+
# Use Claude SDK's native interrupt - this causes the SDK to stop yielding
|
|
962
1529
|
if self._client:
|
|
963
1530
|
try:
|
|
964
1531
|
await self._client.interrupt()
|
|
@@ -966,25 +1533,23 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
966
1533
|
except Exception:
|
|
967
1534
|
self.log.exception("Failed to interrupt Claude Code client")
|
|
968
1535
|
|
|
969
|
-
|
|
970
|
-
if self._current_stream_task and not self._current_stream_task.done():
|
|
971
|
-
self._current_stream_task.cancel()
|
|
972
|
-
|
|
973
|
-
async def set_model(self, model: str) -> None:
|
|
1536
|
+
async def set_model(self, model: AnthropicMaxModelName | str) -> None:
|
|
974
1537
|
"""Set the model for future requests.
|
|
975
1538
|
|
|
976
|
-
Note: This updates the model for the next query. The client
|
|
977
|
-
maintains the connection, so this takes effect on the next query().
|
|
978
|
-
|
|
979
1539
|
Args:
|
|
980
1540
|
model: Model name to use
|
|
981
1541
|
"""
|
|
982
1542
|
self._model = model
|
|
983
1543
|
self._current_model = model
|
|
984
1544
|
|
|
1545
|
+
# Ensure client is connected before setting model
|
|
985
1546
|
if self._client:
|
|
1547
|
+
await self.ensure_initialized()
|
|
986
1548
|
await self._client.set_model(model)
|
|
987
1549
|
self.log.info("Model changed", model=model)
|
|
1550
|
+
else:
|
|
1551
|
+
# Client not created yet, model will be used during _build_options()
|
|
1552
|
+
self.log.info("Model set for initialization", model=model)
|
|
988
1553
|
|
|
989
1554
|
async def set_tool_confirmation_mode(self, mode: ToolConfirmationMode) -> None:
|
|
990
1555
|
"""Set tool confirmation mode.
|
|
@@ -993,17 +1558,207 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
993
1558
|
mode: Confirmation mode - "always", "never", or "per_tool"
|
|
994
1559
|
"""
|
|
995
1560
|
self.tool_confirmation_mode = mode
|
|
1561
|
+
# Map confirmation mode to permission mode
|
|
1562
|
+
if mode == "never":
|
|
1563
|
+
self._permission_mode = "bypassPermissions"
|
|
1564
|
+
elif mode in {"always", "per_tool"}:
|
|
1565
|
+
self._permission_mode = "default"
|
|
996
1566
|
# Update permission mode on client if connected
|
|
997
|
-
if self._client and
|
|
998
|
-
await self._client.set_permission_mode(
|
|
999
|
-
|
|
1000
|
-
|
|
1567
|
+
if self._client and self._permission_mode:
|
|
1568
|
+
await self._client.set_permission_mode(self._permission_mode)
|
|
1569
|
+
|
|
1570
|
+
async def get_available_models(self) -> list[ModelInfo] | None:
|
|
1571
|
+
"""Get available models for Claude Code agent.
|
|
1572
|
+
|
|
1573
|
+
Returns a static list of Claude models (opus, sonnet, haiku) since
|
|
1574
|
+
Claude Code SDK only supports these models with simple IDs.
|
|
1575
|
+
|
|
1576
|
+
Returns:
|
|
1577
|
+
List of tokonomics ModelInfo for Claude models
|
|
1578
|
+
"""
|
|
1579
|
+
from agentpool.agents.claude_code_agent.static_info import MODELS
|
|
1580
|
+
|
|
1581
|
+
return MODELS
|
|
1001
1582
|
|
|
1002
|
-
async def
|
|
1003
|
-
"""Get
|
|
1004
|
-
from agentpool.talk.stats import MessageStats
|
|
1583
|
+
async def get_server_info(self) -> ClaudeCodeServerInfo | None:
|
|
1584
|
+
"""Get server initialization info from Claude Code.
|
|
1005
1585
|
|
|
1006
|
-
|
|
1586
|
+
Returns information from the Claude Code server including:
|
|
1587
|
+
- Available models (opus, sonnet, haiku) with descriptions and pricing
|
|
1588
|
+
- Available slash commands with descriptions and argument hints
|
|
1589
|
+
- Current and available output styles
|
|
1590
|
+
- Account information (token source, API key source)
|
|
1591
|
+
"""
|
|
1592
|
+
from agentpool.agents.claude_code_agent.models import ClaudeCodeServerInfo
|
|
1593
|
+
|
|
1594
|
+
if not self._client:
|
|
1595
|
+
self.log.warning("Cannot get server info: not connected")
|
|
1596
|
+
return None
|
|
1597
|
+
# Get raw server info from SDK client
|
|
1598
|
+
raw_info = await self._client.get_server_info()
|
|
1599
|
+
if not raw_info:
|
|
1600
|
+
self.log.warning("No server info available from Claude Code")
|
|
1601
|
+
return None
|
|
1602
|
+
return ClaudeCodeServerInfo.model_validate(raw_info)
|
|
1603
|
+
|
|
1604
|
+
async def get_modes(self) -> list[ModeCategory]:
|
|
1605
|
+
"""Get available mode categories for Claude Code agent.
|
|
1606
|
+
|
|
1607
|
+
Claude Code exposes permission modes and model selection.
|
|
1608
|
+
|
|
1609
|
+
Returns:
|
|
1610
|
+
List of ModeCategory for permissions and models
|
|
1611
|
+
"""
|
|
1612
|
+
from agentpool.agents.claude_code_agent.static_info import MODES
|
|
1613
|
+
from agentpool.agents.modes import ModeCategory, ModeInfo
|
|
1614
|
+
|
|
1615
|
+
categories: list[ModeCategory] = []
|
|
1616
|
+
# Permission modes
|
|
1617
|
+
current_id = self._permission_mode or "default"
|
|
1618
|
+
if self.tool_confirmation_mode == "never":
|
|
1619
|
+
current_id = "bypassPermissions"
|
|
1620
|
+
|
|
1621
|
+
categories.append(
|
|
1622
|
+
ModeCategory(
|
|
1623
|
+
id="permissions",
|
|
1624
|
+
name="Mode",
|
|
1625
|
+
available_modes=MODES,
|
|
1626
|
+
current_mode_id=current_id,
|
|
1627
|
+
category="mode",
|
|
1628
|
+
)
|
|
1629
|
+
)
|
|
1630
|
+
|
|
1631
|
+
# Model selection
|
|
1632
|
+
models = await self.get_available_models()
|
|
1633
|
+
if models:
|
|
1634
|
+
current_model = self.model_name or (models[0].id if models else "")
|
|
1635
|
+
modes = [
|
|
1636
|
+
ModeInfo(
|
|
1637
|
+
id=m.id,
|
|
1638
|
+
name=m.name or m.id,
|
|
1639
|
+
description=m.description or "",
|
|
1640
|
+
category_id="model",
|
|
1641
|
+
)
|
|
1642
|
+
for m in models
|
|
1643
|
+
]
|
|
1644
|
+
categories.append(
|
|
1645
|
+
ModeCategory(
|
|
1646
|
+
id="model",
|
|
1647
|
+
name="Model",
|
|
1648
|
+
available_modes=modes,
|
|
1649
|
+
current_mode_id=current_model,
|
|
1650
|
+
category="model",
|
|
1651
|
+
)
|
|
1652
|
+
)
|
|
1653
|
+
|
|
1654
|
+
# Thinking level selection
|
|
1655
|
+
# Only expose if MAX_THINKING_TOKENS is not set (keyword only works without env var)
|
|
1656
|
+
if not self._max_thinking_tokens:
|
|
1657
|
+
thinking_modes = [
|
|
1658
|
+
ModeInfo(
|
|
1659
|
+
id="off",
|
|
1660
|
+
name="Thinking Off",
|
|
1661
|
+
description="No extended thinking",
|
|
1662
|
+
category_id="thinking_level",
|
|
1663
|
+
),
|
|
1664
|
+
ModeInfo(
|
|
1665
|
+
id="on",
|
|
1666
|
+
name="Thinking On",
|
|
1667
|
+
description="Extended thinking (~32k tokens)",
|
|
1668
|
+
category_id="thinking_level",
|
|
1669
|
+
),
|
|
1670
|
+
]
|
|
1671
|
+
categories.append(
|
|
1672
|
+
ModeCategory(
|
|
1673
|
+
id="thinking_level",
|
|
1674
|
+
name="Thinking Level",
|
|
1675
|
+
available_modes=thinking_modes,
|
|
1676
|
+
current_mode_id=self._thinking_mode,
|
|
1677
|
+
category="thought_level",
|
|
1678
|
+
)
|
|
1679
|
+
)
|
|
1680
|
+
|
|
1681
|
+
return categories
|
|
1682
|
+
|
|
1683
|
+
async def set_mode(self, mode: ModeInfo | str, category_id: str | None = None) -> None:
|
|
1684
|
+
"""Set a mode within a category.
|
|
1685
|
+
|
|
1686
|
+
For Claude Code, this handles:
|
|
1687
|
+
- "permissions" category: permission modes from the SDK
|
|
1688
|
+
- "model" category: model selection
|
|
1689
|
+
- "thinking_level" category: extended thinking budget allocation
|
|
1690
|
+
|
|
1691
|
+
Args:
|
|
1692
|
+
mode: The mode to set - ModeInfo object or mode ID string
|
|
1693
|
+
category_id: Category ID ("permissions", "model", or "thinking_level")
|
|
1694
|
+
|
|
1695
|
+
Raises:
|
|
1696
|
+
ValueError: If the category or mode is unknown
|
|
1697
|
+
"""
|
|
1698
|
+
from agentpool.agents.claude_code_agent.static_info import VALID_MODES
|
|
1699
|
+
|
|
1700
|
+
# Extract mode_id and category from ModeInfo if provided
|
|
1701
|
+
if isinstance(mode, ModeInfo):
|
|
1702
|
+
mode_id = mode.id
|
|
1703
|
+
category_id = category_id or mode.category_id
|
|
1704
|
+
else:
|
|
1705
|
+
mode_id = mode
|
|
1706
|
+
|
|
1707
|
+
# Default to permissions if no category specified
|
|
1708
|
+
if category_id is None:
|
|
1709
|
+
category_id = "permissions"
|
|
1710
|
+
|
|
1711
|
+
if category_id == "permissions":
|
|
1712
|
+
# Map mode_id to PermissionMode
|
|
1713
|
+
if mode_id not in VALID_MODES:
|
|
1714
|
+
msg = f"Unknown permission mode: {mode_id}. Available: {list(VALID_MODES)}"
|
|
1715
|
+
raise ValueError(msg)
|
|
1716
|
+
|
|
1717
|
+
permission_mode: PermissionMode = mode_id # type: ignore[assignment]
|
|
1718
|
+
self._permission_mode = permission_mode
|
|
1719
|
+
|
|
1720
|
+
# Update tool confirmation mode based on permission mode
|
|
1721
|
+
if mode_id == "bypassPermissions":
|
|
1722
|
+
self.tool_confirmation_mode = "never"
|
|
1723
|
+
elif mode_id in ("default", "plan"):
|
|
1724
|
+
self.tool_confirmation_mode = "always"
|
|
1725
|
+
|
|
1726
|
+
# Update SDK client if initialized
|
|
1727
|
+
if self._client:
|
|
1728
|
+
await self.ensure_initialized()
|
|
1729
|
+
await self._client.set_permission_mode(permission_mode)
|
|
1730
|
+
self.log.info("Permission mode changed", mode=mode_id)
|
|
1731
|
+
|
|
1732
|
+
elif category_id == "model":
|
|
1733
|
+
# Validate model exists
|
|
1734
|
+
models = await self.get_available_models()
|
|
1735
|
+
if models:
|
|
1736
|
+
valid_ids = {m.id for m in models}
|
|
1737
|
+
if mode_id not in valid_ids:
|
|
1738
|
+
msg = f"Unknown model: {mode_id}. Available: {valid_ids}"
|
|
1739
|
+
raise ValueError(msg)
|
|
1740
|
+
# Set the model using set_model method
|
|
1741
|
+
await self.set_model(mode_id)
|
|
1742
|
+
self.log.info("Model changed", model=mode_id)
|
|
1743
|
+
|
|
1744
|
+
elif category_id == "thinking_level":
|
|
1745
|
+
# Check if max_thinking_tokens is configured (takes precedence over keyword)
|
|
1746
|
+
if self._max_thinking_tokens:
|
|
1747
|
+
msg = (
|
|
1748
|
+
"Cannot change thinking mode: max_thinking_tokens is configured. "
|
|
1749
|
+
"The envvar MAX_THINKING_TOKENS takes precedence over the 'ultrathink' keyword."
|
|
1750
|
+
)
|
|
1751
|
+
raise ValueError(msg)
|
|
1752
|
+
# Validate thinking mode
|
|
1753
|
+
if mode_id not in THINKING_MODE_PROMPTS:
|
|
1754
|
+
msg = f"Unknown mode: {mode_id}. Available: {list(THINKING_MODE_PROMPTS.keys())}"
|
|
1755
|
+
raise ValueError(msg)
|
|
1756
|
+
self._thinking_mode = mode_id # type: ignore[assignment]
|
|
1757
|
+
self.log.info("Thinking mode changed", mode=mode_id)
|
|
1758
|
+
|
|
1759
|
+
else:
|
|
1760
|
+
msg = f"Unknown category: {category_id}. Available: permissions, model, thinking_level"
|
|
1761
|
+
raise ValueError(msg)
|
|
1007
1762
|
|
|
1008
1763
|
|
|
1009
1764
|
if __name__ == "__main__":
|
|
@@ -1011,11 +1766,23 @@ if __name__ == "__main__":
|
|
|
1011
1766
|
|
|
1012
1767
|
os.environ["ANTHROPIC_API_KEY"] = ""
|
|
1013
1768
|
|
|
1769
|
+
# async def main() -> None:
|
|
1770
|
+
# """Demo: Basic call to Claude Code."""
|
|
1771
|
+
# async with ClaudeCodeAgent(name="demo", event_handlers=["detailed"]) as agent:
|
|
1772
|
+
# print("Response (streaming): ", end="", flush=True)
|
|
1773
|
+
# async for _ in agent.run_stream("What files are in the current directory?"):
|
|
1774
|
+
# pass
|
|
1775
|
+
|
|
1014
1776
|
async def main() -> None:
|
|
1015
1777
|
"""Demo: Basic call to Claude Code."""
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1778
|
+
from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient
|
|
1779
|
+
|
|
1780
|
+
options = ClaudeAgentOptions(include_partial_messages=True)
|
|
1781
|
+
client = ClaudeSDKClient(options=options)
|
|
1782
|
+
await client.connect()
|
|
1783
|
+
prompt = "Do one tool call. list the cwd"
|
|
1784
|
+
await client.query(prompt)
|
|
1785
|
+
async for message in client.receive_response():
|
|
1786
|
+
print(message)
|
|
1020
1787
|
|
|
1021
1788
|
anyio.run(main)
|