klaude-code 2.0.2__py3-none-any.whl → 2.1.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 (151) hide show
  1. klaude_code/app/__init__.py +12 -0
  2. klaude_code/app/runtime.py +215 -0
  3. klaude_code/cli/auth_cmd.py +2 -2
  4. klaude_code/cli/config_cmd.py +2 -2
  5. klaude_code/cli/cost_cmd.py +1 -1
  6. klaude_code/cli/debug.py +12 -36
  7. klaude_code/cli/list_model.py +3 -3
  8. klaude_code/cli/main.py +17 -60
  9. klaude_code/cli/self_update.py +2 -187
  10. klaude_code/cli/session_cmd.py +2 -2
  11. klaude_code/config/config.py +1 -1
  12. klaude_code/config/select_model.py +1 -1
  13. klaude_code/const.py +9 -1
  14. klaude_code/core/agent.py +9 -62
  15. klaude_code/core/agent_profile.py +284 -0
  16. klaude_code/core/executor.py +335 -230
  17. klaude_code/core/manager/llm_clients_builder.py +1 -1
  18. klaude_code/core/manager/sub_agent_manager.py +16 -29
  19. klaude_code/core/reminders.py +64 -99
  20. klaude_code/core/task.py +12 -20
  21. klaude_code/core/tool/__init__.py +5 -17
  22. klaude_code/core/tool/context.py +84 -0
  23. klaude_code/core/tool/file/apply_patch_tool.py +18 -21
  24. klaude_code/core/tool/file/edit_tool.py +39 -42
  25. klaude_code/core/tool/file/read_tool.py +14 -9
  26. klaude_code/core/tool/file/write_tool.py +12 -13
  27. klaude_code/core/tool/report_back_tool.py +4 -1
  28. klaude_code/core/tool/shell/bash_tool.py +6 -11
  29. klaude_code/core/tool/skill/skill_tool.py +3 -1
  30. klaude_code/core/tool/sub_agent_tool.py +8 -7
  31. klaude_code/core/tool/todo/todo_write_tool.py +3 -9
  32. klaude_code/core/tool/todo/update_plan_tool.py +3 -5
  33. klaude_code/core/tool/tool_abc.py +2 -1
  34. klaude_code/core/tool/tool_registry.py +2 -33
  35. klaude_code/core/tool/tool_runner.py +13 -10
  36. klaude_code/core/tool/web/mermaid_tool.py +3 -1
  37. klaude_code/core/tool/web/web_fetch_tool.py +5 -3
  38. klaude_code/core/tool/web/web_search_tool.py +5 -3
  39. klaude_code/core/turn.py +86 -26
  40. klaude_code/llm/anthropic/client.py +1 -1
  41. klaude_code/llm/bedrock/client.py +1 -1
  42. klaude_code/llm/claude/client.py +1 -1
  43. klaude_code/llm/codex/client.py +1 -1
  44. klaude_code/llm/google/client.py +1 -1
  45. klaude_code/llm/openai_compatible/client.py +1 -1
  46. klaude_code/llm/openai_compatible/tool_call_accumulator.py +1 -1
  47. klaude_code/llm/openrouter/client.py +1 -1
  48. klaude_code/llm/openrouter/reasoning.py +1 -1
  49. klaude_code/llm/responses/client.py +1 -1
  50. klaude_code/protocol/events/__init__.py +57 -0
  51. klaude_code/protocol/events/base.py +18 -0
  52. klaude_code/protocol/events/chat.py +20 -0
  53. klaude_code/protocol/events/lifecycle.py +22 -0
  54. klaude_code/protocol/events/metadata.py +15 -0
  55. klaude_code/protocol/events/streaming.py +43 -0
  56. klaude_code/protocol/events/system.py +53 -0
  57. klaude_code/protocol/events/tools.py +23 -0
  58. klaude_code/protocol/op.py +5 -0
  59. klaude_code/session/session.py +6 -5
  60. klaude_code/skill/assets/create-plan/SKILL.md +76 -0
  61. klaude_code/skill/loader.py +1 -1
  62. klaude_code/skill/system_skills.py +1 -1
  63. klaude_code/tui/__init__.py +8 -0
  64. klaude_code/{command → tui/command}/clear_cmd.py +2 -1
  65. klaude_code/{command → tui/command}/debug_cmd.py +3 -2
  66. klaude_code/{command → tui/command}/export_cmd.py +2 -1
  67. klaude_code/{command → tui/command}/export_online_cmd.py +2 -1
  68. klaude_code/{command → tui/command}/fork_session_cmd.py +4 -3
  69. klaude_code/{command → tui/command}/help_cmd.py +2 -1
  70. klaude_code/{command → tui/command}/model_cmd.py +4 -3
  71. klaude_code/{command → tui/command}/model_select.py +2 -2
  72. klaude_code/{command → tui/command}/prompt_command.py +4 -3
  73. klaude_code/{command → tui/command}/refresh_cmd.py +3 -1
  74. klaude_code/{command → tui/command}/registry.py +6 -5
  75. klaude_code/{command → tui/command}/release_notes_cmd.py +2 -1
  76. klaude_code/{command → tui/command}/resume_cmd.py +4 -3
  77. klaude_code/{command → tui/command}/status_cmd.py +2 -1
  78. klaude_code/{command → tui/command}/terminal_setup_cmd.py +2 -1
  79. klaude_code/{command → tui/command}/thinking_cmd.py +3 -2
  80. klaude_code/tui/commands.py +164 -0
  81. klaude_code/{ui/renderers → tui/components}/assistant.py +3 -3
  82. klaude_code/{ui/renderers → tui/components}/bash_syntax.py +2 -2
  83. klaude_code/{ui/renderers → tui/components}/common.py +1 -1
  84. klaude_code/{ui/renderers → tui/components}/developer.py +4 -4
  85. klaude_code/{ui/renderers → tui/components}/diffs.py +2 -2
  86. klaude_code/{ui/renderers → tui/components}/errors.py +2 -2
  87. klaude_code/{ui/renderers → tui/components}/metadata.py +7 -7
  88. klaude_code/{ui → tui/components}/rich/markdown.py +9 -23
  89. klaude_code/{ui → tui/components}/rich/status.py +2 -2
  90. klaude_code/{ui → tui/components}/rich/theme.py +3 -1
  91. klaude_code/{ui/renderers → tui/components}/sub_agent.py +23 -43
  92. klaude_code/{ui/renderers → tui/components}/thinking.py +3 -3
  93. klaude_code/{ui/renderers → tui/components}/tools.py +9 -9
  94. klaude_code/{ui/renderers → tui/components}/user_input.py +3 -20
  95. klaude_code/tui/display.py +85 -0
  96. klaude_code/{ui/modes/repl → tui/input}/__init__.py +1 -1
  97. klaude_code/{ui/modes/repl → tui/input}/completers.py +1 -1
  98. klaude_code/{ui/modes/repl/input_prompt_toolkit.py → tui/input/prompt_toolkit.py} +6 -6
  99. klaude_code/tui/machine.py +606 -0
  100. klaude_code/tui/renderer.py +707 -0
  101. klaude_code/tui/runner.py +321 -0
  102. klaude_code/tui/terminal/__init__.py +56 -0
  103. klaude_code/{ui → tui}/terminal/color.py +1 -1
  104. klaude_code/{ui → tui}/terminal/control.py +1 -1
  105. klaude_code/{ui → tui}/terminal/notifier.py +1 -1
  106. klaude_code/ui/__init__.py +6 -50
  107. klaude_code/ui/core/display.py +3 -3
  108. klaude_code/ui/core/input.py +2 -1
  109. klaude_code/ui/{modes/debug/display.py → debug_mode.py} +1 -1
  110. klaude_code/ui/{modes/exec/display.py → exec_mode.py} +0 -2
  111. klaude_code/ui/terminal/__init__.py +6 -54
  112. klaude_code/ui/terminal/title.py +31 -0
  113. klaude_code/update.py +163 -0
  114. {klaude_code-2.0.2.dist-info → klaude_code-2.1.0.dist-info}/METADATA +1 -1
  115. klaude_code-2.1.0.dist-info/RECORD +235 -0
  116. klaude_code/cli/runtime.py +0 -518
  117. klaude_code/core/prompt.py +0 -108
  118. klaude_code/core/tool/tool_context.py +0 -148
  119. klaude_code/protocol/events.py +0 -195
  120. klaude_code/skill/assets/dev-docs/SKILL.md +0 -108
  121. klaude_code/trace/__init__.py +0 -21
  122. klaude_code/ui/core/stage_manager.py +0 -48
  123. klaude_code/ui/modes/__init__.py +0 -1
  124. klaude_code/ui/modes/debug/__init__.py +0 -1
  125. klaude_code/ui/modes/exec/__init__.py +0 -1
  126. klaude_code/ui/modes/repl/display.py +0 -61
  127. klaude_code/ui/modes/repl/event_handler.py +0 -629
  128. klaude_code/ui/modes/repl/renderer.py +0 -464
  129. klaude_code/ui/utils/__init__.py +0 -1
  130. klaude_code-2.0.2.dist-info/RECORD +0 -227
  131. /klaude_code/{trace/log.py → log.py} +0 -0
  132. /klaude_code/{command → tui/command}/__init__.py +0 -0
  133. /klaude_code/{command → tui/command}/command_abc.py +0 -0
  134. /klaude_code/{command → tui/command}/prompt-commit.md +0 -0
  135. /klaude_code/{command → tui/command}/prompt-init.md +0 -0
  136. /klaude_code/{ui/renderers → tui/components}/__init__.py +0 -0
  137. /klaude_code/{ui/renderers → tui/components}/mermaid_viewer.py +0 -0
  138. /klaude_code/{ui → tui/components}/rich/__init__.py +0 -0
  139. /klaude_code/{ui → tui/components}/rich/cjk_wrap.py +0 -0
  140. /klaude_code/{ui → tui/components}/rich/code_panel.py +0 -0
  141. /klaude_code/{ui → tui/components}/rich/live.py +0 -0
  142. /klaude_code/{ui → tui/components}/rich/quote.py +0 -0
  143. /klaude_code/{ui → tui/components}/rich/searchable_text.py +0 -0
  144. /klaude_code/{ui/modes/repl → tui/input}/clipboard.py +0 -0
  145. /klaude_code/{ui/modes/repl → tui/input}/key_bindings.py +0 -0
  146. /klaude_code/{ui → tui}/terminal/image.py +0 -0
  147. /klaude_code/{ui → tui}/terminal/progress_bar.py +0 -0
  148. /klaude_code/{ui → tui}/terminal/selector.py +0 -0
  149. /klaude_code/ui/{utils/common.py → common.py} +0 -0
  150. {klaude_code-2.0.2.dist-info → klaude_code-2.1.0.dist-info}/WHEEL +0 -0
  151. {klaude_code-2.0.2.dist-info → klaude_code-2.1.0.dist-info}/entry_points.txt +0 -0
@@ -1,172 +1,14 @@
1
1
  """Self-update and version utilities for klaude-code."""
2
2
 
3
- from __future__ import annotations
4
-
5
- import json
6
3
  import shutil
7
4
  import subprocess
8
- import threading
9
- import time
10
- import urllib.request
11
5
  from importlib.metadata import PackageNotFoundError
12
6
  from importlib.metadata import version as pkg_version
13
- from typing import NamedTuple
14
7
 
15
8
  import typer
16
9
 
17
- from klaude_code.trace import log
18
-
19
- PACKAGE_NAME = "klaude-code"
20
- PYPI_URL = f"https://pypi.org/pypi/{PACKAGE_NAME}/json"
21
- CHECK_INTERVAL_SECONDS = 3600 # Check at most once per hour
22
-
23
-
24
- class VersionInfo(NamedTuple):
25
- """Version check result."""
26
-
27
- installed: str | None
28
- latest: str | None
29
- update_available: bool
30
-
31
-
32
- # Async check state
33
- _cached_version_info: VersionInfo | None = None
34
- _last_check_time: float = 0.0
35
- _check_lock = threading.Lock()
36
- _check_in_progress = False
37
-
38
-
39
- def _has_uv() -> bool:
40
- """Check if uv command is available."""
41
- return shutil.which("uv") is not None
42
-
43
-
44
- def _get_installed_version() -> str | None:
45
- """Get installed version of klaude-code via uv tool list."""
46
- try:
47
- result = subprocess.run(
48
- ["uv", "tool", "list"],
49
- capture_output=True,
50
- text=True,
51
- timeout=5,
52
- )
53
- if result.returncode != 0:
54
- return None
55
- # Parse output like "klaude-code v0.1.0"
56
- for line in result.stdout.splitlines():
57
- if line.startswith(PACKAGE_NAME):
58
- parts = line.split()
59
- if len(parts) >= 2:
60
- ver = parts[1]
61
- # Remove 'v' prefix if present
62
- if ver.startswith("v"):
63
- ver = ver[1:]
64
- return ver
65
- return None
66
- except (OSError, subprocess.SubprocessError):
67
- return None
68
-
69
-
70
- def _get_latest_version() -> str | None:
71
- """Get latest version from PyPI."""
72
- try:
73
- with urllib.request.urlopen(PYPI_URL, timeout=5) as response:
74
- data = json.loads(response.read().decode())
75
- return data.get("info", {}).get("version")
76
- except (OSError, json.JSONDecodeError, ValueError):
77
- return None
78
-
79
-
80
- def _parse_version(v: str) -> tuple[int, ...]:
81
- """Parse version string into comparable tuple of integers."""
82
- parts: list[int] = []
83
- for part in v.split("."):
84
- # Extract leading digits
85
- digits = ""
86
- for c in part:
87
- if c.isdigit():
88
- digits += c
89
- else:
90
- break
91
- if digits:
92
- parts.append(int(digits))
93
- return tuple(parts)
94
-
95
-
96
- def _compare_versions(installed: str, latest: str) -> bool:
97
- """Return True if latest is newer than installed."""
98
- try:
99
- installed_tuple = _parse_version(installed)
100
- latest_tuple = _parse_version(latest)
101
- return latest_tuple > installed_tuple
102
- except ValueError:
103
- return False
104
-
105
-
106
- def _do_version_check() -> None:
107
- """Perform version check in background thread."""
108
- global _cached_version_info, _last_check_time, _check_in_progress
109
-
110
- try:
111
- installed = _get_installed_version()
112
- latest = _get_latest_version()
113
-
114
- update_available = False
115
- if installed and latest:
116
- update_available = _compare_versions(installed, latest)
117
-
118
- with _check_lock:
119
- _cached_version_info = VersionInfo(
120
- installed=installed,
121
- latest=latest,
122
- update_available=update_available,
123
- )
124
- _last_check_time = time.time()
125
- finally:
126
- with _check_lock:
127
- _check_in_progress = False
128
-
129
-
130
- def check_for_updates() -> VersionInfo | None:
131
- """Check for updates to klaude-code asynchronously.
132
-
133
- Returns cached VersionInfo immediately if available.
134
- Triggers background check if cache is stale or missing.
135
- Returns None if uv is not available or no cached result yet.
136
- """
137
- global _check_in_progress
138
-
139
- if not _has_uv():
140
- return None
141
-
142
- now = time.time()
143
-
144
- with _check_lock:
145
- cache_valid = _cached_version_info is not None and (now - _last_check_time) < CHECK_INTERVAL_SECONDS
146
-
147
- if cache_valid:
148
- return _cached_version_info
149
-
150
- # Start background check if not already in progress
151
- if not _check_in_progress:
152
- _check_in_progress = True
153
- thread = threading.Thread(target=_do_version_check, daemon=True)
154
- thread.start()
155
-
156
- # Return cached result (may be stale) or None if no cache yet
157
- return _cached_version_info
158
-
159
-
160
- def get_update_message() -> str | None:
161
- """Get update message if an update is available.
162
-
163
- Returns immediately with cached result. Triggers async check if needed.
164
- Returns None if no update, uv unavailable, or check not complete yet.
165
- """
166
- info = check_for_updates()
167
- if info is None or not info.update_available:
168
- return None
169
- return f"New version available: {info.latest}. Please run `klaude upgrade` to upgrade."
10
+ from klaude_code.log import log
11
+ from klaude_code.update import PACKAGE_NAME, check_for_updates_blocking
170
12
 
171
13
 
172
14
  def _print_version() -> None:
@@ -243,30 +85,3 @@ def register_self_update_commands(app: typer.Typer) -> None:
243
85
  app.command("update")(update_command)
244
86
  app.command("upgrade", help="Alias for `klaude update`.")(update_command)
245
87
  app.command("version", help="Alias for `klaude --version`.")(version_command)
246
-
247
-
248
- def check_for_updates_blocking() -> VersionInfo | None:
249
- """Check for updates to klaude-code synchronously.
250
-
251
- This is intended for CLI commands (e.g. `klaude update --check`) that need
252
- a deterministic result instead of the async cached behavior.
253
-
254
- Returns:
255
- VersionInfo if uv is available, otherwise None.
256
- """
257
-
258
- if not _has_uv():
259
- return None
260
-
261
- installed = _get_installed_version()
262
- latest = _get_latest_version()
263
-
264
- update_available = False
265
- if installed and latest:
266
- update_available = _compare_versions(installed, latest)
267
-
268
- return VersionInfo(
269
- installed=installed,
270
- latest=latest,
271
- update_available=update_available,
272
- )
@@ -2,8 +2,8 @@ import time
2
2
 
3
3
  import typer
4
4
 
5
+ from klaude_code.log import log
5
6
  from klaude_code.session import Session
6
- from klaude_code.trace import log
7
7
 
8
8
 
9
9
  def _session_confirm(sessions: list[Session.SessionMetaBrief], message: str) -> bool:
@@ -11,7 +11,7 @@ def _session_confirm(sessions: list[Session.SessionMetaBrief], message: str) ->
11
11
 
12
12
  from prompt_toolkit.styles import Style
13
13
 
14
- from klaude_code.ui.terminal.selector import SelectItem, select_one
14
+ from klaude_code.tui.terminal.selector import SelectItem, select_one
15
15
 
16
16
  def _fmt(ts: float) -> str:
17
17
  try:
@@ -13,9 +13,9 @@ from klaude_code.config.builtin_config import (
13
13
  get_builtin_provider_configs,
14
14
  get_builtin_sub_agent_models,
15
15
  )
16
+ from klaude_code.log import log
16
17
  from klaude_code.protocol import llm_param
17
18
  from klaude_code.protocol.sub_agent import iter_sub_agent_profiles
18
- from klaude_code.trace import log
19
19
 
20
20
  # Pattern to match ${ENV_VAR} syntax
21
21
  _ENV_VAR_PATTERN = re.compile(r"^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$")
@@ -1,7 +1,7 @@
1
1
  from dataclasses import dataclass
2
2
 
3
3
  from klaude_code.config.config import ModelEntry, load_config, print_no_available_models_hint
4
- from klaude_code.trace import log
4
+ from klaude_code.log import log
5
5
 
6
6
 
7
7
  def _normalize_model_key(value: str) -> str:
klaude_code/const.py CHANGED
@@ -153,7 +153,15 @@ MARKDOWN_RIGHT_MARGIN = 2 # Right margin (columns) for markdown rendering
153
153
  # =============================================================================
154
154
 
155
155
  STATUS_HINT_TEXT = " (esc to interrupt)" # Status hint text shown after spinner
156
- STATUS_DEFAULT_TEXT = "Thinking …" # Default spinner status text
156
+
157
+ # Spinner status texts
158
+ STATUS_WAITING_TEXT = "Awaiting …"
159
+ STATUS_THINKING_TEXT = "Reasoning …"
160
+ STATUS_COMPOSING_TEXT = "Generating"
161
+
162
+ # Backwards-compatible alias for the default spinner status text.
163
+ STATUS_DEFAULT_TEXT = STATUS_WAITING_TEXT
164
+ SIGINT_DOUBLE_PRESS_EXIT_TEXT = "Press ctrl+c again to exit" # Toast shown on first Ctrl+C during task waits
157
165
  SPINNER_BREATH_PERIOD_SECONDS: float = 2.0 # Spinner breathing animation period (seconds)
158
166
  STATUS_SHIMMER_PADDING = 10 # Horizontal padding for shimmer band position
159
167
  STATUS_SHIMMER_BAND_HALF_WIDTH = 5.0 # Half-width of shimmer band in characters
klaude_code/core/agent.py CHANGED
@@ -1,72 +1,16 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from collections.abc import AsyncGenerator, Iterable
4
- from dataclasses import dataclass
5
- from typing import Protocol
6
4
 
7
- from klaude_code.core.prompt import load_system_prompt
8
- from klaude_code.core.reminders import Reminder, load_agent_reminders
5
+ from klaude_code.core.agent_profile import AgentProfile, Reminder
9
6
  from klaude_code.core.task import SessionContext, TaskExecutionContext, TaskExecutor
10
- from klaude_code.core.tool import build_todo_context, get_registry, load_agent_tools
7
+ from klaude_code.core.tool import build_todo_context, get_registry
8
+ from klaude_code.core.tool.context import RunSubtask
11
9
  from klaude_code.llm import LLMClientABC
12
- from klaude_code.protocol import events, llm_param, tools
10
+ from klaude_code.log import DebugType, log_debug
11
+ from klaude_code.protocol import events
13
12
  from klaude_code.protocol.message import UserInputPayload
14
13
  from klaude_code.session import Session
15
- from klaude_code.trace import DebugType, log_debug
16
-
17
-
18
- @dataclass(frozen=True)
19
- class AgentProfile:
20
- """Encapsulates the active LLM client plus prompts/tools/reminders."""
21
-
22
- llm_client: LLMClientABC
23
- system_prompt: str | None
24
- tools: list[llm_param.ToolSchema]
25
- reminders: list[Reminder]
26
-
27
-
28
- class ModelProfileProvider(Protocol):
29
- """Strategy interface for constructing agent profiles."""
30
-
31
- def build_profile(
32
- self,
33
- llm_client: LLMClientABC,
34
- sub_agent_type: tools.SubAgentType | None = None,
35
- ) -> AgentProfile: ...
36
-
37
-
38
- class DefaultModelProfileProvider(ModelProfileProvider):
39
- """Default provider backed by global prompts/tool/reminder registries."""
40
-
41
- def build_profile(
42
- self,
43
- llm_client: LLMClientABC,
44
- sub_agent_type: tools.SubAgentType | None = None,
45
- ) -> AgentProfile:
46
- model_name = llm_client.model_name
47
- return AgentProfile(
48
- llm_client=llm_client,
49
- system_prompt=load_system_prompt(model_name, llm_client.protocol, sub_agent_type),
50
- tools=load_agent_tools(model_name, sub_agent_type),
51
- reminders=load_agent_reminders(model_name, sub_agent_type),
52
- )
53
-
54
-
55
- class VanillaModelProfileProvider(ModelProfileProvider):
56
- """Provider that strips prompts, reminders, and tools for vanilla mode."""
57
-
58
- def build_profile(
59
- self,
60
- llm_client: LLMClientABC,
61
- sub_agent_type: tools.SubAgentType | None = None,
62
- ) -> AgentProfile:
63
- model_name = llm_client.model_name
64
- return AgentProfile(
65
- llm_client=llm_client,
66
- system_prompt=None,
67
- tools=load_agent_tools(model_name, vanilla=True),
68
- reminders=load_agent_reminders(model_name, vanilla=True),
69
- )
70
14
 
71
15
 
72
16
  class Agent:
@@ -94,13 +38,16 @@ class Agent:
94
38
  debug_type=DebugType.EXECUTION,
95
39
  )
96
40
 
97
- async def run_task(self, user_input: UserInputPayload) -> AsyncGenerator[events.Event]:
41
+ async def run_task(
42
+ self, user_input: UserInputPayload, *, run_subtask: RunSubtask | None = None
43
+ ) -> AsyncGenerator[events.Event]:
98
44
  session_ctx = SessionContext(
99
45
  session_id=self.session.id,
100
46
  get_conversation_history=lambda: self.session.conversation_history,
101
47
  append_history=self.session.append_history,
102
48
  file_tracker=self.session.file_tracker,
103
49
  todo_context=build_todo_context(self.session),
50
+ run_subtask=run_subtask,
104
51
  )
105
52
  context = TaskExecutionContext(
106
53
  session_ctx=session_ctx,
@@ -0,0 +1,284 @@
1
+ from __future__ import annotations
2
+
3
+ import datetime
4
+ import shutil
5
+ from collections.abc import Awaitable, Callable
6
+ from dataclasses import dataclass
7
+ from functools import cache
8
+ from importlib.resources import files
9
+ from pathlib import Path
10
+ from typing import Any, Protocol
11
+
12
+ from klaude_code.core.reminders import (
13
+ at_file_reader_reminder,
14
+ empty_todo_reminder,
15
+ file_changed_externally_reminder,
16
+ image_reminder,
17
+ last_path_memory_reminder,
18
+ memory_reminder,
19
+ skill_reminder,
20
+ todo_not_used_recently_reminder,
21
+ )
22
+ from klaude_code.core.tool.report_back_tool import ReportBackTool
23
+ from klaude_code.core.tool.tool_registry import get_tool_schemas
24
+ from klaude_code.llm import LLMClientABC
25
+ 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
27
+ from klaude_code.session import Session
28
+
29
+ type Reminder = Callable[[Session], Awaitable[message.DeveloperMessage | None]]
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class AgentProfile:
34
+ """Encapsulates the active LLM client plus prompts/tools/reminders."""
35
+
36
+ llm_client: LLMClientABC
37
+ system_prompt: str | None
38
+ tools: list[llm_param.ToolSchema]
39
+ reminders: list[Reminder]
40
+
41
+
42
+ COMMAND_DESCRIPTIONS: dict[str, str] = {
43
+ "rg": "ripgrep - fast text search",
44
+ "fd": "simple and fast alternative to find",
45
+ "tree": "directory listing as a tree",
46
+ "sg": "ast-grep - AST-aware code search",
47
+ "jj": "jujutsu - Git-compatible version control system",
48
+ }
49
+
50
+
51
+ # Mapping from logical prompt keys to resource file paths under the core/prompt directory.
52
+ PROMPT_FILES: dict[str, str] = {
53
+ "main_codex": "prompts/prompt-codex.md",
54
+ "main_gpt_5_1_codex_max": "prompts/prompt-codex-gpt-5-1-codex-max.md",
55
+ "main_gpt_5_2_codex": "prompts/prompt-codex-gpt-5-2-codex.md",
56
+ "main": "prompts/prompt-claude-code.md",
57
+ "main_gemini": "prompts/prompt-gemini.md", # https://ai.google.dev/gemini-api/docs/prompting-strategies?hl=zh-cn#agentic-si-template
58
+ }
59
+
60
+
61
+ STRUCTURED_OUTPUT_PROMPT = """\
62
+
63
+ # Structured Output
64
+ You have a `report_back` tool available. When you complete the task,\
65
+ you MUST call `report_back` with the structured result matching the required schema.\
66
+ Only the content passed to `report_back` will be returned to user.\
67
+ """
68
+
69
+
70
+ @cache
71
+ def _load_prompt_by_path(prompt_path: str) -> str:
72
+ """Load and cache prompt content from a file path relative to core package."""
73
+
74
+ return files(__package__).joinpath(prompt_path).read_text(encoding="utf-8").strip()
75
+
76
+
77
+ def _load_base_prompt(file_key: str) -> str:
78
+ """Load and cache the base prompt content from file."""
79
+
80
+ try:
81
+ prompt_path = PROMPT_FILES[file_key]
82
+ except KeyError as exc:
83
+ raise ValueError(f"Unknown prompt key: {file_key}") from exc
84
+
85
+ return _load_prompt_by_path(prompt_path)
86
+
87
+
88
+ def _get_file_key(model_name: str, protocol: llm_param.LLMClientProtocol) -> str:
89
+ """Determine which prompt file to use based on model."""
90
+
91
+ match model_name:
92
+ case name if "gpt-5.2-codex" in name:
93
+ return "main_gpt_5_2_codex"
94
+ case name if "gpt-5.1-codex-max" in name:
95
+ return "main_gpt_5_1_codex_max"
96
+ case name if "gpt-5" in name:
97
+ return "main_codex"
98
+ case name if "gemini" in name:
99
+ return "main_gemini"
100
+ case _:
101
+ return "main"
102
+
103
+
104
+ def _build_env_info(model_name: str) -> str:
105
+ """Build environment info section with dynamic runtime values."""
106
+
107
+ cwd = Path.cwd()
108
+ today = datetime.datetime.now().strftime("%Y-%m-%d")
109
+ is_git_repo = (cwd / ".git").exists()
110
+ is_empty_dir = not any(cwd.iterdir())
111
+
112
+ available_tools: list[str] = []
113
+ for command, desc in COMMAND_DESCRIPTIONS.items():
114
+ if shutil.which(command) is not None:
115
+ available_tools.append(f"{command}: {desc}")
116
+
117
+ cwd_display = f"{cwd} (empty)" if is_empty_dir else str(cwd)
118
+ env_lines: list[str] = [
119
+ "",
120
+ "",
121
+ "Here is useful information about the environment you are running in:",
122
+ "<env>",
123
+ f"Working directory: {cwd_display}",
124
+ f"Today's Date: {today}",
125
+ f"Is directory a git repo: {is_git_repo}",
126
+ f"You are powered by the model: {model_name}",
127
+ ]
128
+
129
+ if available_tools:
130
+ env_lines.append("Prefer to use the following CLI utilities:")
131
+ for tool in available_tools:
132
+ env_lines.append(f"- {tool}")
133
+
134
+ env_lines.append("</env>")
135
+ return "\n".join(env_lines)
136
+
137
+
138
+ def load_system_prompt(
139
+ model_name: str,
140
+ protocol: llm_param.LLMClientProtocol,
141
+ sub_agent_type: str | None = None,
142
+ ) -> str:
143
+ """Get system prompt content for the given model and sub-agent type."""
144
+
145
+ if sub_agent_type is not None:
146
+ profile = get_sub_agent_profile(sub_agent_type)
147
+ base_prompt = _load_prompt_by_path(profile.prompt_file)
148
+ else:
149
+ file_key = _get_file_key(model_name, protocol)
150
+ base_prompt = _load_base_prompt(file_key)
151
+
152
+ if protocol == llm_param.LLMClientProtocol.CODEX_OAUTH:
153
+ # Do not append environment info for Codex protocol
154
+ return base_prompt
155
+
156
+ return base_prompt + _build_env_info(model_name)
157
+
158
+
159
+ def load_agent_tools(
160
+ model_name: str,
161
+ sub_agent_type: tools.SubAgentType | None = None,
162
+ ) -> list[llm_param.ToolSchema]:
163
+ """Get tools for an agent based on model and agent type.
164
+
165
+ Args:
166
+ model_name: The model name.
167
+ sub_agent_type: If None, returns main agent tools. Otherwise returns sub-agent tools.
168
+ """
169
+
170
+ if sub_agent_type is not None:
171
+ profile = get_sub_agent_profile(sub_agent_type)
172
+ return get_tool_schemas(list(profile.tool_set))
173
+
174
+ # Main agent tools
175
+ if "gpt-5" in model_name:
176
+ tool_names = [tools.BASH, tools.READ, tools.APPLY_PATCH, tools.UPDATE_PLAN]
177
+ elif "gemini-3" in model_name:
178
+ tool_names = [tools.BASH, tools.READ, tools.EDIT, tools.WRITE]
179
+ else:
180
+ tool_names = [tools.BASH, tools.READ, tools.EDIT, tools.WRITE, tools.TODO_WRITE]
181
+
182
+ tool_names.extend(sub_agent_tool_names(enabled_only=True, model_name=model_name))
183
+ tool_names.extend([tools.SKILL, tools.MERMAID])
184
+ # tool_names.extend([tools.MEMORY])
185
+ return get_tool_schemas(tool_names)
186
+
187
+
188
+ def load_agent_reminders(
189
+ model_name: str,
190
+ sub_agent_type: str | None = None,
191
+ ) -> list[Reminder]:
192
+ """Get reminders for an agent based on model and agent type.
193
+
194
+ Args:
195
+ model_name: The model name.
196
+ sub_agent_type: If None, returns main agent reminders. Otherwise returns sub-agent reminders.
197
+ """
198
+
199
+ reminders: list[Reminder] = []
200
+
201
+ # Only main agent (not sub-agent) gets todo reminders, and not for GPT-5
202
+ if sub_agent_type is None and ("gpt-5" not in model_name and "gemini" not in model_name):
203
+ reminders.append(empty_todo_reminder)
204
+ reminders.append(todo_not_used_recently_reminder)
205
+
206
+ reminders.extend(
207
+ [
208
+ memory_reminder,
209
+ at_file_reader_reminder,
210
+ last_path_memory_reminder,
211
+ file_changed_externally_reminder,
212
+ image_reminder,
213
+ skill_reminder,
214
+ ]
215
+ )
216
+
217
+ return reminders
218
+
219
+
220
+ def with_structured_output(profile: AgentProfile, output_schema: dict[str, Any]) -> AgentProfile:
221
+ report_back_tool_class = ReportBackTool.for_schema(output_schema)
222
+ base_prompt = profile.system_prompt or ""
223
+ return AgentProfile(
224
+ llm_client=profile.llm_client,
225
+ system_prompt=base_prompt + STRUCTURED_OUTPUT_PROMPT,
226
+ tools=[*profile.tools, report_back_tool_class.schema()],
227
+ reminders=profile.reminders,
228
+ )
229
+
230
+
231
+ class ModelProfileProvider(Protocol):
232
+ """Strategy interface for constructing agent profiles."""
233
+
234
+ def build_profile(
235
+ self,
236
+ llm_client: LLMClientABC,
237
+ sub_agent_type: tools.SubAgentType | None = None,
238
+ *,
239
+ output_schema: dict[str, Any] | None = None,
240
+ ) -> AgentProfile: ...
241
+
242
+
243
+ class DefaultModelProfileProvider(ModelProfileProvider):
244
+ """Default provider backed by global prompts/tool/reminder registries."""
245
+
246
+ def build_profile(
247
+ self,
248
+ llm_client: LLMClientABC,
249
+ sub_agent_type: tools.SubAgentType | None = None,
250
+ *,
251
+ output_schema: dict[str, Any] | None = None,
252
+ ) -> AgentProfile:
253
+ model_name = llm_client.model_name
254
+ profile = AgentProfile(
255
+ llm_client=llm_client,
256
+ system_prompt=load_system_prompt(model_name, llm_client.protocol, sub_agent_type),
257
+ tools=load_agent_tools(model_name, sub_agent_type),
258
+ reminders=load_agent_reminders(model_name, sub_agent_type),
259
+ )
260
+ if output_schema:
261
+ return with_structured_output(profile, output_schema)
262
+ return profile
263
+
264
+
265
+ class VanillaModelProfileProvider(ModelProfileProvider):
266
+ """Provider that strips prompts, reminders, and tools for vanilla mode."""
267
+
268
+ def build_profile(
269
+ self,
270
+ llm_client: LLMClientABC,
271
+ sub_agent_type: tools.SubAgentType | None = None,
272
+ *,
273
+ output_schema: dict[str, Any] | None = None,
274
+ ) -> AgentProfile:
275
+ del sub_agent_type
276
+ profile = AgentProfile(
277
+ llm_client=llm_client,
278
+ system_prompt=None,
279
+ tools=get_tool_schemas([tools.BASH, tools.EDIT, tools.WRITE, tools.READ]),
280
+ reminders=[],
281
+ )
282
+ if output_schema:
283
+ return with_structured_output(profile, output_schema)
284
+ return profile