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.
- klaude_code/app/runtime.py +2 -1
- klaude_code/auth/antigravity/oauth.py +0 -9
- klaude_code/auth/antigravity/token_manager.py +0 -18
- klaude_code/auth/base.py +53 -0
- klaude_code/auth/codex/exceptions.py +0 -4
- klaude_code/auth/codex/oauth.py +32 -28
- klaude_code/auth/codex/token_manager.py +0 -18
- klaude_code/cli/cost_cmd.py +128 -39
- klaude_code/cli/list_model.py +27 -10
- klaude_code/cli/main.py +14 -3
- klaude_code/config/assets/builtin_config.yaml +8 -24
- klaude_code/config/config.py +47 -25
- klaude_code/config/sub_agent_model_helper.py +18 -13
- klaude_code/config/thinking.py +0 -8
- klaude_code/const.py +1 -1
- klaude_code/core/agent_profile.py +10 -52
- klaude_code/core/compaction/overflow.py +0 -4
- klaude_code/core/executor.py +33 -5
- klaude_code/core/manager/llm_clients.py +9 -1
- klaude_code/core/prompts/prompt-claude-code.md +4 -4
- klaude_code/core/reminders.py +21 -23
- klaude_code/core/task.py +0 -4
- klaude_code/core/tool/__init__.py +3 -2
- klaude_code/core/tool/file/apply_patch.py +0 -27
- klaude_code/core/tool/file/read_tool.md +3 -2
- klaude_code/core/tool/file/read_tool.py +15 -2
- klaude_code/core/tool/offload.py +0 -35
- klaude_code/core/tool/sub_agent/__init__.py +6 -0
- klaude_code/core/tool/sub_agent/image_gen.md +16 -0
- klaude_code/core/tool/sub_agent/image_gen.py +146 -0
- klaude_code/core/tool/sub_agent/task.md +20 -0
- klaude_code/core/tool/sub_agent/task.py +205 -0
- klaude_code/core/tool/tool_registry.py +0 -16
- klaude_code/core/turn.py +1 -1
- klaude_code/llm/anthropic/input.py +6 -5
- klaude_code/llm/antigravity/input.py +14 -7
- klaude_code/llm/codex/client.py +22 -0
- klaude_code/llm/codex/prompt_sync.py +237 -0
- klaude_code/llm/google/client.py +8 -6
- klaude_code/llm/google/input.py +20 -12
- klaude_code/llm/image.py +18 -11
- klaude_code/llm/input_common.py +14 -6
- klaude_code/llm/json_stable.py +37 -0
- klaude_code/llm/openai_compatible/input.py +0 -10
- klaude_code/llm/openai_compatible/stream.py +16 -1
- klaude_code/llm/registry.py +0 -5
- klaude_code/llm/responses/input.py +15 -5
- klaude_code/llm/usage.py +0 -8
- klaude_code/protocol/events.py +2 -1
- klaude_code/protocol/message.py +2 -2
- klaude_code/protocol/model.py +20 -1
- klaude_code/protocol/op.py +13 -0
- klaude_code/protocol/op_handler.py +5 -0
- klaude_code/protocol/sub_agent/AGENTS.md +5 -5
- klaude_code/protocol/sub_agent/__init__.py +13 -34
- klaude_code/protocol/sub_agent/explore.py +7 -34
- klaude_code/protocol/sub_agent/image_gen.py +3 -74
- klaude_code/protocol/sub_agent/task.py +3 -47
- klaude_code/protocol/sub_agent/web.py +8 -52
- klaude_code/protocol/tools.py +2 -0
- klaude_code/session/session.py +58 -21
- klaude_code/session/store.py +0 -4
- klaude_code/skill/assets/deslop/SKILL.md +9 -0
- klaude_code/skill/system_skills.py +0 -20
- klaude_code/tui/command/fork_session_cmd.py +5 -2
- klaude_code/tui/command/resume_cmd.py +9 -2
- klaude_code/tui/command/sub_agent_model_cmd.py +85 -18
- klaude_code/tui/components/assistant.py +0 -26
- klaude_code/tui/components/command_output.py +3 -1
- klaude_code/tui/components/developer.py +3 -0
- klaude_code/tui/components/diffs.py +2 -208
- klaude_code/tui/components/errors.py +4 -0
- klaude_code/tui/components/mermaid_viewer.py +2 -2
- klaude_code/tui/components/rich/markdown.py +0 -54
- klaude_code/tui/components/rich/theme.py +2 -0
- klaude_code/tui/components/sub_agent.py +2 -46
- klaude_code/tui/components/thinking.py +0 -33
- klaude_code/tui/components/tools.py +43 -21
- klaude_code/tui/input/images.py +21 -18
- klaude_code/tui/input/key_bindings.py +2 -2
- klaude_code/tui/input/prompt_toolkit.py +49 -49
- klaude_code/tui/machine.py +15 -11
- klaude_code/tui/renderer.py +11 -20
- klaude_code/tui/runner.py +2 -1
- klaude_code/tui/terminal/image.py +6 -34
- klaude_code/ui/common.py +0 -70
- {klaude_code-2.8.1.dist-info → klaude_code-2.9.0.dist-info}/METADATA +3 -6
- {klaude_code-2.8.1.dist-info → klaude_code-2.9.0.dist-info}/RECORD +90 -86
- klaude_code/core/tool/sub_agent_tool.py +0 -126
- klaude_code/llm/openai_compatible/tool_call_accumulator.py +0 -108
- klaude_code/tui/components/rich/searchable_text.py +0 -68
- {klaude_code-2.8.1.dist-info → klaude_code-2.9.0.dist-info}/WHEEL +0 -0
- {klaude_code-2.8.1.dist-info → klaude_code-2.9.0.dist-info}/entry_points.txt +0 -0
klaude_code/config/config.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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"
|
|
122
|
-
resolved_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(
|
|
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
|
-
|
|
194
|
-
|
|
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
|
klaude_code/config/thinking.py
CHANGED
|
@@ -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 =
|
|
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
|
|
133
|
+
# For codex_oauth protocol, use dynamic prompts from GitHub (no additions).
|
|
169
134
|
if protocol == llm_param.LLMClientProtocol.CODEX_OAUTH:
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
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
|
-
|
|
187
|
+
if helper.check_availability_requirement(AVAILABILITY_IMAGE_MODEL):
|
|
188
|
+
tool_names.append(tools.IMAGE_GEN)
|
|
228
189
|
else:
|
|
229
|
-
|
|
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)
|
klaude_code/core/executor.py
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
86
|
+
assistant: [Uses the Task tool with subagent_type=Explore]
|
|
87
87
|
</example>
|
klaude_code/core/reminders.py
CHANGED
|
@@ -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=
|
|
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
|
|
414
|
-
"""Get image
|
|
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
|
|
411
|
+
return []
|
|
418
412
|
if isinstance(item, message.UserMessage):
|
|
419
|
-
|
|
420
|
-
|
|
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
|
-
|
|
426
|
-
if
|
|
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=
|
|
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
|
|