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.
Files changed (52) hide show
  1. klaude_code/app/runtime.py +2 -15
  2. klaude_code/cli/list_model.py +27 -10
  3. klaude_code/cli/main.py +25 -9
  4. klaude_code/config/assets/builtin_config.yaml +25 -16
  5. klaude_code/config/config.py +144 -7
  6. klaude_code/config/select_model.py +38 -13
  7. klaude_code/config/sub_agent_model_helper.py +217 -0
  8. klaude_code/const.py +1 -1
  9. klaude_code/core/agent_profile.py +43 -5
  10. klaude_code/core/executor.py +75 -0
  11. klaude_code/core/manager/llm_clients_builder.py +17 -11
  12. klaude_code/core/prompts/prompt-nano-banana.md +1 -1
  13. klaude_code/core/tool/sub_agent_tool.py +2 -1
  14. klaude_code/llm/anthropic/client.py +7 -4
  15. klaude_code/llm/anthropic/input.py +54 -29
  16. klaude_code/llm/google/client.py +1 -1
  17. klaude_code/llm/google/input.py +23 -2
  18. klaude_code/llm/openai_compatible/input.py +22 -13
  19. klaude_code/llm/openrouter/input.py +37 -25
  20. klaude_code/llm/responses/input.py +96 -57
  21. klaude_code/protocol/commands.py +1 -2
  22. klaude_code/protocol/events/system.py +4 -0
  23. klaude_code/protocol/op.py +17 -0
  24. klaude_code/protocol/op_handler.py +5 -0
  25. klaude_code/protocol/sub_agent/AGENTS.md +28 -0
  26. klaude_code/protocol/sub_agent/__init__.py +10 -14
  27. klaude_code/protocol/sub_agent/image_gen.py +2 -1
  28. klaude_code/session/codec.py +2 -6
  29. klaude_code/session/session.py +9 -1
  30. klaude_code/skill/assets/create-plan/SKILL.md +3 -5
  31. klaude_code/tui/command/__init__.py +3 -6
  32. klaude_code/tui/command/model_cmd.py +6 -43
  33. klaude_code/tui/command/model_select.py +75 -15
  34. klaude_code/tui/command/sub_agent_model_cmd.py +190 -0
  35. klaude_code/tui/components/bash_syntax.py +1 -1
  36. klaude_code/tui/components/common.py +1 -1
  37. klaude_code/tui/components/developer.py +0 -5
  38. klaude_code/tui/components/metadata.py +1 -63
  39. klaude_code/tui/components/rich/cjk_wrap.py +3 -2
  40. klaude_code/tui/components/rich/status.py +49 -3
  41. klaude_code/tui/components/rich/theme.py +2 -0
  42. klaude_code/tui/components/sub_agent.py +25 -46
  43. klaude_code/tui/components/welcome.py +99 -0
  44. klaude_code/tui/input/prompt_toolkit.py +14 -1
  45. klaude_code/tui/renderer.py +2 -3
  46. klaude_code/tui/terminal/selector.py +5 -3
  47. {klaude_code-2.2.0.dist-info → klaude_code-2.3.0.dist-info}/METADATA +1 -1
  48. {klaude_code-2.2.0.dist-info → klaude_code-2.3.0.dist-info}/RECORD +50 -48
  49. klaude_code/tui/command/help_cmd.py +0 -51
  50. klaude_code/tui/command/release_notes_cmd.py +0 -85
  51. {klaude_code-2.2.0.dist-info → klaude_code-2.3.0.dist-info}/WHEEL +0 -0
  52. {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 = 200 # Maximum line length for truncated display
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, sub_agent_tool_names
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
- tool_names.extend(sub_agent_tool_names(enabled_only=True, model_name=model_name))
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()
@@ -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
- for profile in iter_sub_agent_profiles():
38
- model_name = config.sub_agent_models.get(profile.name)
39
- if not model_name:
40
- continue
43
+ if skip_sub_agents:
44
+ return LLMClients(main=main_client)
41
45
 
42
- if not profile.enabled_for_model(main_client.model_name):
43
- continue
46
+ helper = SubAgentModelHelper(config)
47
+ sub_agent_configs = helper.build_sub_agent_client_configs()
44
48
 
45
- sub_llm_config = config.get_model_config(model_name)
46
- sub_clients[profile.name] = create_llm_client(sub_llm_config)
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 art assistant
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
- return type(
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,