klaude-code 2.2.0__py3-none-any.whl → 2.3.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.
- klaude_code/app/runtime.py +2 -15
- klaude_code/cli/list_model.py +27 -10
- klaude_code/cli/main.py +25 -9
- klaude_code/config/assets/builtin_config.yaml +25 -16
- klaude_code/config/config.py +144 -7
- klaude_code/config/select_model.py +38 -13
- klaude_code/config/sub_agent_model_helper.py +217 -0
- klaude_code/const.py +1 -1
- klaude_code/core/agent_profile.py +43 -5
- klaude_code/core/executor.py +75 -0
- klaude_code/core/manager/llm_clients_builder.py +17 -11
- klaude_code/core/prompts/prompt-nano-banana.md +1 -1
- klaude_code/core/tool/sub_agent_tool.py +2 -1
- klaude_code/llm/anthropic/client.py +7 -4
- klaude_code/llm/anthropic/input.py +54 -29
- klaude_code/llm/google/client.py +1 -1
- klaude_code/llm/google/input.py +23 -2
- klaude_code/llm/openai_compatible/input.py +22 -13
- klaude_code/llm/openrouter/input.py +37 -25
- klaude_code/llm/responses/input.py +96 -57
- klaude_code/protocol/commands.py +1 -2
- klaude_code/protocol/events/system.py +4 -0
- klaude_code/protocol/op.py +17 -0
- klaude_code/protocol/op_handler.py +5 -0
- klaude_code/protocol/sub_agent/AGENTS.md +28 -0
- klaude_code/protocol/sub_agent/__init__.py +10 -14
- klaude_code/protocol/sub_agent/image_gen.py +2 -1
- klaude_code/session/codec.py +2 -6
- klaude_code/session/session.py +9 -1
- klaude_code/skill/assets/create-plan/SKILL.md +3 -5
- klaude_code/tui/command/__init__.py +3 -6
- klaude_code/tui/command/model_cmd.py +6 -43
- klaude_code/tui/command/model_select.py +75 -15
- klaude_code/tui/command/sub_agent_model_cmd.py +190 -0
- klaude_code/tui/components/bash_syntax.py +1 -1
- klaude_code/tui/components/common.py +1 -1
- klaude_code/tui/components/developer.py +0 -5
- klaude_code/tui/components/metadata.py +1 -63
- klaude_code/tui/components/rich/cjk_wrap.py +3 -2
- klaude_code/tui/components/rich/status.py +49 -3
- klaude_code/tui/components/rich/theme.py +2 -0
- klaude_code/tui/components/sub_agent.py +25 -46
- klaude_code/tui/components/welcome.py +99 -0
- klaude_code/tui/input/prompt_toolkit.py +14 -1
- klaude_code/tui/renderer.py +2 -3
- klaude_code/tui/terminal/selector.py +5 -3
- {klaude_code-2.2.0.dist-info → klaude_code-2.3.0.dist-info}/METADATA +1 -1
- {klaude_code-2.2.0.dist-info → klaude_code-2.3.0.dist-info}/RECORD +50 -48
- klaude_code/tui/command/help_cmd.py +0 -51
- klaude_code/tui/command/release_notes_cmd.py +0 -85
- {klaude_code-2.2.0.dist-info → klaude_code-2.3.0.dist-info}/WHEEL +0 -0
- {klaude_code-2.2.0.dist-info → klaude_code-2.3.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""Helper for sub-agent model availability and selection logic."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from klaude_code.protocol.sub_agent import (
|
|
9
|
+
AVAILABILITY_IMAGE_MODEL,
|
|
10
|
+
SubAgentProfile,
|
|
11
|
+
get_sub_agent_profile,
|
|
12
|
+
get_sub_agent_profile_by_tool,
|
|
13
|
+
iter_sub_agent_profiles,
|
|
14
|
+
sub_agent_tool_names,
|
|
15
|
+
)
|
|
16
|
+
from klaude_code.protocol.tools import SubAgentType
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from klaude_code.config.config import Config, ModelEntry
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class SubAgentModelInfo:
|
|
24
|
+
"""Sub-agent and its current model configuration."""
|
|
25
|
+
|
|
26
|
+
profile: SubAgentProfile
|
|
27
|
+
# Explicitly configured model selector (from config), if any.
|
|
28
|
+
configured_model: str | None
|
|
29
|
+
|
|
30
|
+
# Effective model name used by this sub-agent.
|
|
31
|
+
# - When configured_model is set: equals configured_model.
|
|
32
|
+
# - When requirement-based default applies (e.g. ImageGen): resolved model.
|
|
33
|
+
# - When inheriting from main agent: None.
|
|
34
|
+
effective_model: str | None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True, slots=True)
|
|
38
|
+
class EmptySubAgentModelBehavior:
|
|
39
|
+
"""Human-facing description for an unset (empty) sub-agent model config."""
|
|
40
|
+
|
|
41
|
+
# Summary text for UI (kept UI-framework agnostic).
|
|
42
|
+
description: str
|
|
43
|
+
|
|
44
|
+
# Best-effort resolved model name (if any). For ImageGen this is usually the
|
|
45
|
+
# first available image model; for other sub-agents it's the main model.
|
|
46
|
+
resolved_model_name: str | None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class SubAgentModelHelper:
|
|
50
|
+
"""Centralized logic for sub-agent availability and model selection."""
|
|
51
|
+
|
|
52
|
+
def __init__(self, config: Config) -> None:
|
|
53
|
+
self._config = config
|
|
54
|
+
|
|
55
|
+
def check_availability_requirement(self, requirement: str | None) -> bool:
|
|
56
|
+
"""Check if a sub-agent's availability requirement is met.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
requirement: The availability requirement constant (e.g., AVAILABILITY_IMAGE_MODEL).
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
True if the requirement is met or if there's no requirement.
|
|
63
|
+
"""
|
|
64
|
+
if requirement is None:
|
|
65
|
+
return True
|
|
66
|
+
|
|
67
|
+
if requirement == AVAILABILITY_IMAGE_MODEL:
|
|
68
|
+
return self._config.has_available_image_model()
|
|
69
|
+
|
|
70
|
+
return True
|
|
71
|
+
|
|
72
|
+
def resolve_model_for_requirement(self, requirement: str | None) -> str | None:
|
|
73
|
+
"""Resolve the model name for a given availability requirement.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
requirement: The availability requirement constant.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
The model name if found, None otherwise.
|
|
80
|
+
"""
|
|
81
|
+
if requirement == AVAILABILITY_IMAGE_MODEL:
|
|
82
|
+
return self._config.get_first_available_image_model()
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
def resolve_default_model_override(self, sub_agent_type: str) -> str | None:
|
|
86
|
+
"""Resolve the default model override for a sub-agent when unset.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
- None for sub-agents that default to inheriting the main agent.
|
|
90
|
+
- A model name for sub-agents that require a dedicated model (e.g. ImageGen).
|
|
91
|
+
|
|
92
|
+
Note: This intentionally ignores any explicit user config; callers use this
|
|
93
|
+
when they want the *unset* behavior.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
profile = get_sub_agent_profile(sub_agent_type)
|
|
97
|
+
if profile.availability_requirement is None:
|
|
98
|
+
return None
|
|
99
|
+
return self.resolve_model_for_requirement(profile.availability_requirement)
|
|
100
|
+
|
|
101
|
+
def describe_empty_model_config_behavior(
|
|
102
|
+
self,
|
|
103
|
+
sub_agent_type: str,
|
|
104
|
+
*,
|
|
105
|
+
main_model_name: str,
|
|
106
|
+
) -> EmptySubAgentModelBehavior:
|
|
107
|
+
"""Describe what happens when a sub-agent model is not configured.
|
|
108
|
+
|
|
109
|
+
Most sub-agents default to inheriting the main model.
|
|
110
|
+
|
|
111
|
+
Sub-agents with an availability requirement (e.g. ImageGen) do NOT
|
|
112
|
+
inherit from the main model; instead they auto-resolve a suitable model
|
|
113
|
+
(currently: the first available image model).
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
profile = get_sub_agent_profile(sub_agent_type)
|
|
117
|
+
|
|
118
|
+
requirement = profile.availability_requirement
|
|
119
|
+
if requirement is None:
|
|
120
|
+
return EmptySubAgentModelBehavior(
|
|
121
|
+
description=f"inherit from main agent: {main_model_name}",
|
|
122
|
+
resolved_model_name=main_model_name,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
resolved = self.resolve_model_for_requirement(requirement)
|
|
126
|
+
if requirement == AVAILABILITY_IMAGE_MODEL:
|
|
127
|
+
if resolved:
|
|
128
|
+
return EmptySubAgentModelBehavior(
|
|
129
|
+
description=f"auto-select first available image model: {resolved}",
|
|
130
|
+
resolved_model_name=resolved,
|
|
131
|
+
)
|
|
132
|
+
return EmptySubAgentModelBehavior(
|
|
133
|
+
description="auto-select first available image model",
|
|
134
|
+
resolved_model_name=None,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
if resolved:
|
|
138
|
+
return EmptySubAgentModelBehavior(
|
|
139
|
+
description=f"auto-select model for requirement '{requirement}': {resolved}",
|
|
140
|
+
resolved_model_name=resolved,
|
|
141
|
+
)
|
|
142
|
+
return EmptySubAgentModelBehavior(
|
|
143
|
+
description=f"auto-select model for requirement '{requirement}'",
|
|
144
|
+
resolved_model_name=None,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
def get_available_sub_agents(self) -> list[SubAgentModelInfo]:
|
|
148
|
+
"""Return all available sub-agents with their current model config.
|
|
149
|
+
|
|
150
|
+
Only returns sub-agents that:
|
|
151
|
+
1. Are enabled by default
|
|
152
|
+
2. Have their availability requirements met
|
|
153
|
+
|
|
154
|
+
For sub-agents without explicit config, resolves model based on availability_requirement.
|
|
155
|
+
"""
|
|
156
|
+
result: list[SubAgentModelInfo] = []
|
|
157
|
+
for profile in iter_sub_agent_profiles(enabled_only=True):
|
|
158
|
+
if not self.check_availability_requirement(profile.availability_requirement):
|
|
159
|
+
continue
|
|
160
|
+
configured_model = self._config.sub_agent_models.get(profile.name)
|
|
161
|
+
effective_model = configured_model
|
|
162
|
+
if not effective_model and profile.availability_requirement:
|
|
163
|
+
effective_model = self.resolve_model_for_requirement(profile.availability_requirement)
|
|
164
|
+
result.append(
|
|
165
|
+
SubAgentModelInfo(
|
|
166
|
+
profile=profile,
|
|
167
|
+
configured_model=configured_model,
|
|
168
|
+
effective_model=effective_model,
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
return result
|
|
172
|
+
|
|
173
|
+
def get_selectable_models(self, sub_agent_type: str) -> list[ModelEntry]:
|
|
174
|
+
"""Return selectable models for a specific sub-agent type.
|
|
175
|
+
|
|
176
|
+
For sub-agents with availability_requirement (e.g., ImageGen):
|
|
177
|
+
- Only returns models matching the requirement (e.g., image models)
|
|
178
|
+
|
|
179
|
+
For other sub-agents:
|
|
180
|
+
- Returns all available models
|
|
181
|
+
"""
|
|
182
|
+
profile = get_sub_agent_profile(sub_agent_type)
|
|
183
|
+
all_models = self._config.iter_model_entries(only_available=True)
|
|
184
|
+
|
|
185
|
+
if profile.availability_requirement == AVAILABILITY_IMAGE_MODEL:
|
|
186
|
+
return [m for m in all_models if m.model_params.modalities and "image" in m.model_params.modalities]
|
|
187
|
+
|
|
188
|
+
return all_models
|
|
189
|
+
|
|
190
|
+
def get_enabled_sub_agent_tool_names(self) -> list[str]:
|
|
191
|
+
"""Return sub-agent tool names that should be added to main agent's tool list."""
|
|
192
|
+
result: list[str] = []
|
|
193
|
+
for name in sub_agent_tool_names(enabled_only=True):
|
|
194
|
+
profile = get_sub_agent_profile_by_tool(name)
|
|
195
|
+
if profile is not None and self.check_availability_requirement(profile.availability_requirement):
|
|
196
|
+
result.append(name)
|
|
197
|
+
return result
|
|
198
|
+
|
|
199
|
+
def get_enabled_sub_agent_types(self) -> set[SubAgentType]:
|
|
200
|
+
"""Return set of sub-agent types that are enabled and available."""
|
|
201
|
+
enabled: set[SubAgentType] = set()
|
|
202
|
+
for name in sub_agent_tool_names(enabled_only=True):
|
|
203
|
+
profile = get_sub_agent_profile_by_tool(name)
|
|
204
|
+
if profile is not None and self.check_availability_requirement(profile.availability_requirement):
|
|
205
|
+
enabled.add(profile.name)
|
|
206
|
+
return enabled
|
|
207
|
+
|
|
208
|
+
def build_sub_agent_client_configs(self) -> dict[SubAgentType, str]:
|
|
209
|
+
"""Return model names for each sub-agent that needs a dedicated client."""
|
|
210
|
+
result: dict[SubAgentType, str] = {}
|
|
211
|
+
for profile in iter_sub_agent_profiles():
|
|
212
|
+
model_name = self._config.sub_agent_models.get(profile.name)
|
|
213
|
+
if not model_name and profile.availability_requirement:
|
|
214
|
+
model_name = self.resolve_model_for_requirement(profile.availability_requirement)
|
|
215
|
+
if model_name:
|
|
216
|
+
result[profile.name] = model_name
|
|
217
|
+
return result
|
klaude_code/const.py
CHANGED
|
@@ -123,7 +123,7 @@ TAB_EXPAND_WIDTH = 8 # Tab expansion width for text rendering
|
|
|
123
123
|
DIFF_PREFIX_WIDTH = 4 # Width of line number prefix in diff display
|
|
124
124
|
MAX_DIFF_LINES = 500 # Maximum lines to show in diff output
|
|
125
125
|
INVALID_TOOL_CALL_MAX_LENGTH = 200 # Maximum length for invalid tool call display
|
|
126
|
-
TRUNCATE_DISPLAY_MAX_LINE_LENGTH =
|
|
126
|
+
TRUNCATE_DISPLAY_MAX_LINE_LENGTH = 500 # Maximum line length for truncated display
|
|
127
127
|
TRUNCATE_DISPLAY_MAX_LINES = 4 # Maximum lines for truncated display
|
|
128
128
|
MIN_HIDDEN_LINES_FOR_INDICATOR = 5 # Minimum hidden lines before showing truncation indicator
|
|
129
129
|
SUB_AGENT_RESULT_MAX_LINES = 10 # Maximum lines for sub-agent result display
|
|
@@ -7,8 +7,12 @@ from dataclasses import dataclass
|
|
|
7
7
|
from functools import cache
|
|
8
8
|
from importlib.resources import files
|
|
9
9
|
from pathlib import Path
|
|
10
|
-
from typing import Any, Protocol
|
|
10
|
+
from typing import TYPE_CHECKING, Any, Protocol
|
|
11
11
|
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from klaude_code.config.config import Config
|
|
14
|
+
|
|
15
|
+
from klaude_code.config.sub_agent_model_helper import SubAgentModelHelper
|
|
12
16
|
from klaude_code.core.reminders import (
|
|
13
17
|
at_file_reader_reminder,
|
|
14
18
|
empty_todo_reminder,
|
|
@@ -23,7 +27,7 @@ from klaude_code.core.tool.report_back_tool import ReportBackTool
|
|
|
23
27
|
from klaude_code.core.tool.tool_registry import get_tool_schemas
|
|
24
28
|
from klaude_code.llm import LLMClientABC
|
|
25
29
|
from klaude_code.protocol import llm_param, message, tools
|
|
26
|
-
from klaude_code.protocol.sub_agent import get_sub_agent_profile
|
|
30
|
+
from klaude_code.protocol.sub_agent import get_sub_agent_profile
|
|
27
31
|
from klaude_code.session import Session
|
|
28
32
|
|
|
29
33
|
type Reminder = Callable[[Session], Awaitable[message.DeveloperMessage | None]]
|
|
@@ -169,12 +173,14 @@ def load_system_prompt(
|
|
|
169
173
|
def load_agent_tools(
|
|
170
174
|
model_name: str,
|
|
171
175
|
sub_agent_type: tools.SubAgentType | None = None,
|
|
176
|
+
config: Config | None = None,
|
|
172
177
|
) -> list[llm_param.ToolSchema]:
|
|
173
178
|
"""Get tools for an agent based on model and agent type.
|
|
174
179
|
|
|
175
180
|
Args:
|
|
176
181
|
model_name: The model name.
|
|
177
182
|
sub_agent_type: If None, returns main agent tools. Otherwise returns sub-agent tools.
|
|
183
|
+
config: Config for checking sub-agent availability (e.g., image model availability).
|
|
178
184
|
"""
|
|
179
185
|
|
|
180
186
|
if sub_agent_type is not None:
|
|
@@ -183,13 +189,20 @@ def load_agent_tools(
|
|
|
183
189
|
|
|
184
190
|
# Main agent tools
|
|
185
191
|
if "gpt-5" in model_name:
|
|
186
|
-
tool_names = [tools.BASH, tools.READ, tools.APPLY_PATCH, tools.UPDATE_PLAN]
|
|
192
|
+
tool_names: list[str] = [tools.BASH, tools.READ, tools.APPLY_PATCH, tools.UPDATE_PLAN]
|
|
187
193
|
elif "gemini-3" in model_name:
|
|
188
194
|
tool_names = [tools.BASH, tools.READ, tools.EDIT, tools.WRITE]
|
|
189
195
|
else:
|
|
190
196
|
tool_names = [tools.BASH, tools.READ, tools.EDIT, tools.WRITE, tools.TODO_WRITE]
|
|
191
197
|
|
|
192
|
-
|
|
198
|
+
if config is not None:
|
|
199
|
+
helper = SubAgentModelHelper(config)
|
|
200
|
+
tool_names.extend(helper.get_enabled_sub_agent_tool_names())
|
|
201
|
+
else:
|
|
202
|
+
from klaude_code.protocol.sub_agent import sub_agent_tool_names
|
|
203
|
+
|
|
204
|
+
tool_names.extend(sub_agent_tool_names(enabled_only=True))
|
|
205
|
+
|
|
193
206
|
tool_names.extend([tools.MERMAID])
|
|
194
207
|
# tool_names.extend([tools.MEMORY])
|
|
195
208
|
return get_tool_schemas(tool_names)
|
|
@@ -249,10 +262,17 @@ class ModelProfileProvider(Protocol):
|
|
|
249
262
|
output_schema: dict[str, Any] | None = None,
|
|
250
263
|
) -> AgentProfile: ...
|
|
251
264
|
|
|
265
|
+
def enabled_sub_agent_types(self) -> set[tools.SubAgentType]:
|
|
266
|
+
"""Return set of sub-agent types enabled for this provider."""
|
|
267
|
+
...
|
|
268
|
+
|
|
252
269
|
|
|
253
270
|
class DefaultModelProfileProvider(ModelProfileProvider):
|
|
254
271
|
"""Default provider backed by global prompts/tool/reminder registries."""
|
|
255
272
|
|
|
273
|
+
def __init__(self, config: Config | None = None) -> None:
|
|
274
|
+
self._config = config
|
|
275
|
+
|
|
256
276
|
def build_profile(
|
|
257
277
|
self,
|
|
258
278
|
llm_client: LLMClientABC,
|
|
@@ -264,13 +284,25 @@ class DefaultModelProfileProvider(ModelProfileProvider):
|
|
|
264
284
|
profile = AgentProfile(
|
|
265
285
|
llm_client=llm_client,
|
|
266
286
|
system_prompt=load_system_prompt(model_name, llm_client.protocol, sub_agent_type),
|
|
267
|
-
tools=load_agent_tools(model_name, sub_agent_type),
|
|
287
|
+
tools=load_agent_tools(model_name, sub_agent_type, config=self._config),
|
|
268
288
|
reminders=load_agent_reminders(model_name, sub_agent_type),
|
|
269
289
|
)
|
|
270
290
|
if output_schema:
|
|
271
291
|
return with_structured_output(profile, output_schema)
|
|
272
292
|
return profile
|
|
273
293
|
|
|
294
|
+
def enabled_sub_agent_types(self) -> set[tools.SubAgentType]:
|
|
295
|
+
if self._config is None:
|
|
296
|
+
from klaude_code.protocol.sub_agent import get_sub_agent_profile_by_tool, sub_agent_tool_names
|
|
297
|
+
|
|
298
|
+
return {
|
|
299
|
+
profile.name
|
|
300
|
+
for name in sub_agent_tool_names(enabled_only=True)
|
|
301
|
+
if (profile := get_sub_agent_profile_by_tool(name)) is not None
|
|
302
|
+
}
|
|
303
|
+
helper = SubAgentModelHelper(self._config)
|
|
304
|
+
return helper.get_enabled_sub_agent_types()
|
|
305
|
+
|
|
274
306
|
|
|
275
307
|
class VanillaModelProfileProvider(ModelProfileProvider):
|
|
276
308
|
"""Provider that strips prompts, reminders, and tools for vanilla mode."""
|
|
@@ -293,6 +325,9 @@ class VanillaModelProfileProvider(ModelProfileProvider):
|
|
|
293
325
|
return with_structured_output(profile, output_schema)
|
|
294
326
|
return profile
|
|
295
327
|
|
|
328
|
+
def enabled_sub_agent_types(self) -> set[tools.SubAgentType]:
|
|
329
|
+
return set()
|
|
330
|
+
|
|
296
331
|
|
|
297
332
|
class NanoBananaModelProfileProvider(ModelProfileProvider):
|
|
298
333
|
"""Provider for the Nano Banana image generation model.
|
|
@@ -317,3 +352,6 @@ class NanoBananaModelProfileProvider(ModelProfileProvider):
|
|
|
317
352
|
if output_schema:
|
|
318
353
|
return with_structured_output(profile, output_schema)
|
|
319
354
|
return profile
|
|
355
|
+
|
|
356
|
+
def enabled_sub_agent_types(self) -> set[tools.SubAgentType]:
|
|
357
|
+
return set()
|
klaude_code/core/executor.py
CHANGED
|
@@ -15,6 +15,7 @@ from dataclasses import dataclass
|
|
|
15
15
|
from pathlib import Path
|
|
16
16
|
|
|
17
17
|
from klaude_code.config import load_config
|
|
18
|
+
from klaude_code.config.sub_agent_model_helper import SubAgentModelHelper
|
|
18
19
|
from klaude_code.core.agent import Agent
|
|
19
20
|
from klaude_code.core.agent_profile import DefaultModelProfileProvider, ModelProfileProvider
|
|
20
21
|
from klaude_code.core.manager import LLMClients, SubAgentManager
|
|
@@ -109,6 +110,15 @@ class AgentRuntime:
|
|
|
109
110
|
def current_agent(self) -> Agent | None:
|
|
110
111
|
return self._agent
|
|
111
112
|
|
|
113
|
+
def _get_sub_agent_models(self) -> dict[str, LLMConfigParameter]:
|
|
114
|
+
"""Build a dict of sub-agent type to LLMConfigParameter."""
|
|
115
|
+
enabled = self._model_profile_provider.enabled_sub_agent_types()
|
|
116
|
+
return {
|
|
117
|
+
sub_agent_type: client.get_llm_config()
|
|
118
|
+
for sub_agent_type, client in self._llm_clients.sub_clients.items()
|
|
119
|
+
if sub_agent_type in enabled
|
|
120
|
+
}
|
|
121
|
+
|
|
112
122
|
async def ensure_agent(self, session_id: str | None = None) -> Agent:
|
|
113
123
|
"""Return the active agent, creating or loading a session as needed."""
|
|
114
124
|
|
|
@@ -135,6 +145,7 @@ class AgentRuntime:
|
|
|
135
145
|
session_id=session.id,
|
|
136
146
|
work_dir=str(session.work_dir),
|
|
137
147
|
llm_config=self._llm_clients.main.get_llm_config(),
|
|
148
|
+
sub_agent_models=self._get_sub_agent_models(),
|
|
138
149
|
)
|
|
139
150
|
)
|
|
140
151
|
|
|
@@ -200,6 +211,7 @@ class AgentRuntime:
|
|
|
200
211
|
session_id=agent.session.id,
|
|
201
212
|
work_dir=str(agent.session.work_dir),
|
|
202
213
|
llm_config=self._llm_clients.main.get_llm_config(),
|
|
214
|
+
sub_agent_models=self._get_sub_agent_models(),
|
|
203
215
|
)
|
|
204
216
|
)
|
|
205
217
|
|
|
@@ -223,6 +235,7 @@ class AgentRuntime:
|
|
|
223
235
|
session_id=target_session.id,
|
|
224
236
|
work_dir=str(target_session.work_dir),
|
|
225
237
|
llm_config=self._llm_clients.main.get_llm_config(),
|
|
238
|
+
sub_agent_models=self._get_sub_agent_models(),
|
|
226
239
|
)
|
|
227
240
|
)
|
|
228
241
|
|
|
@@ -406,6 +419,15 @@ class ExecutorContext:
|
|
|
406
419
|
"""Emit an event to the UI display system."""
|
|
407
420
|
await self.event_queue.put(event)
|
|
408
421
|
|
|
422
|
+
def _get_sub_agent_models(self) -> dict[str, LLMConfigParameter]:
|
|
423
|
+
"""Build a dict of sub-agent type to LLMConfigParameter."""
|
|
424
|
+
enabled = self.model_profile_provider.enabled_sub_agent_types()
|
|
425
|
+
return {
|
|
426
|
+
sub_agent_type: client.get_llm_config()
|
|
427
|
+
for sub_agent_type, client in self.llm_clients.sub_clients.items()
|
|
428
|
+
if sub_agent_type in enabled
|
|
429
|
+
}
|
|
430
|
+
|
|
409
431
|
def current_session_id(self) -> str | None:
|
|
410
432
|
"""Return the primary active session id, if any.
|
|
411
433
|
|
|
@@ -455,6 +477,7 @@ class ExecutorContext:
|
|
|
455
477
|
llm_config=llm_config,
|
|
456
478
|
work_dir=str(agent.session.work_dir),
|
|
457
479
|
show_klaude_code_info=False,
|
|
480
|
+
show_sub_agent_models=False,
|
|
458
481
|
)
|
|
459
482
|
)
|
|
460
483
|
|
|
@@ -501,9 +524,61 @@ class ExecutorContext:
|
|
|
501
524
|
work_dir=str(agent.session.work_dir),
|
|
502
525
|
llm_config=agent.profile.llm_client.get_llm_config(),
|
|
503
526
|
show_klaude_code_info=False,
|
|
527
|
+
show_sub_agent_models=False,
|
|
504
528
|
)
|
|
505
529
|
)
|
|
506
530
|
|
|
531
|
+
async def handle_change_sub_agent_model(self, operation: op.ChangeSubAgentModelOperation) -> None:
|
|
532
|
+
"""Handle a change sub-agent model operation."""
|
|
533
|
+
agent = await self._agent_runtime.ensure_agent(operation.session_id)
|
|
534
|
+
config = load_config()
|
|
535
|
+
|
|
536
|
+
helper = SubAgentModelHelper(config)
|
|
537
|
+
|
|
538
|
+
sub_agent_type = operation.sub_agent_type
|
|
539
|
+
model_name = operation.model_name
|
|
540
|
+
|
|
541
|
+
if model_name is None:
|
|
542
|
+
# Clear explicit override and revert to sub-agent default behavior.
|
|
543
|
+
behavior = helper.describe_empty_model_config_behavior(
|
|
544
|
+
sub_agent_type,
|
|
545
|
+
main_model_name=self.llm_clients.main.model_name,
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
resolved = helper.resolve_default_model_override(sub_agent_type)
|
|
549
|
+
if resolved is None:
|
|
550
|
+
# Default: inherit from main client.
|
|
551
|
+
self.llm_clients.sub_clients.pop(sub_agent_type, None)
|
|
552
|
+
else:
|
|
553
|
+
# Default: use a dedicated model (e.g. first available image model).
|
|
554
|
+
llm_config = config.get_model_config(resolved)
|
|
555
|
+
new_client = create_llm_client(llm_config)
|
|
556
|
+
self.llm_clients.sub_clients[sub_agent_type] = new_client
|
|
557
|
+
|
|
558
|
+
display_model = f"({behavior.description})"
|
|
559
|
+
else:
|
|
560
|
+
# Create new client for the sub-agent
|
|
561
|
+
llm_config = config.get_model_config(model_name)
|
|
562
|
+
new_client = create_llm_client(llm_config)
|
|
563
|
+
self.llm_clients.sub_clients[sub_agent_type] = new_client
|
|
564
|
+
display_model = new_client.model_name
|
|
565
|
+
|
|
566
|
+
if operation.save_as_default:
|
|
567
|
+
if model_name is None:
|
|
568
|
+
# Remove from config to inherit
|
|
569
|
+
config.sub_agent_models.pop(sub_agent_type, None)
|
|
570
|
+
else:
|
|
571
|
+
config.sub_agent_models[sub_agent_type] = model_name
|
|
572
|
+
await config.save()
|
|
573
|
+
|
|
574
|
+
saved_note = " (saved in ~/.klaude/klaude-config.yaml)" if operation.save_as_default else ""
|
|
575
|
+
developer_item = message.DeveloperMessage(
|
|
576
|
+
parts=message.text_parts_from_str(f"{sub_agent_type} model: {display_model}{saved_note}"),
|
|
577
|
+
ui_extra=model.build_command_output_extra(commands.CommandName.SUB_AGENT_MODEL),
|
|
578
|
+
)
|
|
579
|
+
agent.session.append_history([developer_item])
|
|
580
|
+
await self.emit_event(events.DeveloperMessageEvent(session_id=agent.session.id, item=developer_item))
|
|
581
|
+
|
|
507
582
|
async def handle_clear_session(self, operation: op.ClearSessionOperation) -> None:
|
|
508
583
|
await self._agent_runtime.clear_session(operation.session_id)
|
|
509
584
|
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from klaude_code.config import Config
|
|
6
|
+
from klaude_code.config.sub_agent_model_helper import SubAgentModelHelper
|
|
6
7
|
from klaude_code.core.manager.llm_clients import LLMClients
|
|
7
8
|
from klaude_code.llm.client import LLMClientABC
|
|
8
9
|
from klaude_code.llm.registry import create_llm_client
|
|
9
10
|
from klaude_code.log import DebugType, log_debug
|
|
10
|
-
from klaude_code.protocol.sub_agent import iter_sub_agent_profiles
|
|
11
11
|
from klaude_code.protocol.tools import SubAgentType
|
|
12
12
|
|
|
13
13
|
|
|
@@ -15,8 +15,15 @@ def build_llm_clients(
|
|
|
15
15
|
config: Config,
|
|
16
16
|
*,
|
|
17
17
|
model_override: str | None = None,
|
|
18
|
+
skip_sub_agents: bool = False,
|
|
18
19
|
) -> LLMClients:
|
|
19
|
-
"""Create an ``LLMClients`` bundle driven by application config.
|
|
20
|
+
"""Create an ``LLMClients`` bundle driven by application config.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
config: Application configuration.
|
|
24
|
+
model_override: Override for the main model name.
|
|
25
|
+
skip_sub_agents: If True, skip initializing sub-agent clients (e.g., for vanilla/banana modes).
|
|
26
|
+
"""
|
|
20
27
|
|
|
21
28
|
# Resolve main agent LLM config
|
|
22
29
|
model_name = model_override or config.main_model
|
|
@@ -32,17 +39,16 @@ def build_llm_clients(
|
|
|
32
39
|
)
|
|
33
40
|
|
|
34
41
|
main_client = create_llm_client(llm_config)
|
|
35
|
-
sub_clients: dict[SubAgentType, LLMClientABC] = {}
|
|
36
42
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if not model_name:
|
|
40
|
-
continue
|
|
43
|
+
if skip_sub_agents:
|
|
44
|
+
return LLMClients(main=main_client)
|
|
41
45
|
|
|
42
|
-
|
|
43
|
-
|
|
46
|
+
helper = SubAgentModelHelper(config)
|
|
47
|
+
sub_agent_configs = helper.build_sub_agent_client_configs()
|
|
44
48
|
|
|
45
|
-
|
|
46
|
-
|
|
49
|
+
sub_clients: dict[SubAgentType, LLMClientABC] = {}
|
|
50
|
+
for sub_agent_type, sub_model_name in sub_agent_configs.items():
|
|
51
|
+
sub_llm_config = config.get_model_config(sub_model_name)
|
|
52
|
+
sub_clients[sub_agent_type] = create_llm_client(sub_llm_config)
|
|
47
53
|
|
|
48
54
|
return LLMClients(main=main_client, sub_clients=sub_clients)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
You're a helpful
|
|
1
|
+
You're a helpful assistant with capabilities to generate images and edit images.
|
|
@@ -31,11 +31,12 @@ class SubAgentTool(ToolABC):
|
|
|
31
31
|
@classmethod
|
|
32
32
|
def for_profile(cls, profile: SubAgentProfile) -> type[SubAgentTool]:
|
|
33
33
|
"""Create a tool class for a specific sub-agent profile."""
|
|
34
|
-
|
|
34
|
+
tool_cls = type(
|
|
35
35
|
f"{profile.name}Tool",
|
|
36
36
|
(SubAgentTool,),
|
|
37
37
|
{"_profile": profile},
|
|
38
38
|
)
|
|
39
|
+
return cast(type[SubAgentTool], tool_cls)
|
|
39
40
|
|
|
40
41
|
@classmethod
|
|
41
42
|
def metadata(cls) -> ToolMetadata:
|
|
@@ -16,6 +16,7 @@ from anthropic.types.beta.beta_raw_message_start_event import BetaRawMessageStar
|
|
|
16
16
|
from anthropic.types.beta.beta_signature_delta import BetaSignatureDelta
|
|
17
17
|
from anthropic.types.beta.beta_text_delta import BetaTextDelta
|
|
18
18
|
from anthropic.types.beta.beta_thinking_delta import BetaThinkingDelta
|
|
19
|
+
from anthropic.types.beta.beta_tool_choice_auto_param import BetaToolChoiceAutoParam
|
|
19
20
|
from anthropic.types.beta.beta_tool_use_block import BetaToolUseBlock
|
|
20
21
|
from anthropic.types.beta.message_create_params import MessageCreateParamsStreaming
|
|
21
22
|
|
|
@@ -82,12 +83,14 @@ def build_payload(
|
|
|
82
83
|
# Prepend extra betas, avoiding duplicates
|
|
83
84
|
betas = [b for b in extra_betas if b not in betas] + betas
|
|
84
85
|
|
|
86
|
+
tool_choice: BetaToolChoiceAutoParam = {
|
|
87
|
+
"type": "auto",
|
|
88
|
+
"disable_parallel_tool_use": False,
|
|
89
|
+
}
|
|
90
|
+
|
|
85
91
|
payload: MessageCreateParamsStreaming = {
|
|
86
92
|
"model": str(param.model),
|
|
87
|
-
"tool_choice":
|
|
88
|
-
"type": "auto",
|
|
89
|
-
"disable_parallel_tool_use": False,
|
|
90
|
-
},
|
|
93
|
+
"tool_choice": tool_choice,
|
|
91
94
|
"stream": True,
|
|
92
95
|
"max_tokens": param.max_tokens or DEFAULT_MAX_TOKENS,
|
|
93
96
|
"temperature": param.temperature or DEFAULT_TEMPERATURE,
|