klaude-code 2.8.1__py3-none-any.whl → 2.9.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 (93) hide show
  1. klaude_code/app/runtime.py +2 -1
  2. klaude_code/auth/antigravity/oauth.py +0 -9
  3. klaude_code/auth/antigravity/token_manager.py +0 -18
  4. klaude_code/auth/base.py +53 -0
  5. klaude_code/auth/codex/exceptions.py +0 -4
  6. klaude_code/auth/codex/oauth.py +32 -28
  7. klaude_code/auth/codex/token_manager.py +0 -18
  8. klaude_code/cli/cost_cmd.py +128 -39
  9. klaude_code/cli/list_model.py +27 -10
  10. klaude_code/cli/main.py +14 -3
  11. klaude_code/config/assets/builtin_config.yaml +8 -24
  12. klaude_code/config/config.py +47 -25
  13. klaude_code/config/sub_agent_model_helper.py +18 -13
  14. klaude_code/config/thinking.py +0 -8
  15. klaude_code/const.py +1 -1
  16. klaude_code/core/agent_profile.py +10 -52
  17. klaude_code/core/compaction/overflow.py +0 -4
  18. klaude_code/core/executor.py +33 -5
  19. klaude_code/core/manager/llm_clients.py +9 -1
  20. klaude_code/core/prompts/prompt-claude-code.md +4 -4
  21. klaude_code/core/reminders.py +21 -23
  22. klaude_code/core/task.py +0 -4
  23. klaude_code/core/tool/__init__.py +3 -2
  24. klaude_code/core/tool/file/apply_patch.py +0 -27
  25. klaude_code/core/tool/file/read_tool.md +3 -2
  26. klaude_code/core/tool/file/read_tool.py +15 -2
  27. klaude_code/core/tool/offload.py +0 -35
  28. klaude_code/core/tool/sub_agent/__init__.py +6 -0
  29. klaude_code/core/tool/sub_agent/image_gen.md +16 -0
  30. klaude_code/core/tool/sub_agent/image_gen.py +146 -0
  31. klaude_code/core/tool/sub_agent/task.md +20 -0
  32. klaude_code/core/tool/sub_agent/task.py +205 -0
  33. klaude_code/core/tool/tool_registry.py +0 -16
  34. klaude_code/core/turn.py +1 -1
  35. klaude_code/llm/anthropic/input.py +6 -5
  36. klaude_code/llm/antigravity/input.py +14 -7
  37. klaude_code/llm/codex/client.py +22 -0
  38. klaude_code/llm/codex/prompt_sync.py +237 -0
  39. klaude_code/llm/google/client.py +8 -6
  40. klaude_code/llm/google/input.py +20 -12
  41. klaude_code/llm/image.py +18 -11
  42. klaude_code/llm/input_common.py +14 -6
  43. klaude_code/llm/json_stable.py +37 -0
  44. klaude_code/llm/openai_compatible/input.py +0 -10
  45. klaude_code/llm/openai_compatible/stream.py +16 -1
  46. klaude_code/llm/registry.py +0 -5
  47. klaude_code/llm/responses/input.py +15 -5
  48. klaude_code/llm/usage.py +0 -8
  49. klaude_code/protocol/events.py +2 -1
  50. klaude_code/protocol/message.py +2 -2
  51. klaude_code/protocol/model.py +20 -1
  52. klaude_code/protocol/op.py +13 -0
  53. klaude_code/protocol/op_handler.py +5 -0
  54. klaude_code/protocol/sub_agent/AGENTS.md +5 -5
  55. klaude_code/protocol/sub_agent/__init__.py +13 -34
  56. klaude_code/protocol/sub_agent/explore.py +7 -34
  57. klaude_code/protocol/sub_agent/image_gen.py +3 -74
  58. klaude_code/protocol/sub_agent/task.py +3 -47
  59. klaude_code/protocol/sub_agent/web.py +8 -52
  60. klaude_code/protocol/tools.py +2 -0
  61. klaude_code/session/session.py +58 -21
  62. klaude_code/session/store.py +0 -4
  63. klaude_code/skill/assets/deslop/SKILL.md +9 -0
  64. klaude_code/skill/system_skills.py +0 -20
  65. klaude_code/tui/command/fork_session_cmd.py +5 -2
  66. klaude_code/tui/command/resume_cmd.py +9 -2
  67. klaude_code/tui/command/sub_agent_model_cmd.py +85 -18
  68. klaude_code/tui/components/assistant.py +0 -26
  69. klaude_code/tui/components/command_output.py +3 -1
  70. klaude_code/tui/components/developer.py +3 -0
  71. klaude_code/tui/components/diffs.py +2 -208
  72. klaude_code/tui/components/errors.py +4 -0
  73. klaude_code/tui/components/mermaid_viewer.py +2 -2
  74. klaude_code/tui/components/rich/markdown.py +0 -54
  75. klaude_code/tui/components/rich/theme.py +2 -0
  76. klaude_code/tui/components/sub_agent.py +2 -46
  77. klaude_code/tui/components/thinking.py +0 -33
  78. klaude_code/tui/components/tools.py +43 -21
  79. klaude_code/tui/input/images.py +21 -18
  80. klaude_code/tui/input/key_bindings.py +2 -2
  81. klaude_code/tui/input/prompt_toolkit.py +49 -49
  82. klaude_code/tui/machine.py +15 -11
  83. klaude_code/tui/renderer.py +11 -20
  84. klaude_code/tui/runner.py +2 -1
  85. klaude_code/tui/terminal/image.py +6 -34
  86. klaude_code/ui/common.py +0 -70
  87. {klaude_code-2.8.1.dist-info → klaude_code-2.9.0.dist-info}/METADATA +3 -6
  88. {klaude_code-2.8.1.dist-info → klaude_code-2.9.0.dist-info}/RECORD +90 -86
  89. klaude_code/core/tool/sub_agent_tool.py +0 -126
  90. klaude_code/llm/openai_compatible/tool_call_accumulator.py +0 -108
  91. klaude_code/tui/components/rich/searchable_text.py +0 -68
  92. {klaude_code-2.8.1.dist-info → klaude_code-2.9.0.dist-info}/WHEEL +0 -0
  93. {klaude_code-2.8.1.dist-info → klaude_code-2.9.0.dist-info}/entry_points.txt +0 -0
@@ -44,13 +44,6 @@ def parse_env_var_syntax(value: str | None) -> tuple[str | None, str | None]:
44
44
  return None, value
45
45
 
46
46
 
47
- def is_env_var_syntax(value: str | None) -> bool:
48
- """Check if a value uses ${ENV_VAR} syntax."""
49
- if value is None:
50
- return False
51
- return _ENV_VAR_PATTERN.match(value) is not None
52
-
53
-
54
47
  def resolve_api_key(value: str | None) -> str | None:
55
48
  """Resolve an API key value, expanding ${ENV_VAR} syntax if present."""
56
49
  _, resolved = parse_env_var_syntax(value)
@@ -70,6 +63,7 @@ class ModelConfig(llm_param.LLMConfigModelParameter):
70
63
  class ProviderConfig(llm_param.LLMConfigProviderParameter):
71
64
  """Full provider configuration (used in merged config)."""
72
65
 
66
+ disabled: bool = False
73
67
  model_list: list[ModelConfig] = Field(default_factory=lambda: [])
74
68
 
75
69
  def get_resolved_api_key(self) -> str | None:
@@ -141,6 +135,7 @@ class UserProviderConfig(BaseModel):
141
135
 
142
136
  provider_name: str
143
137
  protocol: llm_param.LLMClientProtocol | None = None
138
+ disabled: bool = False
144
139
  base_url: str | None = None
145
140
  api_key: str | None = None
146
141
  is_azure: bool = False
@@ -290,6 +285,9 @@ class Config(BaseModel):
290
285
  if requested_provider is not None and provider.provider_name.casefold() != requested_provider.casefold():
291
286
  continue
292
287
 
288
+ if provider.disabled:
289
+ continue
290
+
293
291
  api_key = provider.get_resolved_api_key()
294
292
  if (
295
293
  provider.protocol
@@ -303,7 +301,11 @@ class Config(BaseModel):
303
301
  ):
304
302
  continue
305
303
 
306
- if any(m.model_name == requested_model for m in provider.model_list):
304
+ for model in provider.model_list:
305
+ if model.model_name != requested_model:
306
+ continue
307
+ if model.disabled:
308
+ continue
307
309
  return requested_model, provider.provider_name
308
310
 
309
311
  return None
@@ -315,6 +317,11 @@ class Config(BaseModel):
315
317
  if requested_provider is not None and provider.provider_name.casefold() != requested_provider.casefold():
316
318
  continue
317
319
 
320
+ if provider.disabled:
321
+ if requested_provider is not None:
322
+ raise ValueError(f"Provider '{provider.provider_name}' is disabled for: {model_name}")
323
+ continue
324
+
318
325
  # Resolve ${ENV_VAR} syntax for api_key
319
326
  api_key = provider.get_resolved_api_key()
320
327
 
@@ -339,7 +346,15 @@ class Config(BaseModel):
339
346
  for model in provider.model_list:
340
347
  if model.model_name != requested_model:
341
348
  continue
342
- provider_dump = provider.model_dump(exclude={"model_list"})
349
+
350
+ if model.disabled:
351
+ if requested_provider is not None:
352
+ raise ValueError(
353
+ f"Model '{requested_model}' is disabled in provider '{provider.provider_name}' for: {model_name}"
354
+ )
355
+ break
356
+
357
+ provider_dump = provider.model_dump(exclude={"model_list", "disabled"})
343
358
  provider_dump["api_key"] = api_key
344
359
  return llm_param.LLMConfigParameter(
345
360
  **provider_dump,
@@ -353,7 +368,7 @@ class Config(BaseModel):
353
368
 
354
369
  Args:
355
370
  only_available: If True, only return models from providers with valid API keys.
356
- include_disabled: If False, exclude models with disabled=True.
371
+ include_disabled: If False, exclude models/providers with disabled=True.
357
372
  """
358
373
  return [
359
374
  ModelEntry(
@@ -362,7 +377,8 @@ class Config(BaseModel):
362
377
  **model.model_dump(exclude={"model_name"}),
363
378
  )
364
379
  for provider in self.provider_list
365
- if not only_available or not provider.is_api_key_missing()
380
+ if include_disabled or not provider.disabled
381
+ if not only_available or (not provider.disabled and not provider.is_api_key_missing())
366
382
  for model in provider.model_list
367
383
  if include_disabled or not model.disabled
368
384
  ]
@@ -374,13 +390,6 @@ class Config(BaseModel):
374
390
  return True
375
391
  return False
376
392
 
377
- def get_first_available_nano_banana_model(self) -> str | None:
378
- """Get the first available nano-banana model, or None."""
379
- for entry in self.iter_model_entries(only_available=True, include_disabled=False):
380
- if "nano-banana" in entry.model_name:
381
- return entry.model_name
382
- return None
383
-
384
393
  def get_first_available_image_model(self) -> str | None:
385
394
  """Get the first available image generation model, or None."""
386
395
  for entry in self.iter_model_entries(only_available=True, include_disabled=False):
@@ -406,7 +415,21 @@ class Config(BaseModel):
406
415
  user_config.theme = self.theme
407
416
  # Note: provider_list is NOT synced - user providers are already in user_config
408
417
 
409
- config_dict = user_config.model_dump(mode="json", exclude_none=True, exclude_defaults=True)
418
+ # Keep the saved file compact (exclude defaults), but preserve explicit
419
+ # overrides inside provider_list (e.g. `disabled: false` to re-enable a
420
+ # builtin provider that is disabled by default).
421
+ config_dict = user_config.model_dump(
422
+ mode="json",
423
+ exclude_none=True,
424
+ exclude_defaults=True,
425
+ exclude={"provider_list"},
426
+ )
427
+
428
+ provider_list = [
429
+ p.model_dump(mode="json", exclude_none=True, exclude_unset=True) for p in (user_config.provider_list or [])
430
+ ]
431
+ if provider_list:
432
+ config_dict["provider_list"] = provider_list
410
433
 
411
434
  def _save_config() -> None:
412
435
  config_path.parent.mkdir(parents=True, exist_ok=True)
@@ -454,12 +477,12 @@ def _get_builtin_config() -> Config:
454
477
  def _merge_model(builtin: ModelConfig, user: ModelConfig) -> ModelConfig:
455
478
  """Merge user model config with builtin model config.
456
479
 
457
- Strategy: user values take precedence if explicitly set (not default).
458
- This allows users to override specific fields (e.g., disabled=true)
480
+ Strategy: user values take precedence if explicitly set (not unset).
481
+ This allows users to override specific fields (e.g., disabled=true/false)
459
482
  without losing other builtin settings (e.g., model_id, max_tokens).
460
483
  """
461
484
  merged_data = builtin.model_dump()
462
- user_data = user.model_dump(exclude_defaults=True, exclude={"model_name"})
485
+ user_data = user.model_dump(exclude_unset=True, exclude={"model_name"})
463
486
  for key, value in user_data.items():
464
487
  if value is not None:
465
488
  merged_data[key] = value
@@ -485,10 +508,9 @@ def _merge_provider(builtin: ProviderConfig, user: UserProviderConfig) -> Provid
485
508
  # New model from user
486
509
  merged_models[m.model_name] = m
487
510
 
488
- # For other fields, use user values if explicitly set, otherwise use builtin
489
- # We check if user explicitly provided a value by comparing to defaults
511
+ # For other fields, use user values if explicitly set, otherwise use builtin.
490
512
  merged_data = builtin.model_dump()
491
- user_data = user.model_dump(exclude_defaults=True, exclude={"model_list"})
513
+ user_data = user.model_dump(exclude_unset=True, exclude={"model_list"})
492
514
 
493
515
  # Update with user's explicit settings
494
516
  for key, value in user_data.items():
@@ -5,13 +5,12 @@ from __future__ import annotations
5
5
  from dataclasses import dataclass
6
6
  from typing import TYPE_CHECKING
7
7
 
8
+ from klaude_code.protocol import tools
8
9
  from klaude_code.protocol.sub_agent import (
9
10
  AVAILABILITY_IMAGE_MODEL,
10
11
  SubAgentProfile,
11
12
  get_sub_agent_profile,
12
- get_sub_agent_profile_by_tool,
13
13
  iter_sub_agent_profiles,
14
- sub_agent_tool_names,
15
14
  )
16
15
  from klaude_code.protocol.tools import SubAgentType
17
16
 
@@ -30,7 +29,7 @@ class SubAgentModelInfo:
30
29
  # Effective model name used by this sub-agent.
31
30
  # - When configured_model is set: equals configured_model.
32
31
  # - When requirement-based default applies (e.g. ImageGen): resolved model.
33
- # - When inheriting from main agent: None.
32
+ # - When inheriting from defaults: resolved model name.
34
33
  effective_model: str | None
35
34
 
36
35
 
@@ -106,10 +105,11 @@ class SubAgentModelHelper:
106
105
  ) -> EmptySubAgentModelBehavior:
107
106
  """Describe what happens when a sub-agent model is not configured.
108
107
 
109
- Most sub-agents default to inheriting the main model.
108
+ Most sub-agents default to the Task model if configured, otherwise
109
+ they inherit the main model.
110
110
 
111
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
112
+ inherit from Task/main; instead they auto-resolve a suitable model
113
113
  (currently: the first available image model).
114
114
  """
115
115
 
@@ -117,9 +117,11 @@ class SubAgentModelHelper:
117
117
 
118
118
  requirement = profile.availability_requirement
119
119
  if requirement is None:
120
+ task_model = self._config.sub_agent_models.get(tools.TASK)
121
+ resolved = task_model or main_model_name
120
122
  return EmptySubAgentModelBehavior(
121
- description=f"inherit from main agent: {main_model_name}",
122
- resolved_model_name=main_model_name,
123
+ description=f"use default behavior: {resolved}",
124
+ resolved_model_name=resolved,
123
125
  )
124
126
 
125
127
  resolved = self.resolve_model_for_requirement(requirement)
@@ -154,13 +156,15 @@ class SubAgentModelHelper:
154
156
  For sub-agents without explicit config, resolves model based on availability_requirement.
155
157
  """
156
158
  result: list[SubAgentModelInfo] = []
157
- for profile in iter_sub_agent_profiles(enabled_only=True):
159
+ for profile in iter_sub_agent_profiles():
158
160
  if not self.check_availability_requirement(profile.availability_requirement):
159
161
  continue
160
162
  configured_model = self._config.sub_agent_models.get(profile.name)
161
163
  effective_model = configured_model
162
164
  if not effective_model and profile.availability_requirement:
163
165
  effective_model = self.resolve_model_for_requirement(profile.availability_requirement)
166
+ if not effective_model and profile.availability_requirement is None:
167
+ effective_model = self._config.sub_agent_models.get(tools.TASK) or self._config.main_model
164
168
  result.append(
165
169
  SubAgentModelInfo(
166
170
  profile=profile,
@@ -189,11 +193,9 @@ class SubAgentModelHelper:
189
193
 
190
194
  def get_enabled_sub_agent_tool_names(self) -> list[str]:
191
195
  """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)
196
+ result: list[str] = [tools.TASK]
197
+ if self.check_availability_requirement(AVAILABILITY_IMAGE_MODEL):
198
+ result.append(tools.IMAGE_GEN)
197
199
  return result
198
200
 
199
201
  def build_sub_agent_client_configs(self) -> dict[SubAgentType, str]:
@@ -205,4 +207,7 @@ class SubAgentModelHelper:
205
207
  model_name = self.resolve_model_for_requirement(profile.availability_requirement)
206
208
  if model_name:
207
209
  result[profile.name] = model_name
210
+ task_model = self._config.sub_agent_models.get(tools.TASK)
211
+ if task_model:
212
+ result.setdefault(tools.TASK, task_model)
208
213
  return result
@@ -62,14 +62,6 @@ def _is_gemini_flash_model(model_name: str | None) -> bool:
62
62
  return "gemini-3-flash" in model_name.lower()
63
63
 
64
64
 
65
- def should_auto_trigger_thinking(model_name: str | None) -> bool:
66
- """Check if model should auto-trigger thinking selection on switch."""
67
- if not model_name:
68
- return False
69
- model_lower = model_name.lower()
70
- return "gpt-5" in model_lower or "gemini-3" in model_lower or "opus" in model_lower
71
-
72
-
73
65
  def get_levels_for_responses(model_name: str | None) -> list[str]:
74
66
  """Get thinking levels for responses protocol."""
75
67
  if _is_codex_max_model(model_name):
klaude_code/const.py CHANGED
@@ -141,7 +141,7 @@ MIN_HIDDEN_LINES_FOR_INDICATOR = 5 # Minimum hidden lines before showing trunca
141
141
  SUB_AGENT_RESULT_MAX_LINES = 10 # Maximum lines for sub-agent result display
142
142
  TRUNCATE_HEAD_MAX_LINES = 2 # Maximum lines for sub-agent error display
143
143
  BASH_OUTPUT_PANEL_THRESHOLD = 10 # Bash output line threshold for CodePanel display
144
- BASH_MULTILINE_STRING_TRUNCATE_MAX_LINES = 2 # Max lines shown for heredoc / multiline string tokens in bash tool calls
144
+ BASH_MULTILINE_STRING_TRUNCATE_MAX_LINES = 4 # Max lines shown for heredoc / multiline string tokens in bash tool calls
145
145
  URL_TRUNCATE_MAX_LENGTH = 400 # Maximum length for URL truncation in display
146
146
  QUERY_DISPLAY_TRUNCATE_LENGTH = 80 # Maximum length for search query display
147
147
  NOTIFY_COMPACT_LIMIT = 160 # Maximum length for notification body text
@@ -12,12 +12,10 @@ from typing import TYPE_CHECKING, Any, Protocol
12
12
  if TYPE_CHECKING:
13
13
  from klaude_code.config.config import Config
14
14
 
15
- from klaude_code.auth.codex.exceptions import CodexUnsupportedModelError
16
15
  from klaude_code.config.sub_agent_model_helper import SubAgentModelHelper
17
16
  from klaude_code.core.reminders import (
18
17
  at_file_reader_reminder,
19
18
  empty_todo_reminder,
20
- file_changed_externally_reminder,
21
19
  image_reminder,
22
20
  last_path_memory_reminder,
23
21
  memory_reminder,
@@ -28,7 +26,7 @@ from klaude_code.core.tool.report_back_tool import ReportBackTool
28
26
  from klaude_code.core.tool.tool_registry import get_tool_schemas
29
27
  from klaude_code.llm import LLMClientABC
30
28
  from klaude_code.protocol import llm_param, message, tools
31
- from klaude_code.protocol.sub_agent import get_sub_agent_profile
29
+ from klaude_code.protocol.sub_agent import AVAILABILITY_IMAGE_MODEL, get_sub_agent_profile
32
30
  from klaude_code.session import Session
33
31
 
34
32
  type Reminder = Callable[[Session], Awaitable[message.DeveloperMessage | None]]
@@ -54,12 +52,6 @@ COMMAND_DESCRIPTIONS: dict[str, str] = {
54
52
  }
55
53
 
56
54
 
57
- # Prompts for codex_oauth protocol - must be used exactly as-is without any additions.
58
- CODEX_OAUTH_PROMPTS: dict[str, str] = {
59
- "gpt-5.2-codex": "prompts/prompt-codex-gpt-5-2-codex.md",
60
- "gpt-5.2": "prompts/prompt-codex-gpt-5-2.md",
61
- }
62
-
63
55
  # Prompt for antigravity protocol - used exactly as-is without any additions.
64
56
  ANTIGRAVITY_PROMPT_PATH = "prompts/prompt-antigravity.md"
65
57
 
@@ -73,20 +65,6 @@ Only the content passed to `report_back` will be returned to user.\
73
65
  """
74
66
 
75
67
 
76
- SUB_AGENT_COMMON_PROMPT_FOR_MAIN_AGENT = """\
77
-
78
- # Sub-agent capabilities
79
- You have sub-agents (e.g. Task, Explore, WebAgent, ImageGen) with structured output and resume capabilites:
80
- - Agents can be provided with an `output_format` (JSON Schema) parameter for structured output
81
- - Example: `output_format={"type": "object", "properties": {"files": {"type": "array", "items": {"type": "string"}, "description": "List of file paths that match the search criteria, e.g. ['src/main.py', 'src/utils/helper.py']"}}, "required": ["files"]}`
82
- - Agents can be resumed using the `resume` parameter by passing the agent ID from a previous invocation. \
83
- When resumed, the agent continues with its full previous context preserved. \
84
- When NOT resuming, each invocation starts fresh and you should provide a detailed task description with all necessary context.
85
- - When the agent is done, it will return a single message back to you along with its agent ID. \
86
- You can use this ID to resume the agent later if needed for follow-up work.
87
- """
88
-
89
-
90
68
  @cache
91
69
  def _load_prompt_by_path(prompt_path: str) -> str:
92
70
  """Load and cache prompt content from a file path relative to core package."""
@@ -144,19 +122,6 @@ def _build_env_info(model_name: str) -> str:
144
122
  return "\n".join(env_lines)
145
123
 
146
124
 
147
- def _has_sub_agents(config: Config | None) -> bool:
148
- """Check if there are any sub-agent tools available for the main agent."""
149
- if config is not None:
150
- from klaude_code.config.sub_agent_model_helper import SubAgentModelHelper
151
-
152
- helper = SubAgentModelHelper(config)
153
- return bool(helper.get_enabled_sub_agent_tool_names())
154
-
155
- from klaude_code.protocol.sub_agent import sub_agent_tool_names
156
-
157
- return bool(sub_agent_tool_names(enabled_only=True))
158
-
159
-
160
125
  def load_system_prompt(
161
126
  model_name: str,
162
127
  protocol: llm_param.LLMClientProtocol,
@@ -165,12 +130,11 @@ def load_system_prompt(
165
130
  ) -> str:
166
131
  """Get system prompt content for the given model and sub-agent type."""
167
132
 
168
- # For codex_oauth protocol, use exact prompts without any additions.
133
+ # For codex_oauth protocol, use dynamic prompts from GitHub (no additions).
169
134
  if protocol == llm_param.LLMClientProtocol.CODEX_OAUTH:
170
- for model_key, prompt_path in CODEX_OAUTH_PROMPTS.items():
171
- if model_key in model_name:
172
- return _load_prompt_by_path(prompt_path)
173
- raise CodexUnsupportedModelError(f"codex_oauth protocol does not support model: {model_name}")
135
+ from klaude_code.llm.codex.prompt_sync import get_codex_instructions
136
+
137
+ return get_codex_instructions(model_name)
174
138
 
175
139
  # For antigravity protocol, use exact prompt without any additions.
176
140
  if protocol == llm_param.LLMClientProtocol.ANTIGRAVITY:
@@ -183,18 +147,13 @@ def load_system_prompt(
183
147
  base_prompt = _load_prompt_by_model(model_name)
184
148
 
185
149
  skills_prompt = ""
186
- sub_agent_prompt = ""
187
150
  if sub_agent_type is None:
188
151
  # Skills are progressive-disclosure: keep only metadata in the system prompt.
189
152
  from klaude_code.skill.manager import format_available_skills_for_system_prompt
190
153
 
191
154
  skills_prompt = format_available_skills_for_system_prompt()
192
155
 
193
- # Add sub-agent resume instructions if there are sub-agent tools available.
194
- if _has_sub_agents(config):
195
- sub_agent_prompt = "\n" + SUB_AGENT_COMMON_PROMPT_FOR_MAIN_AGENT
196
-
197
- return base_prompt + _build_env_info(model_name) + skills_prompt + sub_agent_prompt
156
+ return base_prompt + _build_env_info(model_name) + skills_prompt
198
157
 
199
158
 
200
159
  def load_agent_tools(
@@ -222,13 +181,13 @@ def load_agent_tools(
222
181
  else:
223
182
  tool_names = [tools.BASH, tools.READ, tools.EDIT, tools.WRITE, tools.TODO_WRITE]
224
183
 
184
+ tool_names.append(tools.TASK)
225
185
  if config is not None:
226
186
  helper = SubAgentModelHelper(config)
227
- tool_names.extend(helper.get_enabled_sub_agent_tool_names())
187
+ if helper.check_availability_requirement(AVAILABILITY_IMAGE_MODEL):
188
+ tool_names.append(tools.IMAGE_GEN)
228
189
  else:
229
- from klaude_code.protocol.sub_agent import sub_agent_tool_names
230
-
231
- tool_names.extend(sub_agent_tool_names(enabled_only=True))
190
+ tool_names.append(tools.IMAGE_GEN)
232
191
 
233
192
  tool_names.extend([tools.MERMAID])
234
193
  # tool_names.extend([tools.MEMORY])
@@ -258,7 +217,6 @@ def load_agent_reminders(
258
217
  memory_reminder,
259
218
  at_file_reader_reminder,
260
219
  last_path_memory_reminder,
261
- file_changed_externally_reminder,
262
220
  image_reminder,
263
221
  skill_reminder,
264
222
  ]
@@ -24,7 +24,3 @@ def is_context_overflow(error_message: str | None) -> bool:
24
24
  if _STATUS_CODE_PATTERN.search(error_message):
25
25
  return True
26
26
  return any(pattern.search(error_message) for pattern in _OVERFLOW_PATTERNS)
27
-
28
-
29
- def get_overflow_patterns() -> list[re.Pattern[str]]:
30
- return list(_OVERFLOW_PATTERNS)
@@ -676,6 +676,39 @@ class ExecutorContext:
676
676
  )
677
677
  )
678
678
 
679
+ async def handle_change_compact_model(self, operation: op.ChangeCompactModelOperation) -> None:
680
+ """Handle a change compact model operation."""
681
+ agent = await self._agent_runtime.ensure_agent(operation.session_id)
682
+ config = load_config()
683
+
684
+ model_name = operation.model_name
685
+
686
+ if model_name is None:
687
+ # Clear explicit override and use main client for compaction
688
+ self.llm_clients.compact = None
689
+ agent.compact_llm_client = None
690
+ display_model = "(inherit from main agent)"
691
+ else:
692
+ # Create new client for compaction
693
+ llm_config = config.get_model_config(model_name)
694
+ new_client = create_llm_client(llm_config)
695
+ self.llm_clients.compact = new_client
696
+ agent.compact_llm_client = new_client
697
+ display_model = new_client.model_name
698
+
699
+ if operation.save_as_default:
700
+ config.compact_model = model_name
701
+ await config.save()
702
+
703
+ saved_note = " (saved in ~/.klaude/klaude-config.yaml)" if operation.save_as_default else ""
704
+ await self.emit_event(
705
+ events.CommandOutputEvent(
706
+ session_id=agent.session.id,
707
+ command_name=commands.CommandName.SUB_AGENT_MODEL,
708
+ content=f"Compact model: {display_model}{saved_note}",
709
+ )
710
+ )
711
+
679
712
  async def handle_clear_session(self, operation: op.ClearSessionOperation) -> None:
680
713
  await self._agent_runtime.clear_session(operation.session_id)
681
714
 
@@ -763,11 +796,6 @@ class ExecutorContext:
763
796
  return None
764
797
  return active.task
765
798
 
766
- def has_active_task(self, submission_id: str) -> bool:
767
- """Return True if a task is registered for the submission id."""
768
-
769
- return self.task_manager.get(submission_id) is not None
770
-
771
799
 
772
800
  class Executor:
773
801
  """
@@ -6,6 +6,7 @@ from dataclasses import dataclass
6
6
  from dataclasses import field as dataclass_field
7
7
 
8
8
  from klaude_code.llm.client import LLMClientABC
9
+ from klaude_code.protocol import tools
9
10
  from klaude_code.protocol.tools import SubAgentType
10
11
 
11
12
 
@@ -26,7 +27,14 @@ class LLMClients:
26
27
 
27
28
  if sub_agent_type is None:
28
29
  return self.main
29
- return self.sub_clients.get(sub_agent_type) or self.main
30
+ client = self.sub_clients.get(sub_agent_type)
31
+ if client is not None:
32
+ return client
33
+ if sub_agent_type != tools.TASK:
34
+ fallback = self.sub_clients.get(tools.TASK)
35
+ if fallback is not None:
36
+ return fallback
37
+ return self.main
30
38
 
31
39
  def get_compact_client(self) -> LLMClientABC:
32
40
  """Return compact client if configured, otherwise main client."""
@@ -72,16 +72,16 @@ The user will primarily request you perform software engineering tasks. This inc
72
72
  - Tool results and user messages may include <system-reminder> tags. <system-reminder> tags contain useful information and reminders. They are automatically added by the system, and bear no direct relation to the specific tool results or user messages in which they appear.
73
73
 
74
74
  ## Tool usage policy
75
- - When doing file search, prefer to use the Explore tool in order to reduce context usage.
75
+ - When doing file search, prefer to use the Task tool in order to reduce context usage.
76
76
  - You can call multiple tools in a single response. If you intend to call multiple tools and there are no dependencies between them, make all independent tool calls in parallel. Maximize use of parallel tool calls where possible to increase efficiency. However, if some tool calls depend on previous calls to inform dependent values, do NOT call these tools in parallel and instead call them sequentially. For instance, if one operation must complete before another starts, run these operations sequentially instead. Never use placeholders or guess missing parameters in tool calls.
77
77
  - If the user specifies that they want you to run tools "in parallel", you MUST send a single message with multiple tool use content blocks. For example, if you need to launch multiple agents in parallel, send a single message with multiple Task tool calls.
78
78
  - Use specialized tools instead of bash commands when possible, as this provides a better user experience. For file operations, use dedicated tools: Read for reading files instead of cat/head/tail, Edit for editing instead of sed/awk. NEVER use bash echo or other command-line tools to communicate thoughts, explanations, or instructions to the user. Output all communication directly in your response text instead.
79
- - VERY IMPORTANT: When exploring the codebase to gather context or to answer a question that is not a needle query for a specific file/class/function, it is CRITICAL that you use the Explore tool instead of running search commands directly.
79
+ - VERY IMPORTANT: When exploring the codebase to gather context or to answer a question that is not a needle query for a specific file/class/function, it is CRITICAL that you use the Task tool with subagent_type=Explore instead of running search commands directly.
80
80
  <example>
81
81
  user: Where are errors from the client handled?
82
- assistant: [Uses the Explore tool to find the files that handle client errors instead of using Glob or Grep directly]
82
+ assistant: [Uses the Task tool with subagent_type=Explore to find the files that handle client errors instead of using Glob or Grep directly]
83
83
  </example>
84
84
  <example>
85
85
  user: What is the codebase structure?
86
- assistant: [Uses the Explore tool]
86
+ assistant: [Uses the Task tool with subagent_type=Explore]
87
87
  </example>
@@ -21,20 +21,6 @@ AT_FILE_PATTERN = re.compile(r'(?:(?<!\S)|(?<=\u2192))@("(?P<quoted>[^\"]+)"|(?P
21
21
  SKILL_PATTERN = re.compile(r"(?:^|\s)[$¥](?P<skill>\S+)")
22
22
 
23
23
 
24
- def get_last_new_user_input(session: Session) -> str | None:
25
- """Get last user input & developer message (CLAUDE.md) from conversation history. if there's a tool result after user input, return None"""
26
- result: list[str] = []
27
- for item in reversed(session.conversation_history):
28
- if isinstance(item, message.ToolResultMessage):
29
- return None
30
- if isinstance(item, message.UserMessage):
31
- result.append(message.join_text_parts(item.parts))
32
- break
33
- if isinstance(item, message.DeveloperMessage):
34
- result.append(message.join_text_parts(item.parts))
35
- return "\n\n".join(result)
36
-
37
-
38
24
  @dataclass
39
25
  class AtPatternSource:
40
26
  """Represents an @ pattern with its source file (if from a memory file)."""
@@ -115,6 +101,7 @@ async def _load_at_file_recursive(
115
101
  at_ops: list[model.AtFileOp],
116
102
  formatted_blocks: list[str],
117
103
  collected_images: list[message.ImageURLPart],
104
+ collected_image_paths: list[str],
118
105
  visited: set[str],
119
106
  base_dir: Path | None = None,
120
107
  mentioned_in: str | None = None,
@@ -150,6 +137,7 @@ Result of calling the {tools.READ} tool:
150
137
  at_ops.append(model.AtFileOp(operation="Read", path=path_str, mentioned_in=mentioned_in))
151
138
  if images:
152
139
  collected_images.extend(images)
140
+ collected_image_paths.append(path_str)
153
141
 
154
142
  # Recursively parse @ references from ReadTool output
155
143
  output = tool_result.output_text
@@ -163,6 +151,7 @@ Result of calling the {tools.READ} tool:
163
151
  at_ops,
164
152
  formatted_blocks,
165
153
  collected_images,
154
+ collected_image_paths,
166
155
  visited,
167
156
  base_dir=path.parent,
168
157
  mentioned_in=path_str,
@@ -193,6 +182,7 @@ async def at_file_reader_reminder(
193
182
  at_ops: list[model.AtFileOp] = []
194
183
  formatted_blocks: list[str] = []
195
184
  collected_images: list[message.ImageURLPart] = []
185
+ collected_image_paths: list[str] = []
196
186
  visited: set[str] = set()
197
187
 
198
188
  for source in at_pattern_sources:
@@ -202,6 +192,7 @@ async def at_file_reader_reminder(
202
192
  at_ops,
203
193
  formatted_blocks,
204
194
  collected_images,
195
+ collected_image_paths,
205
196
  visited,
206
197
  mentioned_in=source.mentioned_in,
207
198
  )
@@ -210,12 +201,15 @@ async def at_file_reader_reminder(
210
201
  return None
211
202
 
212
203
  at_files_str = "\n\n".join(formatted_blocks)
204
+ ui_items: list[model.DeveloperUIItem] = [model.AtFileOpsUIItem(ops=at_ops)]
205
+ if collected_image_paths:
206
+ ui_items.append(model.AtFileImagesUIItem(paths=collected_image_paths))
213
207
  return message.DeveloperMessage(
214
208
  parts=message.parts_from_text_and_images(
215
209
  f"""<system-reminder>{at_files_str}\n</system-reminder>""",
216
210
  collected_images or None,
217
211
  ),
218
- ui_extra=model.DeveloperUIExtra(items=[model.AtFileOpsUIItem(ops=at_ops)]),
212
+ ui_extra=model.DeveloperUIExtra(items=ui_items),
219
213
  )
220
214
 
221
215
 
@@ -410,25 +404,29 @@ class Memory(BaseModel):
410
404
  content: str
411
405
 
412
406
 
413
- def get_last_user_message_image_count(session: Session) -> int:
414
- """Get image count from the last user message in conversation history."""
407
+ def get_last_user_message_image_paths(session: Session) -> list[str]:
408
+ """Get image file paths from the last user message in conversation history."""
415
409
  for item in reversed(session.conversation_history):
416
410
  if isinstance(item, message.ToolResultMessage):
417
- return 0
411
+ return []
418
412
  if isinstance(item, message.UserMessage):
419
- return len([part for part in item.parts if isinstance(part, message.ImageURLPart)])
420
- return 0
413
+ paths: list[str] = []
414
+ for part in item.parts:
415
+ if isinstance(part, message.ImageFilePart):
416
+ paths.append(part.file_path)
417
+ return paths
418
+ return []
421
419
 
422
420
 
423
421
  async def image_reminder(session: Session) -> message.DeveloperMessage | None:
424
422
  """Remind agent about images attached by user in the last message."""
425
- image_count = get_last_user_message_image_count(session)
426
- if image_count == 0:
423
+ image_paths = get_last_user_message_image_paths(session)
424
+ if not image_paths:
427
425
  return None
428
426
 
429
427
  return message.DeveloperMessage(
430
428
  parts=[],
431
- ui_extra=model.DeveloperUIExtra(items=[model.UserImagesUIItem(count=image_count)]),
429
+ ui_extra=model.DeveloperUIExtra(items=[model.UserImagesUIItem(count=len(image_paths), paths=image_paths)]),
432
430
  )
433
431
 
434
432
 
klaude_code/core/task.py CHANGED
@@ -179,10 +179,6 @@ class TaskExecutor:
179
179
  self._started_at: float = 0.0
180
180
  self._metadata_accumulator: MetadataAccumulator | None = None
181
181
 
182
- @property
183
- def current_turn(self) -> TurnExecutor | None:
184
- return self._current_turn
185
-
186
182
  def get_partial_metadata(self) -> model.TaskMetadata | None:
187
183
  """Get the currently accumulated metadata without finalizing.
188
184