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.
- klaude_code/app/__init__.py +12 -0
- klaude_code/app/runtime.py +215 -0
- klaude_code/cli/auth_cmd.py +2 -2
- klaude_code/cli/config_cmd.py +2 -2
- klaude_code/cli/cost_cmd.py +1 -1
- klaude_code/cli/debug.py +12 -36
- klaude_code/cli/list_model.py +3 -3
- klaude_code/cli/main.py +17 -60
- klaude_code/cli/self_update.py +2 -187
- klaude_code/cli/session_cmd.py +2 -2
- klaude_code/config/config.py +1 -1
- klaude_code/config/select_model.py +1 -1
- klaude_code/const.py +9 -1
- klaude_code/core/agent.py +9 -62
- klaude_code/core/agent_profile.py +284 -0
- klaude_code/core/executor.py +335 -230
- klaude_code/core/manager/llm_clients_builder.py +1 -1
- klaude_code/core/manager/sub_agent_manager.py +16 -29
- klaude_code/core/reminders.py +64 -99
- klaude_code/core/task.py +12 -20
- klaude_code/core/tool/__init__.py +5 -17
- klaude_code/core/tool/context.py +84 -0
- klaude_code/core/tool/file/apply_patch_tool.py +18 -21
- klaude_code/core/tool/file/edit_tool.py +39 -42
- klaude_code/core/tool/file/read_tool.py +14 -9
- klaude_code/core/tool/file/write_tool.py +12 -13
- klaude_code/core/tool/report_back_tool.py +4 -1
- klaude_code/core/tool/shell/bash_tool.py +6 -11
- klaude_code/core/tool/skill/skill_tool.py +3 -1
- klaude_code/core/tool/sub_agent_tool.py +8 -7
- klaude_code/core/tool/todo/todo_write_tool.py +3 -9
- klaude_code/core/tool/todo/update_plan_tool.py +3 -5
- klaude_code/core/tool/tool_abc.py +2 -1
- klaude_code/core/tool/tool_registry.py +2 -33
- klaude_code/core/tool/tool_runner.py +13 -10
- klaude_code/core/tool/web/mermaid_tool.py +3 -1
- klaude_code/core/tool/web/web_fetch_tool.py +5 -3
- klaude_code/core/tool/web/web_search_tool.py +5 -3
- klaude_code/core/turn.py +86 -26
- klaude_code/llm/anthropic/client.py +1 -1
- klaude_code/llm/bedrock/client.py +1 -1
- klaude_code/llm/claude/client.py +1 -1
- klaude_code/llm/codex/client.py +1 -1
- klaude_code/llm/google/client.py +1 -1
- klaude_code/llm/openai_compatible/client.py +1 -1
- klaude_code/llm/openai_compatible/tool_call_accumulator.py +1 -1
- klaude_code/llm/openrouter/client.py +1 -1
- klaude_code/llm/openrouter/reasoning.py +1 -1
- klaude_code/llm/responses/client.py +1 -1
- klaude_code/protocol/events/__init__.py +57 -0
- klaude_code/protocol/events/base.py +18 -0
- klaude_code/protocol/events/chat.py +20 -0
- klaude_code/protocol/events/lifecycle.py +22 -0
- klaude_code/protocol/events/metadata.py +15 -0
- klaude_code/protocol/events/streaming.py +43 -0
- klaude_code/protocol/events/system.py +53 -0
- klaude_code/protocol/events/tools.py +23 -0
- klaude_code/protocol/op.py +5 -0
- klaude_code/session/session.py +6 -5
- klaude_code/skill/assets/create-plan/SKILL.md +76 -0
- klaude_code/skill/loader.py +1 -1
- klaude_code/skill/system_skills.py +1 -1
- klaude_code/tui/__init__.py +8 -0
- klaude_code/{command → tui/command}/clear_cmd.py +2 -1
- klaude_code/{command → tui/command}/debug_cmd.py +3 -2
- klaude_code/{command → tui/command}/export_cmd.py +2 -1
- klaude_code/{command → tui/command}/export_online_cmd.py +2 -1
- klaude_code/{command → tui/command}/fork_session_cmd.py +4 -3
- klaude_code/{command → tui/command}/help_cmd.py +2 -1
- klaude_code/{command → tui/command}/model_cmd.py +4 -3
- klaude_code/{command → tui/command}/model_select.py +2 -2
- klaude_code/{command → tui/command}/prompt_command.py +4 -3
- klaude_code/{command → tui/command}/refresh_cmd.py +3 -1
- klaude_code/{command → tui/command}/registry.py +6 -5
- klaude_code/{command → tui/command}/release_notes_cmd.py +2 -1
- klaude_code/{command → tui/command}/resume_cmd.py +4 -3
- klaude_code/{command → tui/command}/status_cmd.py +2 -1
- klaude_code/{command → tui/command}/terminal_setup_cmd.py +2 -1
- klaude_code/{command → tui/command}/thinking_cmd.py +3 -2
- klaude_code/tui/commands.py +164 -0
- klaude_code/{ui/renderers → tui/components}/assistant.py +3 -3
- klaude_code/{ui/renderers → tui/components}/bash_syntax.py +2 -2
- klaude_code/{ui/renderers → tui/components}/common.py +1 -1
- klaude_code/{ui/renderers → tui/components}/developer.py +4 -4
- klaude_code/{ui/renderers → tui/components}/diffs.py +2 -2
- klaude_code/{ui/renderers → tui/components}/errors.py +2 -2
- klaude_code/{ui/renderers → tui/components}/metadata.py +7 -7
- klaude_code/{ui → tui/components}/rich/markdown.py +9 -23
- klaude_code/{ui → tui/components}/rich/status.py +2 -2
- klaude_code/{ui → tui/components}/rich/theme.py +3 -1
- klaude_code/{ui/renderers → tui/components}/sub_agent.py +23 -43
- klaude_code/{ui/renderers → tui/components}/thinking.py +3 -3
- klaude_code/{ui/renderers → tui/components}/tools.py +9 -9
- klaude_code/{ui/renderers → tui/components}/user_input.py +3 -20
- klaude_code/tui/display.py +85 -0
- klaude_code/{ui/modes/repl → tui/input}/__init__.py +1 -1
- klaude_code/{ui/modes/repl → tui/input}/completers.py +1 -1
- klaude_code/{ui/modes/repl/input_prompt_toolkit.py → tui/input/prompt_toolkit.py} +6 -6
- klaude_code/tui/machine.py +606 -0
- klaude_code/tui/renderer.py +707 -0
- klaude_code/tui/runner.py +321 -0
- klaude_code/tui/terminal/__init__.py +56 -0
- klaude_code/{ui → tui}/terminal/color.py +1 -1
- klaude_code/{ui → tui}/terminal/control.py +1 -1
- klaude_code/{ui → tui}/terminal/notifier.py +1 -1
- klaude_code/ui/__init__.py +6 -50
- klaude_code/ui/core/display.py +3 -3
- klaude_code/ui/core/input.py +2 -1
- klaude_code/ui/{modes/debug/display.py → debug_mode.py} +1 -1
- klaude_code/ui/{modes/exec/display.py → exec_mode.py} +0 -2
- klaude_code/ui/terminal/__init__.py +6 -54
- klaude_code/ui/terminal/title.py +31 -0
- klaude_code/update.py +163 -0
- {klaude_code-2.0.2.dist-info → klaude_code-2.1.0.dist-info}/METADATA +1 -1
- klaude_code-2.1.0.dist-info/RECORD +235 -0
- klaude_code/cli/runtime.py +0 -518
- klaude_code/core/prompt.py +0 -108
- klaude_code/core/tool/tool_context.py +0 -148
- klaude_code/protocol/events.py +0 -195
- klaude_code/skill/assets/dev-docs/SKILL.md +0 -108
- klaude_code/trace/__init__.py +0 -21
- klaude_code/ui/core/stage_manager.py +0 -48
- klaude_code/ui/modes/__init__.py +0 -1
- klaude_code/ui/modes/debug/__init__.py +0 -1
- klaude_code/ui/modes/exec/__init__.py +0 -1
- klaude_code/ui/modes/repl/display.py +0 -61
- klaude_code/ui/modes/repl/event_handler.py +0 -629
- klaude_code/ui/modes/repl/renderer.py +0 -464
- klaude_code/ui/utils/__init__.py +0 -1
- klaude_code-2.0.2.dist-info/RECORD +0 -227
- /klaude_code/{trace/log.py → log.py} +0 -0
- /klaude_code/{command → tui/command}/__init__.py +0 -0
- /klaude_code/{command → tui/command}/command_abc.py +0 -0
- /klaude_code/{command → tui/command}/prompt-commit.md +0 -0
- /klaude_code/{command → tui/command}/prompt-init.md +0 -0
- /klaude_code/{ui/renderers → tui/components}/__init__.py +0 -0
- /klaude_code/{ui/renderers → tui/components}/mermaid_viewer.py +0 -0
- /klaude_code/{ui → tui/components}/rich/__init__.py +0 -0
- /klaude_code/{ui → tui/components}/rich/cjk_wrap.py +0 -0
- /klaude_code/{ui → tui/components}/rich/code_panel.py +0 -0
- /klaude_code/{ui → tui/components}/rich/live.py +0 -0
- /klaude_code/{ui → tui/components}/rich/quote.py +0 -0
- /klaude_code/{ui → tui/components}/rich/searchable_text.py +0 -0
- /klaude_code/{ui/modes/repl → tui/input}/clipboard.py +0 -0
- /klaude_code/{ui/modes/repl → tui/input}/key_bindings.py +0 -0
- /klaude_code/{ui → tui}/terminal/image.py +0 -0
- /klaude_code/{ui → tui}/terminal/progress_bar.py +0 -0
- /klaude_code/{ui → tui}/terminal/selector.py +0 -0
- /klaude_code/ui/{utils/common.py → common.py} +0 -0
- {klaude_code-2.0.2.dist-info → klaude_code-2.1.0.dist-info}/WHEEL +0 -0
- {klaude_code-2.0.2.dist-info → klaude_code-2.1.0.dist-info}/entry_points.txt +0 -0
klaude_code/cli/self_update.py
CHANGED
|
@@ -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.
|
|
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
|
-
)
|
klaude_code/cli/session_cmd.py
CHANGED
|
@@ -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.
|
|
14
|
+
from klaude_code.tui.terminal.selector import SelectItem, select_one
|
|
15
15
|
|
|
16
16
|
def _fmt(ts: float) -> str:
|
|
17
17
|
try:
|
klaude_code/config/config.py
CHANGED
|
@@ -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_]*)\}$")
|
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
|
-
|
|
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.
|
|
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
|
|
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.
|
|
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(
|
|
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
|