agentpool 2.2.3__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 +0 -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/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 +18 -49
- 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/task/supervisor.py +2 -2
- acp/utils.py +2 -2
- agentpool/__init__.py +2 -0
- agentpool/agents/acp_agent/acp_agent.py +278 -263
- agentpool/agents/acp_agent/acp_converters.py +150 -17
- agentpool/agents/acp_agent/client_handler.py +35 -24
- agentpool/agents/acp_agent/session_state.py +14 -6
- agentpool/agents/agent.py +471 -643
- agentpool/agents/agui_agent/agui_agent.py +104 -107
- agentpool/agents/agui_agent/helpers.py +3 -4
- agentpool/agents/base_agent.py +485 -32
- 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 +654 -334
- agentpool/agents/claude_code_agent/converters.py +4 -141
- 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/events/__init__.py +22 -0
- agentpool/agents/events/builtin_handlers.py +65 -0
- agentpool/agents/events/event_emitter.py +3 -0
- agentpool/agents/events/events.py +84 -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 +13 -0
- agentpool/agents/slashed_agent.py +5 -4
- agentpool/agents/tool_wrapping.py +18 -6
- agentpool/common_types.py +35 -21
- 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 +9 -8
- agentpool/config_resources/external_acp_agents.yml +2 -1
- agentpool/delegation/base_team.py +4 -30
- agentpool/delegation/pool.py +104 -265
- agentpool/delegation/team.py +57 -57
- agentpool/delegation/teamrun.py +50 -55
- agentpool/functional/run.py +10 -4
- agentpool/mcp_server/client.py +73 -38
- agentpool/mcp_server/conversions.py +54 -13
- agentpool/mcp_server/manager.py +9 -23
- agentpool/mcp_server/registries/official_registry_client.py +10 -1
- agentpool/mcp_server/tool_bridge.py +114 -79
- 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 +87 -8
- agentpool/messaging/messagenode.py +52 -14
- agentpool/messaging/messages.py +2 -26
- agentpool/messaging/processing.py +10 -22
- agentpool/models/__init__.py +1 -1
- agentpool/models/acp_agents/base.py +6 -2
- agentpool/models/acp_agents/mcp_capable.py +124 -15
- agentpool/models/acp_agents/non_mcp.py +0 -23
- agentpool/models/agents.py +66 -66
- agentpool/models/agui_agents.py +1 -1
- agentpool/models/claude_code_agents.py +111 -17
- agentpool/models/file_parsing.py +0 -1
- agentpool/models/manifest.py +70 -50
- agentpool/prompts/conversion_manager.py +1 -1
- agentpool/prompts/prompts.py +5 -2
- agentpool/resource_providers/__init__.py +2 -0
- agentpool/resource_providers/aggregating.py +4 -2
- agentpool/resource_providers/base.py +13 -3
- 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 +66 -12
- agentpool/resource_providers/plan_provider.py +111 -18
- agentpool/resource_providers/pool.py +5 -3
- agentpool/resource_providers/resource_info.py +111 -0
- agentpool/resource_providers/static.py +2 -2
- agentpool/sessions/__init__.py +2 -0
- agentpool/sessions/manager.py +2 -3
- agentpool/sessions/models.py +9 -6
- agentpool/sessions/protocol.py +28 -0
- agentpool/sessions/session.py +11 -55
- agentpool/storage/manager.py +361 -54
- agentpool/talk/registry.py +4 -4
- agentpool/talk/talk.py +9 -10
- agentpool/testing.py +1 -1
- 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/streams.py +21 -96
- agentpool/vfs_registry.py +7 -2
- {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/METADATA +16 -22
- {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/RECORD +242 -195
- {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/WHEEL +1 -1
- agentpool_cli/__main__.py +20 -0
- agentpool_cli/create.py +1 -1
- agentpool_cli/serve_acp.py +59 -1
- agentpool_cli/serve_opencode.py +1 -1
- agentpool_cli/ui.py +557 -0
- agentpool_commands/__init__.py +12 -5
- agentpool_commands/agents.py +1 -1
- 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/utils.py +31 -32
- agentpool_config/__init__.py +30 -2
- agentpool_config/agentpool_tools.py +498 -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 +1 -5
- agentpool_config/nodes.py +1 -1
- agentpool_config/observability.py +44 -0
- agentpool_config/session.py +0 -3
- agentpool_config/storage.py +38 -39
- agentpool_config/task.py +3 -3
- agentpool_config/tools.py +11 -28
- agentpool_config/toolsets.py +22 -90
- agentpool_server/a2a_server/agent_worker.py +307 -0
- agentpool_server/a2a_server/server.py +23 -18
- agentpool_server/acp_server/acp_agent.py +125 -56
- agentpool_server/acp_server/commands/acp_commands.py +46 -216
- agentpool_server/acp_server/commands/docs_commands/fetch_repo.py +8 -7
- agentpool_server/acp_server/event_converter.py +651 -0
- agentpool_server/acp_server/input_provider.py +53 -10
- agentpool_server/acp_server/server.py +1 -11
- agentpool_server/acp_server/session.py +90 -410
- 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/ENDPOINTS.md +53 -14
- agentpool_server/opencode_server/OPENCODE_UI_TOOLS_COMPLETE.md +202 -0
- agentpool_server/opencode_server/__init__.py +0 -8
- agentpool_server/opencode_server/converters.py +132 -26
- agentpool_server/opencode_server/input_provider.py +160 -8
- agentpool_server/opencode_server/models/__init__.py +42 -20
- agentpool_server/opencode_server/models/app.py +12 -0
- agentpool_server/opencode_server/models/events.py +203 -29
- agentpool_server/opencode_server/models/mcp.py +19 -0
- agentpool_server/opencode_server/models/message.py +18 -1
- agentpool_server/opencode_server/models/parts.py +134 -1
- agentpool_server/opencode_server/models/question.py +56 -0
- agentpool_server/opencode_server/models/session.py +13 -1
- agentpool_server/opencode_server/routes/__init__.py +4 -0
- agentpool_server/opencode_server/routes/agent_routes.py +33 -2
- agentpool_server/opencode_server/routes/app_routes.py +66 -3
- agentpool_server/opencode_server/routes/config_routes.py +66 -5
- agentpool_server/opencode_server/routes/file_routes.py +184 -5
- agentpool_server/opencode_server/routes/global_routes.py +1 -1
- agentpool_server/opencode_server/routes/lsp_routes.py +1 -1
- agentpool_server/opencode_server/routes/message_routes.py +122 -66
- agentpool_server/opencode_server/routes/permission_routes.py +63 -0
- agentpool_server/opencode_server/routes/pty_routes.py +23 -22
- agentpool_server/opencode_server/routes/question_routes.py +128 -0
- agentpool_server/opencode_server/routes/session_routes.py +139 -68
- agentpool_server/opencode_server/routes/tui_routes.py +1 -1
- agentpool_server/opencode_server/server.py +47 -2
- agentpool_server/opencode_server/state.py +30 -0
- agentpool_storage/__init__.py +0 -4
- agentpool_storage/base.py +81 -2
- agentpool_storage/claude_provider/ARCHITECTURE.md +433 -0
- agentpool_storage/claude_provider/__init__.py +42 -0
- agentpool_storage/{claude_provider.py → claude_provider/provider.py} +190 -8
- agentpool_storage/file_provider.py +149 -15
- agentpool_storage/memory_provider.py +132 -12
- 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/session_store.py +20 -6
- agentpool_storage/sql_provider/sql_provider.py +135 -2
- agentpool_storage/sql_provider/utils.py +2 -12
- 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 -4
- agentpool_toolsets/builtin/code.py +4 -4
- agentpool_toolsets/builtin/debug.py +115 -40
- agentpool_toolsets/builtin/execution_environment.py +54 -165
- agentpool_toolsets/builtin/skills.py +0 -77
- 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/grep.py +25 -5
- agentpool_toolsets/fsspec_toolset/helpers.py +3 -2
- agentpool_toolsets/fsspec_toolset/toolset.py +350 -66
- agentpool_toolsets/mcp_discovery/data/mcp_servers.parquet +0 -0
- agentpool_toolsets/mcp_discovery/toolset.py +74 -17
- agentpool_toolsets/mcp_run_toolset.py +8 -11
- agentpool_toolsets/notifications.py +33 -33
- agentpool_toolsets/openapi.py +3 -1
- agentpool_toolsets/search_toolset.py +3 -1
- 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/opencode_provider.py +0 -730
- agentpool_storage/text_log_provider.py +0 -276
- agentpool_toolsets/builtin/chain.py +0 -288
- agentpool_toolsets/builtin/user_interaction.py +0 -52
- agentpool_toolsets/semantic_memory_toolset.py +0 -536
- {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/entry_points.txt +0 -0
- {agentpool-2.2.3.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,7 +56,9 @@ Example:
|
|
|
23
56
|
from __future__ import annotations
|
|
24
57
|
|
|
25
58
|
import asyncio
|
|
59
|
+
import contextlib
|
|
26
60
|
from decimal import Decimal
|
|
61
|
+
import re
|
|
27
62
|
from typing import TYPE_CHECKING, Any, Literal, Self
|
|
28
63
|
import uuid
|
|
29
64
|
|
|
@@ -34,7 +69,6 @@ from pydantic_ai import (
|
|
|
34
69
|
ModelResponse,
|
|
35
70
|
PartDeltaEvent,
|
|
36
71
|
PartEndEvent,
|
|
37
|
-
PartStartEvent,
|
|
38
72
|
RunUsage,
|
|
39
73
|
TextPart,
|
|
40
74
|
TextPartDelta,
|
|
@@ -50,19 +84,20 @@ from pydantic_ai.usage import RequestUsage
|
|
|
50
84
|
from agentpool.agents.base_agent import BaseAgent
|
|
51
85
|
from agentpool.agents.claude_code_agent.converters import claude_message_to_events
|
|
52
86
|
from agentpool.agents.events import (
|
|
87
|
+
PartStartEvent,
|
|
53
88
|
RunErrorEvent,
|
|
54
89
|
RunStartedEvent,
|
|
55
90
|
StreamCompleteEvent,
|
|
56
91
|
ToolCallCompleteEvent,
|
|
57
92
|
ToolCallStartEvent,
|
|
58
93
|
)
|
|
94
|
+
from agentpool.agents.events.processors import FileTracker
|
|
59
95
|
from agentpool.agents.modes import ModeInfo
|
|
60
96
|
from agentpool.log import get_logger
|
|
61
97
|
from agentpool.messaging import ChatMessage
|
|
62
98
|
from agentpool.messaging.messages import TokenCost
|
|
63
|
-
from agentpool.messaging.processing import prepare_prompts
|
|
64
99
|
from agentpool.models.claude_code_agents import ClaudeCodeAgentConfig
|
|
65
|
-
from agentpool.utils.streams import
|
|
100
|
+
from agentpool.utils.streams import merge_queue_into_iterator
|
|
66
101
|
|
|
67
102
|
|
|
68
103
|
if TYPE_CHECKING:
|
|
@@ -79,12 +114,18 @@ if TYPE_CHECKING:
|
|
|
79
114
|
ToolUseBlock,
|
|
80
115
|
)
|
|
81
116
|
from claude_agent_sdk.types import HookContext, HookInput, SyncHookJSONOutput
|
|
82
|
-
from
|
|
117
|
+
from evented_config import EventConfig
|
|
83
118
|
from exxec import ExecutionEnvironment
|
|
119
|
+
from pydantic_ai import UserContent
|
|
84
120
|
from slashed import BaseCommand, Command, CommandContext
|
|
85
121
|
from tokonomics.model_discovery.model_info import ModelInfo
|
|
122
|
+
from tokonomics.model_names import AnthropicMaxModelName
|
|
86
123
|
from toprompt import AnyPromptType
|
|
87
124
|
|
|
125
|
+
from agentpool.agents.claude_code_agent.models import (
|
|
126
|
+
ClaudeCodeCommandInfo,
|
|
127
|
+
ClaudeCodeServerInfo,
|
|
128
|
+
)
|
|
88
129
|
from agentpool.agents.context import AgentContext
|
|
89
130
|
from agentpool.agents.events import RichAgentStreamEvent
|
|
90
131
|
from agentpool.agents.modes import ModeCategory
|
|
@@ -92,11 +133,11 @@ if TYPE_CHECKING:
|
|
|
92
133
|
BuiltinEventHandlerType,
|
|
93
134
|
IndividualEventHandler,
|
|
94
135
|
MCPServerStatus,
|
|
95
|
-
PromptCompatible,
|
|
96
136
|
)
|
|
97
137
|
from agentpool.delegation import AgentPool
|
|
98
138
|
from agentpool.mcp_server.tool_bridge import ToolManagerBridge
|
|
99
139
|
from agentpool.messaging import MessageHistory
|
|
140
|
+
from agentpool.models.claude_code_agents import SettingSource
|
|
100
141
|
from agentpool.ui.base import InputProvider
|
|
101
142
|
from agentpool_config.mcp_server import MCPServerConfig
|
|
102
143
|
from agentpool_config.nodes import ToolConfirmationMode
|
|
@@ -104,14 +145,28 @@ if TYPE_CHECKING:
|
|
|
104
145
|
|
|
105
146
|
logger = get_logger(__name__)
|
|
106
147
|
|
|
107
|
-
#
|
|
108
|
-
|
|
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
|
+
}
|
|
109
161
|
|
|
110
162
|
|
|
111
163
|
def _strip_mcp_prefix(tool_name: str) -> str:
|
|
112
|
-
"""Strip MCP server prefix from tool names for cleaner UI display.
|
|
113
|
-
|
|
114
|
-
|
|
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
|
|
115
170
|
return tool_name
|
|
116
171
|
|
|
117
172
|
|
|
@@ -141,7 +196,7 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
141
196
|
disallowed_tools: list[str] | None = None,
|
|
142
197
|
system_prompt: str | Sequence[str] | None = None,
|
|
143
198
|
include_builtin_system_prompt: bool = True,
|
|
144
|
-
model: str | None = None,
|
|
199
|
+
model: AnthropicMaxModelName | str | None = None,
|
|
145
200
|
max_turns: int | None = None,
|
|
146
201
|
max_budget_usd: float | None = None,
|
|
147
202
|
max_thinking_tokens: int | None = None,
|
|
@@ -150,8 +205,10 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
150
205
|
environment: dict[str, str] | None = None,
|
|
151
206
|
add_dir: list[str] | None = None,
|
|
152
207
|
builtin_tools: list[str] | None = None,
|
|
153
|
-
fallback_model: str | None = None,
|
|
208
|
+
fallback_model: AnthropicMaxModelName | str | None = None,
|
|
154
209
|
dangerously_skip_permissions: bool = False,
|
|
210
|
+
setting_sources: list[SettingSource] | None = None,
|
|
211
|
+
use_subscription: bool = False,
|
|
155
212
|
env: ExecutionEnvironment | None = None,
|
|
156
213
|
input_provider: InputProvider | None = None,
|
|
157
214
|
agent_pool: AgentPool[Any] | None = None,
|
|
@@ -182,9 +239,12 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
182
239
|
mcp_servers: External MCP servers to connect to (internal format, converted at runtime)
|
|
183
240
|
environment: Environment variables for the agent process
|
|
184
241
|
add_dir: Additional directories to allow tool access to
|
|
185
|
-
builtin_tools: Available tools from
|
|
242
|
+
builtin_tools: Available tools from built-in set. Special: "LSP" for code intelligence,
|
|
243
|
+
"Chrome" for browser control
|
|
186
244
|
fallback_model: Fallback model when default is overloaded
|
|
187
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
|
|
188
248
|
env: Execution environment
|
|
189
249
|
input_provider: Provider for user input/confirmations
|
|
190
250
|
agent_pool: Agent pool for multi-agent coordination
|
|
@@ -218,6 +278,8 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
218
278
|
builtin_tools=builtin_tools,
|
|
219
279
|
fallback_model=fallback_model,
|
|
220
280
|
dangerously_skip_permissions=dangerously_skip_permissions,
|
|
281
|
+
setting_sources=setting_sources,
|
|
282
|
+
use_subscription=use_subscription,
|
|
221
283
|
)
|
|
222
284
|
|
|
223
285
|
super().__init__(
|
|
@@ -259,6 +321,7 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
259
321
|
self._max_budget_usd = max_budget_usd or config.max_budget_usd
|
|
260
322
|
self._max_thinking_tokens = max_thinking_tokens or config.max_thinking_tokens
|
|
261
323
|
self._permission_mode: PermissionMode | None = permission_mode or config.permission_mode
|
|
324
|
+
self._thinking_mode: ThinkingMode = "off"
|
|
262
325
|
self._external_mcp_servers = list(mcp_servers) if mcp_servers else config.get_mcp_servers()
|
|
263
326
|
self._environment = environment or config.env
|
|
264
327
|
self._add_dir = add_dir or config.add_dir
|
|
@@ -267,10 +330,14 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
267
330
|
self._dangerously_skip_permissions = (
|
|
268
331
|
dangerously_skip_permissions or config.dangerously_skip_permissions
|
|
269
332
|
)
|
|
333
|
+
self._setting_sources = setting_sources or config.setting_sources
|
|
334
|
+
self._use_subscription = use_subscription or config.use_subscription
|
|
270
335
|
|
|
271
336
|
# Client state
|
|
272
337
|
self._client: ClaudeSDKClient | None = None
|
|
273
|
-
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
|
|
274
341
|
self.deps_type = type(None)
|
|
275
342
|
|
|
276
343
|
# ToolBridge state for exposing toolsets via MCP
|
|
@@ -282,6 +349,45 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
282
349
|
# Maps tool_name to tool_call_id for matching permissions to tool call UI parts
|
|
283
350
|
self._pending_tool_call_ids: dict[str, str] = {}
|
|
284
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
|
+
|
|
285
391
|
def get_context(self, data: Any = None) -> AgentContext:
|
|
286
392
|
"""Create a new context for this agent.
|
|
287
393
|
|
|
@@ -296,7 +402,12 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
296
402
|
|
|
297
403
|
defn = self.agent_pool.manifest if self.agent_pool else AgentsManifest()
|
|
298
404
|
return AgentContext(
|
|
299
|
-
node=self,
|
|
405
|
+
node=self,
|
|
406
|
+
pool=self.agent_pool,
|
|
407
|
+
config=self._config,
|
|
408
|
+
definition=defn,
|
|
409
|
+
input_provider=self._input_provider,
|
|
410
|
+
data=data,
|
|
300
411
|
)
|
|
301
412
|
|
|
302
413
|
async def _setup_toolsets(self) -> None:
|
|
@@ -315,23 +426,21 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
315
426
|
self._mcp_servers.update(external_configs)
|
|
316
427
|
self.log.info("External MCP servers configured", server_count=len(external_configs))
|
|
317
428
|
|
|
318
|
-
if not self._config.
|
|
429
|
+
if not self._config.tools:
|
|
319
430
|
return
|
|
320
431
|
|
|
321
|
-
# Create providers from
|
|
322
|
-
for
|
|
323
|
-
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():
|
|
324
434
|
self.tools.add_provider(provider)
|
|
325
|
-
|
|
326
435
|
server_name = f"agentpool-{self.name}-tools"
|
|
327
|
-
config = BridgeConfig(
|
|
436
|
+
config = BridgeConfig(server_name=server_name)
|
|
328
437
|
self._tool_bridge = ToolManagerBridge(node=self, config=config)
|
|
329
438
|
await self._tool_bridge.start()
|
|
330
439
|
self._owns_bridge = True
|
|
331
440
|
# Get Claude SDK-compatible MCP config and merge into our servers dict
|
|
332
441
|
mcp_config = self._tool_bridge.get_claude_mcp_server_config()
|
|
333
442
|
self._mcp_servers.update(mcp_config)
|
|
334
|
-
self.log.info("
|
|
443
|
+
self.log.info("Tools initialized", tool_count=len(self._config.tools))
|
|
335
444
|
|
|
336
445
|
async def add_tool_bridge(self, bridge: ToolManagerBridge) -> None:
|
|
337
446
|
"""Add an external tool bridge to expose its tools via MCP.
|
|
@@ -351,14 +460,6 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
351
460
|
self._mcp_servers.update(mcp_config)
|
|
352
461
|
self.log.info("Added external tool bridge", server_name=bridge.config.server_name)
|
|
353
462
|
|
|
354
|
-
async def _cleanup_bridge(self) -> None:
|
|
355
|
-
"""Clean up tool bridge resources."""
|
|
356
|
-
if self._tool_bridge and self._owns_bridge:
|
|
357
|
-
await self._tool_bridge.stop()
|
|
358
|
-
self._tool_bridge = None
|
|
359
|
-
self._owns_bridge = False
|
|
360
|
-
self._mcp_servers.clear()
|
|
361
|
-
|
|
362
463
|
@property
|
|
363
464
|
def model_name(self) -> str | None:
|
|
364
465
|
"""Get the model name."""
|
|
@@ -404,20 +505,13 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
404
505
|
# input_data is PreCompactHookInput when hook_event_name == "PreCompact"
|
|
405
506
|
trigger_value = input_data.get("trigger", "auto")
|
|
406
507
|
trigger: Literal["auto", "manual"] = "manual" if trigger_value == "manual" else "auto"
|
|
407
|
-
|
|
408
508
|
# Emit semantic CompactionEvent - consumers handle display differently
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
trigger=trigger,
|
|
412
|
-
phase="starting",
|
|
413
|
-
)
|
|
509
|
+
ses_id = self.conversation_id or "unknown"
|
|
510
|
+
compaction_event = CompactionEvent(session_id=ses_id, trigger=trigger, phase="starting")
|
|
414
511
|
await self._event_queue.put(compaction_event)
|
|
415
|
-
|
|
416
512
|
return {"continue_": True}
|
|
417
513
|
|
|
418
|
-
return {
|
|
419
|
-
"PreCompact": [HookMatcher(matcher=None, hooks=[on_pre_compact])],
|
|
420
|
-
}
|
|
514
|
+
return {"PreCompact": [HookMatcher(matcher=None, hooks=[on_pre_compact])]}
|
|
421
515
|
|
|
422
516
|
def _build_options(self, *, formatted_system_prompt: str | None = None) -> ClaudeAgentOptions:
|
|
423
517
|
"""Build ClaudeAgentOptions from runtime state.
|
|
@@ -454,6 +548,25 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
454
548
|
self._can_use_tool if self.tool_confirmation_mode != "never" and not bypass else None
|
|
455
549
|
)
|
|
456
550
|
|
|
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"] = ""
|
|
569
|
+
|
|
457
570
|
return ClaudeAgentOptions(
|
|
458
571
|
cwd=self._cwd,
|
|
459
572
|
allowed_tools=self._allowed_tools or [],
|
|
@@ -464,7 +577,7 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
464
577
|
max_budget_usd=self._max_budget_usd,
|
|
465
578
|
max_thinking_tokens=self._max_thinking_tokens,
|
|
466
579
|
permission_mode=permission_mode,
|
|
467
|
-
env=
|
|
580
|
+
env=env,
|
|
468
581
|
add_dirs=self._add_dir or [], # type: ignore[arg-type] # SDK uses list not Sequence
|
|
469
582
|
tools=self._builtin_tools,
|
|
470
583
|
fallback_model=self._fallback_model,
|
|
@@ -473,6 +586,8 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
473
586
|
mcp_servers=self._mcp_servers or {},
|
|
474
587
|
include_partial_messages=True,
|
|
475
588
|
hooks=self._build_hooks(), # type: ignore[arg-type]
|
|
589
|
+
setting_sources=self._setting_sources,
|
|
590
|
+
extra_args=extra_args,
|
|
476
591
|
)
|
|
477
592
|
|
|
478
593
|
async def _can_use_tool( # noqa: PLR0911
|
|
@@ -483,47 +598,68 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
483
598
|
) -> PermissionResult:
|
|
484
599
|
"""Handle tool permission requests.
|
|
485
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
|
+
|
|
486
605
|
Args:
|
|
487
|
-
tool_name: Name of the tool being called
|
|
606
|
+
tool_name: Name of the tool being called (e.g., "Bash", "Write", "AskUserQuestion")
|
|
488
607
|
input_data: Tool input arguments
|
|
489
608
|
context: Permission context with suggestions
|
|
490
609
|
|
|
491
610
|
Returns:
|
|
492
611
|
PermissionResult indicating allow or deny
|
|
493
612
|
"""
|
|
613
|
+
import uuid
|
|
614
|
+
|
|
494
615
|
from claude_agent_sdk import PermissionResultAllow, PermissionResultDeny
|
|
495
616
|
|
|
496
|
-
from agentpool.tools
|
|
617
|
+
from agentpool.tools import FunctionTool
|
|
497
618
|
|
|
498
|
-
#
|
|
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)
|
|
499
624
|
if self.tool_confirmation_mode == "never":
|
|
500
625
|
return PermissionResultAllow()
|
|
501
626
|
|
|
502
|
-
#
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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"):
|
|
507
636
|
return PermissionResultAllow()
|
|
508
637
|
|
|
509
|
-
#
|
|
510
|
-
#
|
|
511
|
-
# Tool names are like: mcp__{server_name}__{tool_name}
|
|
512
|
-
if tool_name.startswith("mcp__") and self._mcp_servers:
|
|
513
|
-
for server_name in self._mcp_servers:
|
|
514
|
-
if tool_name.startswith(f"mcp__{server_name}__"):
|
|
515
|
-
return PermissionResultAllow()
|
|
516
|
-
|
|
517
|
-
# Use input provider if available
|
|
638
|
+
# For "default" mode and non-edit tools in "acceptEdits" mode:
|
|
639
|
+
# Ask for confirmation via input provider
|
|
518
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)
|
|
519
654
|
# Create a dummy Tool for the confirmation dialog
|
|
520
655
|
desc = f"Claude Code tool: {tool_name}"
|
|
521
|
-
tool =
|
|
522
|
-
# Get the tool call ID from our tracking dict (set from streaming events)
|
|
523
|
-
tool_call_id = self._pending_tool_call_ids.get(tool_name)
|
|
656
|
+
tool = FunctionTool(callable=lambda: None, name=display_name, description=desc)
|
|
524
657
|
ctx = self.get_context()
|
|
525
658
|
# Attach tool_call_id to context for permission event
|
|
526
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
|
|
527
663
|
result = await self._input_provider.get_tool_confirmation(
|
|
528
664
|
context=ctx,
|
|
529
665
|
tool=tool,
|
|
@@ -543,8 +679,157 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
543
679
|
# Default: deny if no input provider
|
|
544
680
|
return PermissionResultDeny(message="No input provider configured")
|
|
545
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
|
+
|
|
546
831
|
async def __aenter__(self) -> Self:
|
|
547
|
-
"""Connect to Claude Code."""
|
|
832
|
+
"""Connect to Claude Code with deferred client connection."""
|
|
548
833
|
from claude_agent_sdk import ClaudeSDKClient
|
|
549
834
|
|
|
550
835
|
await super().__aenter__()
|
|
@@ -552,10 +837,31 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
552
837
|
formatted_prompt = await self.sys_prompts.format_system_prompt(self)
|
|
553
838
|
options = self._build_options(formatted_system_prompt=formatted_prompt)
|
|
554
839
|
self._client = ClaudeSDKClient(options=options)
|
|
555
|
-
|
|
556
|
-
|
|
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())
|
|
557
843
|
return self
|
|
558
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
|
+
|
|
559
865
|
async def __aexit__(
|
|
560
866
|
self,
|
|
561
867
|
exc_type: type[BaseException] | None,
|
|
@@ -563,8 +869,19 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
563
869
|
exc_tb: TracebackType | None,
|
|
564
870
|
) -> None:
|
|
565
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
|
+
|
|
566
879
|
# Clean up tool bridge first
|
|
567
|
-
|
|
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()
|
|
568
885
|
if self._client:
|
|
569
886
|
try:
|
|
570
887
|
await self._client.disconnect()
|
|
@@ -584,36 +901,26 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
584
901
|
Commands that are not supported or not useful for external use
|
|
585
902
|
are filtered out (e.g., login, logout, context, cost).
|
|
586
903
|
"""
|
|
587
|
-
|
|
588
|
-
self.log.warning("Cannot populate commands: not connected")
|
|
589
|
-
return
|
|
590
|
-
|
|
591
|
-
server_info = await self._client.get_server_info()
|
|
904
|
+
server_info = await self.get_server_info()
|
|
592
905
|
if not server_info:
|
|
593
906
|
self.log.warning("No server info available for command population")
|
|
594
907
|
return
|
|
595
|
-
|
|
596
|
-
commands = server_info.get("commands", [])
|
|
597
|
-
if not commands:
|
|
908
|
+
if not server_info.commands:
|
|
598
909
|
self.log.debug("No commands available from Claude Code server")
|
|
599
910
|
return
|
|
600
|
-
|
|
601
911
|
# Commands to skip - not useful or problematic in this context
|
|
602
|
-
unsupported = {"
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
name = cmd_info.get("name", "")
|
|
912
|
+
unsupported = {"login", "logout", "release-notes", "todos"}
|
|
913
|
+
for cmd_info in server_info.commands:
|
|
914
|
+
name = cmd_info.name
|
|
606
915
|
if not name or name in unsupported:
|
|
607
916
|
continue
|
|
608
917
|
|
|
609
918
|
command = self._create_claude_code_command(cmd_info)
|
|
610
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)
|
|
611
922
|
|
|
612
|
-
|
|
613
|
-
"Populated command store", command_count=len(self._command_store.list_commands())
|
|
614
|
-
)
|
|
615
|
-
|
|
616
|
-
def _create_claude_code_command(self, cmd_info: dict[str, Any]) -> Command:
|
|
923
|
+
def _create_claude_code_command(self, cmd_info: ClaudeCodeCommandInfo) -> Command:
|
|
617
924
|
"""Create a slashed Command from Claude Code command info.
|
|
618
925
|
|
|
619
926
|
Args:
|
|
@@ -624,10 +931,7 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
624
931
|
"""
|
|
625
932
|
from slashed import Command
|
|
626
933
|
|
|
627
|
-
name = cmd_info.
|
|
628
|
-
description = cmd_info.get("description", "")
|
|
629
|
-
argument_hint = cmd_info.get("argumentHint")
|
|
630
|
-
|
|
934
|
+
name = cmd_info.name
|
|
631
935
|
# Handle MCP commands - they have " (MCP)" suffix in Claude Code
|
|
632
936
|
category = "claude_code"
|
|
633
937
|
if name.endswith(" (MCP)"):
|
|
@@ -640,8 +944,6 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
640
944
|
kwargs: dict[str, str],
|
|
641
945
|
) -> None:
|
|
642
946
|
"""Execute the Claude Code slash command."""
|
|
643
|
-
import re
|
|
644
|
-
|
|
645
947
|
from claude_agent_sdk.types import (
|
|
646
948
|
AssistantMessage,
|
|
647
949
|
ResultMessage,
|
|
@@ -685,71 +987,33 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
685
987
|
return Command.from_raw(
|
|
686
988
|
execute_command,
|
|
687
989
|
name=name,
|
|
688
|
-
description=description or f"Claude Code command: {name}",
|
|
990
|
+
description=cmd_info.description or f"Claude Code command: {name}",
|
|
689
991
|
category=category,
|
|
690
|
-
usage=argument_hint,
|
|
992
|
+
usage=cmd_info.argument_hint,
|
|
691
993
|
)
|
|
692
994
|
|
|
693
|
-
async def
|
|
694
|
-
self,
|
|
695
|
-
*prompts: PromptCompatible,
|
|
696
|
-
message_id: str | None = None,
|
|
697
|
-
input_provider: InputProvider | None = None,
|
|
698
|
-
message_history: MessageHistory | None = None,
|
|
699
|
-
) -> ChatMessage[TResult]:
|
|
700
|
-
"""Execute prompt against Claude Code.
|
|
701
|
-
|
|
702
|
-
Args:
|
|
703
|
-
prompts: Prompts to send
|
|
704
|
-
message_id: Optional message ID for the returned message
|
|
705
|
-
input_provider: Optional input provider for permission requests
|
|
706
|
-
message_history: Optional MessageHistory to use instead of agent's own
|
|
707
|
-
|
|
708
|
-
Returns:
|
|
709
|
-
ChatMessage containing the agent's response
|
|
710
|
-
"""
|
|
711
|
-
final_message: ChatMessage[TResult] | None = None
|
|
712
|
-
async for event in self.run_stream(
|
|
713
|
-
*prompts,
|
|
714
|
-
message_id=message_id,
|
|
715
|
-
input_provider=input_provider,
|
|
716
|
-
message_history=message_history,
|
|
717
|
-
):
|
|
718
|
-
if isinstance(event, StreamCompleteEvent):
|
|
719
|
-
final_message = event.message
|
|
720
|
-
|
|
721
|
-
if final_message is None:
|
|
722
|
-
msg = "No final message received from stream"
|
|
723
|
-
raise RuntimeError(msg)
|
|
724
|
-
|
|
725
|
-
return final_message
|
|
726
|
-
|
|
727
|
-
async def run_stream( # noqa: PLR0915
|
|
995
|
+
async def _stream_events( # noqa: PLR0915
|
|
728
996
|
self,
|
|
729
|
-
|
|
997
|
+
prompts: list[UserContent],
|
|
998
|
+
*,
|
|
999
|
+
user_msg: ChatMessage[Any],
|
|
1000
|
+
effective_parent_id: str | None,
|
|
730
1001
|
message_id: str | None = None,
|
|
1002
|
+
conversation_id: str | None = None,
|
|
1003
|
+
parent_id: str | None = None,
|
|
731
1004
|
input_provider: InputProvider | None = None,
|
|
732
1005
|
message_history: MessageHistory | None = None,
|
|
733
1006
|
deps: TDeps | None = None,
|
|
734
1007
|
event_handlers: Sequence[IndividualEventHandler | BuiltinEventHandlerType] | None = None,
|
|
1008
|
+
wait_for_connections: bool | None = None,
|
|
1009
|
+
store_history: bool = True,
|
|
735
1010
|
) -> AsyncIterator[RichAgentStreamEvent[TResult]]:
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
Args:
|
|
739
|
-
prompts: Prompts to send
|
|
740
|
-
message_id: Optional message ID for the final message
|
|
741
|
-
input_provider: Optional input provider for permission requests
|
|
742
|
-
message_history: Optional MessageHistory to use instead of agent's own
|
|
743
|
-
deps: Optional dependencies accessible via ctx.data in tools
|
|
744
|
-
event_handlers: Optional event handlers for this run (overrides agent's handlers)
|
|
745
|
-
|
|
746
|
-
Yields:
|
|
747
|
-
RichAgentStreamEvent instances during execution
|
|
748
|
-
"""
|
|
1011
|
+
from anyenv import MultiEventHandler
|
|
749
1012
|
from claude_agent_sdk import (
|
|
750
1013
|
AssistantMessage,
|
|
751
1014
|
Message,
|
|
752
1015
|
ResultMessage,
|
|
1016
|
+
SystemMessage,
|
|
753
1017
|
TextBlock,
|
|
754
1018
|
ThinkingBlock,
|
|
755
1019
|
ToolResultBlock,
|
|
@@ -758,9 +1022,24 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
758
1022
|
)
|
|
759
1023
|
from claude_agent_sdk.types import StreamEvent
|
|
760
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()
|
|
761
1031
|
# Reset cancellation state
|
|
762
1032
|
self._cancelled = False
|
|
763
|
-
|
|
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
|
|
1042
|
+
|
|
764
1043
|
# Update input provider if provided
|
|
765
1044
|
if input_provider is not None:
|
|
766
1045
|
self._input_provider = input_provider
|
|
@@ -770,28 +1049,24 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
770
1049
|
conversation = message_history if message_history is not None else self.conversation
|
|
771
1050
|
# Use provided event handlers or fall back to agent's handlers
|
|
772
1051
|
if event_handlers is not None:
|
|
773
|
-
from anyenv import MultiEventHandler
|
|
774
|
-
|
|
775
|
-
from agentpool.agents.events import resolve_event_handlers
|
|
776
|
-
|
|
777
1052
|
handler: MultiEventHandler[IndividualEventHandler] = MultiEventHandler(
|
|
778
1053
|
resolve_event_handlers(event_handlers)
|
|
779
1054
|
)
|
|
780
1055
|
else:
|
|
781
1056
|
handler = self.event_handler
|
|
782
|
-
# Prepare prompts
|
|
783
|
-
# Get parent_id from last message in history for tree structure
|
|
784
|
-
last_msg_id = conversation.get_last_message_id()
|
|
785
|
-
user_msg, processed_prompts, _original_message = await prepare_prompts(
|
|
786
|
-
*prompts, parent_id=last_msg_id
|
|
787
|
-
)
|
|
788
1057
|
# Get pending parts from conversation (staged content)
|
|
789
1058
|
pending_parts = conversation.get_pending_parts()
|
|
790
1059
|
# Combine pending parts with new prompts, then join into single string for Claude SDK
|
|
791
|
-
all_parts = [*pending_parts, *
|
|
1060
|
+
all_parts = [*pending_parts, *prompts]
|
|
792
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}"
|
|
793
1067
|
run_id = str(uuid.uuid4())
|
|
794
1068
|
# Emit run started
|
|
1069
|
+
assert self.conversation_id is not None # Initialized by BaseAgent.run_stream()
|
|
795
1070
|
run_started = RunStartedEvent(
|
|
796
1071
|
thread_id=self.conversation_id,
|
|
797
1072
|
run_id=run_id,
|
|
@@ -806,34 +1081,63 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
806
1081
|
pending_tool_calls: dict[str, ToolUseBlock] = {}
|
|
807
1082
|
# Track tool calls that already had ToolCallStartEvent emitted (via StreamEvent)
|
|
808
1083
|
emitted_tool_starts: set[str] = set()
|
|
809
|
-
|
|
810
|
-
# Accumulator for streaming tool arguments
|
|
811
|
-
from agentpool.agents.tool_call_accumulator import ToolCallAccumulator
|
|
812
|
-
|
|
813
1084
|
tool_accumulator = ToolCallAccumulator()
|
|
814
|
-
|
|
815
1085
|
# Track files modified during this run
|
|
816
1086
|
file_tracker = FileTracker()
|
|
817
|
-
|
|
1087
|
+
# Accumulate metadata events by tool_call_id (workaround for SDK stripping _meta)
|
|
1088
|
+
tool_metadata: dict[str, dict[str, Any]] = {}
|
|
818
1089
|
# Set deps on tool bridge for access during tool invocations
|
|
819
1090
|
# (ContextVar doesn't work because MCP server runs in a separate task)
|
|
820
1091
|
if self._tool_bridge:
|
|
821
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
|
|
1112
|
+
|
|
822
1113
|
try:
|
|
823
|
-
await
|
|
1114
|
+
await active_client.query(prompt_text)
|
|
824
1115
|
# Merge SDK messages with event queue for real-time tool event streaming
|
|
825
1116
|
async with merge_queue_into_iterator(
|
|
826
|
-
|
|
1117
|
+
active_client.receive_response(), self._event_queue
|
|
827
1118
|
) as merged_events:
|
|
828
1119
|
async for event_or_message in merged_events:
|
|
829
1120
|
# Check if it's a queued event (from tools via EventEmitter)
|
|
830
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
|
|
831
1129
|
# It's an event from the queue - yield it immediately
|
|
832
1130
|
await handler(None, event_or_message)
|
|
833
1131
|
yield event_or_message
|
|
834
1132
|
continue
|
|
835
1133
|
|
|
836
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
|
+
|
|
837
1141
|
# Process assistant messages - extract parts incrementally
|
|
838
1142
|
if isinstance(message, AssistantMessage):
|
|
839
1143
|
# Update model name from first assistant message
|
|
@@ -862,10 +1166,6 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
862
1166
|
# Only emit ToolCallStartEvent if not already emitted
|
|
863
1167
|
# via streaming (emits early with partial info)
|
|
864
1168
|
if tc_id not in emitted_tool_starts:
|
|
865
|
-
from agentpool.agents.claude_code_agent.converters import (
|
|
866
|
-
derive_rich_tool_info,
|
|
867
|
-
)
|
|
868
|
-
|
|
869
1169
|
rich_info = derive_rich_tool_info(name, input_data)
|
|
870
1170
|
tool_start_event = ToolCallStartEvent(
|
|
871
1171
|
tool_call_id=tc_id,
|
|
@@ -880,27 +1180,15 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
880
1180
|
file_tracker.process_event(tool_start_event)
|
|
881
1181
|
await handler(None, tool_start_event)
|
|
882
1182
|
yield tool_start_event
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
tool_name=display_name,
|
|
893
|
-
title=rich_info.title,
|
|
894
|
-
kind=rich_info.kind,
|
|
895
|
-
locations=rich_info.locations,
|
|
896
|
-
content=rich_info.content,
|
|
897
|
-
raw_input=input_data,
|
|
898
|
-
)
|
|
899
|
-
# Track file modifications using derived info
|
|
900
|
-
file_tracker.process_event(updated_event)
|
|
901
|
-
await handler(None, updated_event)
|
|
902
|
-
yield updated_event
|
|
903
|
-
# Clean up from accumulator
|
|
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)
|
|
904
1192
|
tool_accumulator.complete(tc_id)
|
|
905
1193
|
case ToolResultBlock(tool_use_id=tc_id, content=content):
|
|
906
1194
|
# Tool result received - flush response parts and add request
|
|
@@ -936,6 +1224,7 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
936
1224
|
tool_result=content,
|
|
937
1225
|
agent_name=self.name,
|
|
938
1226
|
message_id="",
|
|
1227
|
+
metadata=tool_metadata.get(tc_id),
|
|
939
1228
|
)
|
|
940
1229
|
await handler(None, tool_done_event)
|
|
941
1230
|
yield tool_done_event
|
|
@@ -988,6 +1277,7 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
988
1277
|
tool_result=result_content,
|
|
989
1278
|
agent_name=self.name,
|
|
990
1279
|
message_id="",
|
|
1280
|
+
metadata=tool_metadata.get(tc_id),
|
|
991
1281
|
)
|
|
992
1282
|
await handler(None, tool_complete_event)
|
|
993
1283
|
yield tool_complete_event
|
|
@@ -1007,13 +1297,12 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
1007
1297
|
block_type = content_block.get("type")
|
|
1008
1298
|
|
|
1009
1299
|
if block_type == "text":
|
|
1010
|
-
start_event = PartStartEvent(index=index,
|
|
1300
|
+
start_event = PartStartEvent.text(index=index, content="")
|
|
1011
1301
|
await handler(None, start_event)
|
|
1012
1302
|
yield start_event
|
|
1013
1303
|
|
|
1014
1304
|
elif block_type == "thinking":
|
|
1015
|
-
|
|
1016
|
-
start_event = PartStartEvent(index=index, part=thinking_part)
|
|
1305
|
+
start_event = PartStartEvent.thinking(index=index, content="")
|
|
1017
1306
|
await handler(None, start_event)
|
|
1018
1307
|
yield start_event
|
|
1019
1308
|
|
|
@@ -1026,12 +1315,7 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
1026
1315
|
# Track for permission matching - permission callback will use this
|
|
1027
1316
|
# Use raw name since SDK uses raw names for permissions
|
|
1028
1317
|
self._pending_tool_call_ids[raw_tool_name] = tc_id
|
|
1029
|
-
|
|
1030
1318
|
# Derive rich info with empty args for now
|
|
1031
|
-
from agentpool.agents.claude_code_agent.converters import (
|
|
1032
|
-
derive_rich_tool_info,
|
|
1033
|
-
)
|
|
1034
|
-
|
|
1035
1319
|
rich_info = derive_rich_tool_info(raw_tool_name, {})
|
|
1036
1320
|
tool_start_event = ToolCallStartEvent(
|
|
1037
1321
|
tool_call_id=tc_id,
|
|
@@ -1131,6 +1415,11 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
1131
1415
|
except asyncio.CancelledError:
|
|
1132
1416
|
self.log.info("Stream cancelled via CancelledError")
|
|
1133
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
|
+
|
|
1134
1423
|
response_msg = ChatMessage[TResult](
|
|
1135
1424
|
content="".join(text_chunks), # type: ignore[arg-type]
|
|
1136
1425
|
role="assistant",
|
|
@@ -1141,14 +1430,12 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
1141
1430
|
model_name=self.model_name,
|
|
1142
1431
|
messages=model_messages,
|
|
1143
1432
|
finish_reason="stop",
|
|
1144
|
-
metadata=
|
|
1433
|
+
metadata=metadata,
|
|
1145
1434
|
)
|
|
1146
1435
|
complete_event = StreamCompleteEvent(message=response_msg)
|
|
1147
1436
|
await handler(None, complete_event)
|
|
1148
1437
|
yield complete_event
|
|
1149
|
-
#
|
|
1150
|
-
self.message_sent.emit(response_msg)
|
|
1151
|
-
conversation.add_chat_messages([user_msg, response_msg])
|
|
1438
|
+
# Post-processing handled by base class
|
|
1152
1439
|
return
|
|
1153
1440
|
|
|
1154
1441
|
except Exception as e:
|
|
@@ -1158,6 +1445,13 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
1158
1445
|
raise
|
|
1159
1446
|
|
|
1160
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
|
+
|
|
1161
1455
|
# Clear deps from tool bridge
|
|
1162
1456
|
if self._tool_bridge:
|
|
1163
1457
|
self._tool_bridge.current_deps = None
|
|
@@ -1195,6 +1489,11 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
1195
1489
|
)
|
|
1196
1490
|
|
|
1197
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
|
|
1496
|
+
|
|
1198
1497
|
chat_message = ChatMessage[TResult](
|
|
1199
1498
|
content=final_content,
|
|
1200
1499
|
role="assistant",
|
|
@@ -1208,32 +1507,13 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
1208
1507
|
usage=request_usage or RequestUsage(),
|
|
1209
1508
|
response_time=result_message.duration_ms / 1000 if result_message else None,
|
|
1210
1509
|
finish_reason="stop" if self._cancelled else None,
|
|
1211
|
-
metadata=
|
|
1510
|
+
metadata=metadata,
|
|
1212
1511
|
)
|
|
1213
1512
|
|
|
1214
|
-
# Emit stream complete
|
|
1513
|
+
# Emit stream complete - post-processing handled by base class
|
|
1215
1514
|
complete_event = StreamCompleteEvent[TResult](message=chat_message)
|
|
1216
1515
|
await handler(None, complete_event)
|
|
1217
1516
|
yield complete_event
|
|
1218
|
-
# Record to history
|
|
1219
|
-
self.message_sent.emit(chat_message)
|
|
1220
|
-
conversation.add_chat_messages([user_msg, chat_message])
|
|
1221
|
-
|
|
1222
|
-
async def run_iter(
|
|
1223
|
-
self,
|
|
1224
|
-
*prompt_groups: Sequence[PromptCompatible],
|
|
1225
|
-
) -> AsyncIterator[ChatMessage[TResult]]:
|
|
1226
|
-
"""Run agent sequentially on multiple prompt groups.
|
|
1227
|
-
|
|
1228
|
-
Args:
|
|
1229
|
-
prompt_groups: Groups of prompts to process sequentially
|
|
1230
|
-
|
|
1231
|
-
Yields:
|
|
1232
|
-
Response messages in sequence
|
|
1233
|
-
"""
|
|
1234
|
-
for prompts in prompt_groups:
|
|
1235
|
-
response = await self.run(*prompts)
|
|
1236
|
-
yield response
|
|
1237
1517
|
|
|
1238
1518
|
async def interrupt(self) -> None:
|
|
1239
1519
|
"""Interrupt the currently running stream.
|
|
@@ -1253,21 +1533,23 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
1253
1533
|
except Exception:
|
|
1254
1534
|
self.log.exception("Failed to interrupt Claude Code client")
|
|
1255
1535
|
|
|
1256
|
-
async def set_model(self, model: str) -> None:
|
|
1536
|
+
async def set_model(self, model: AnthropicMaxModelName | str) -> None:
|
|
1257
1537
|
"""Set the model for future requests.
|
|
1258
1538
|
|
|
1259
|
-
Note: This updates the model for the next query. The client
|
|
1260
|
-
maintains the connection, so this takes effect on the next query().
|
|
1261
|
-
|
|
1262
1539
|
Args:
|
|
1263
1540
|
model: Model name to use
|
|
1264
1541
|
"""
|
|
1265
1542
|
self._model = model
|
|
1266
1543
|
self._current_model = model
|
|
1267
1544
|
|
|
1545
|
+
# Ensure client is connected before setting model
|
|
1268
1546
|
if self._client:
|
|
1547
|
+
await self.ensure_initialized()
|
|
1269
1548
|
await self._client.set_model(model)
|
|
1270
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)
|
|
1271
1553
|
|
|
1272
1554
|
async def set_tool_confirmation_mode(self, mode: ToolConfirmationMode) -> None:
|
|
1273
1555
|
"""Set tool confirmation mode.
|
|
@@ -1276,11 +1558,14 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
1276
1558
|
mode: Confirmation mode - "always", "never", or "per_tool"
|
|
1277
1559
|
"""
|
|
1278
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"
|
|
1279
1566
|
# Update permission mode on client if connected
|
|
1280
|
-
if self._client and
|
|
1281
|
-
await self._client.set_permission_mode(
|
|
1282
|
-
elif self._client and mode == "always":
|
|
1283
|
-
await self._client.set_permission_mode("default")
|
|
1567
|
+
if self._client and self._permission_mode:
|
|
1568
|
+
await self._client.set_permission_mode(self._permission_mode)
|
|
1284
1569
|
|
|
1285
1570
|
async def get_available_models(self) -> list[ModelInfo] | None:
|
|
1286
1571
|
"""Get available models for Claude Code agent.
|
|
@@ -1291,154 +1576,189 @@ class ClaudeCodeAgent[TDeps = None, TResult = str](BaseAgent[TDeps, TResult]):
|
|
|
1291
1576
|
Returns:
|
|
1292
1577
|
List of tokonomics ModelInfo for Claude models
|
|
1293
1578
|
"""
|
|
1294
|
-
from
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
# Use id_override to ensure pydantic_ai_id returns simple names like "opus"
|
|
1298
|
-
return [
|
|
1299
|
-
ModelInfo(
|
|
1300
|
-
id="claude-opus-4-20250514",
|
|
1301
|
-
name="Claude Opus",
|
|
1302
|
-
provider="anthropic",
|
|
1303
|
-
description="Claude Opus - most capable model",
|
|
1304
|
-
context_window=200000,
|
|
1305
|
-
max_output_tokens=32000,
|
|
1306
|
-
input_modalities={"text", "image"},
|
|
1307
|
-
output_modalities={"text"},
|
|
1308
|
-
pricing=ModelPricing(
|
|
1309
|
-
prompt=0.000015, # $15 per 1M tokens
|
|
1310
|
-
completion=0.000075, # $75 per 1M tokens
|
|
1311
|
-
),
|
|
1312
|
-
id_override="opus", # Claude Code SDK uses simple names
|
|
1313
|
-
),
|
|
1314
|
-
ModelInfo(
|
|
1315
|
-
id="claude-sonnet-4-20250514",
|
|
1316
|
-
name="Claude Sonnet",
|
|
1317
|
-
provider="anthropic",
|
|
1318
|
-
description="Claude Sonnet - balanced performance and speed",
|
|
1319
|
-
context_window=200000,
|
|
1320
|
-
max_output_tokens=16000,
|
|
1321
|
-
input_modalities={"text", "image"},
|
|
1322
|
-
output_modalities={"text"},
|
|
1323
|
-
pricing=ModelPricing(
|
|
1324
|
-
prompt=0.000003, # $3 per 1M tokens
|
|
1325
|
-
completion=0.000015, # $15 per 1M tokens
|
|
1326
|
-
),
|
|
1327
|
-
id_override="sonnet", # Claude Code SDK uses simple names
|
|
1328
|
-
),
|
|
1329
|
-
ModelInfo(
|
|
1330
|
-
id="claude-haiku-3-5-20241022",
|
|
1331
|
-
name="Claude Haiku",
|
|
1332
|
-
provider="anthropic",
|
|
1333
|
-
description="Claude Haiku - fast and cost-effective",
|
|
1334
|
-
context_window=200000,
|
|
1335
|
-
max_output_tokens=8000,
|
|
1336
|
-
input_modalities={"text", "image"},
|
|
1337
|
-
output_modalities={"text"},
|
|
1338
|
-
pricing=ModelPricing(
|
|
1339
|
-
prompt=0.0000008, # $0.80 per 1M tokens
|
|
1340
|
-
completion=0.000004, # $4 per 1M tokens
|
|
1341
|
-
),
|
|
1342
|
-
id_override="haiku", # Claude Code SDK uses simple names
|
|
1343
|
-
),
|
|
1344
|
-
]
|
|
1579
|
+
from agentpool.agents.claude_code_agent.static_info import MODELS
|
|
1580
|
+
|
|
1581
|
+
return MODELS
|
|
1345
1582
|
|
|
1346
|
-
def
|
|
1583
|
+
async def get_server_info(self) -> ClaudeCodeServerInfo | None:
|
|
1584
|
+
"""Get server initialization info from Claude Code.
|
|
1585
|
+
|
|
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]:
|
|
1347
1605
|
"""Get available mode categories for Claude Code agent.
|
|
1348
1606
|
|
|
1349
|
-
Claude Code exposes permission modes
|
|
1607
|
+
Claude Code exposes permission modes and model selection.
|
|
1350
1608
|
|
|
1351
1609
|
Returns:
|
|
1352
|
-
List
|
|
1610
|
+
List of ModeCategory for permissions and models
|
|
1353
1611
|
"""
|
|
1612
|
+
from agentpool.agents.claude_code_agent.static_info import MODES
|
|
1354
1613
|
from agentpool.agents.modes import ModeCategory, ModeInfo
|
|
1355
1614
|
|
|
1356
|
-
|
|
1615
|
+
categories: list[ModeCategory] = []
|
|
1616
|
+
# Permission modes
|
|
1357
1617
|
current_id = self._permission_mode or "default"
|
|
1358
1618
|
if self.tool_confirmation_mode == "never":
|
|
1359
1619
|
current_id = "bypassPermissions"
|
|
1360
1620
|
|
|
1361
|
-
|
|
1362
|
-
return [
|
|
1621
|
+
categories.append(
|
|
1363
1622
|
ModeCategory(
|
|
1364
|
-
id=
|
|
1623
|
+
id="permissions",
|
|
1365
1624
|
name="Mode",
|
|
1366
|
-
available_modes=
|
|
1367
|
-
ModeInfo(
|
|
1368
|
-
id="default",
|
|
1369
|
-
name="Default",
|
|
1370
|
-
description="Require confirmation for tool usage",
|
|
1371
|
-
category_id=category_id,
|
|
1372
|
-
),
|
|
1373
|
-
ModeInfo(
|
|
1374
|
-
id="acceptEdits",
|
|
1375
|
-
name="Accept Edits",
|
|
1376
|
-
description="Auto-approve file edits without confirmation",
|
|
1377
|
-
category_id=category_id,
|
|
1378
|
-
),
|
|
1379
|
-
ModeInfo(
|
|
1380
|
-
id="plan",
|
|
1381
|
-
name="Plan",
|
|
1382
|
-
description="Planning mode - no tool execution",
|
|
1383
|
-
category_id=category_id,
|
|
1384
|
-
),
|
|
1385
|
-
ModeInfo(
|
|
1386
|
-
id="bypassPermissions",
|
|
1387
|
-
name="Bypass Permissions",
|
|
1388
|
-
description="Skip all permission checks (use with caution)",
|
|
1389
|
-
category_id=category_id,
|
|
1390
|
-
),
|
|
1391
|
-
],
|
|
1625
|
+
available_modes=MODES,
|
|
1392
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
|
+
)
|
|
1393
1679
|
)
|
|
1394
|
-
|
|
1680
|
+
|
|
1681
|
+
return categories
|
|
1395
1682
|
|
|
1396
1683
|
async def set_mode(self, mode: ModeInfo | str, category_id: str | None = None) -> None:
|
|
1397
1684
|
"""Set a mode within a category.
|
|
1398
1685
|
|
|
1399
|
-
For Claude Code, this handles
|
|
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
|
|
1400
1690
|
|
|
1401
1691
|
Args:
|
|
1402
1692
|
mode: The mode to set - ModeInfo object or mode ID string
|
|
1403
|
-
category_id:
|
|
1693
|
+
category_id: Category ID ("permissions", "model", or "thinking_level")
|
|
1404
1694
|
|
|
1405
1695
|
Raises:
|
|
1406
1696
|
ValueError: If the category or mode is unknown
|
|
1407
1697
|
"""
|
|
1698
|
+
from agentpool.agents.claude_code_agent.static_info import VALID_MODES
|
|
1699
|
+
|
|
1408
1700
|
# Extract mode_id and category from ModeInfo if provided
|
|
1409
1701
|
if isinstance(mode, ModeInfo):
|
|
1410
1702
|
mode_id = mode.id
|
|
1411
|
-
category_id = category_id or mode.category_id
|
|
1703
|
+
category_id = category_id or mode.category_id
|
|
1412
1704
|
else:
|
|
1413
1705
|
mode_id = mode
|
|
1414
1706
|
|
|
1415
|
-
# Default to
|
|
1707
|
+
# Default to permissions if no category specified
|
|
1416
1708
|
if category_id is None:
|
|
1417
1709
|
category_id = "permissions"
|
|
1418
1710
|
|
|
1419
|
-
if category_id
|
|
1420
|
-
|
|
1421
|
-
|
|
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)
|
|
1422
1716
|
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
if mode_id not in valid_modes:
|
|
1426
|
-
msg = f"Unknown mode: {mode_id}. Available: {list(valid_modes)}"
|
|
1427
|
-
raise ValueError(msg)
|
|
1717
|
+
permission_mode: PermissionMode = mode_id # type: ignore[assignment]
|
|
1718
|
+
self._permission_mode = permission_mode
|
|
1428
1719
|
|
|
1429
|
-
|
|
1430
|
-
|
|
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"
|
|
1431
1725
|
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
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)
|
|
1437
1758
|
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
self.log.info("Permission mode changed", mode=mode_id)
|
|
1759
|
+
else:
|
|
1760
|
+
msg = f"Unknown category: {category_id}. Available: permissions, model, thinking_level"
|
|
1761
|
+
raise ValueError(msg)
|
|
1442
1762
|
|
|
1443
1763
|
|
|
1444
1764
|
if __name__ == "__main__":
|