klaude-code 1.2.11__py3-none-any.whl → 1.2.13__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/auth/codex/oauth.py +3 -3
- klaude_code/cli/main.py +5 -5
- klaude_code/cli/runtime.py +19 -27
- klaude_code/cli/session_cmd.py +6 -8
- klaude_code/command/__init__.py +31 -28
- klaude_code/command/clear_cmd.py +0 -2
- klaude_code/command/diff_cmd.py +0 -2
- klaude_code/command/export_cmd.py +3 -5
- klaude_code/command/help_cmd.py +0 -2
- klaude_code/command/model_cmd.py +0 -2
- klaude_code/command/refresh_cmd.py +0 -2
- klaude_code/command/registry.py +5 -9
- klaude_code/command/release_notes_cmd.py +0 -2
- klaude_code/command/status_cmd.py +2 -4
- klaude_code/command/terminal_setup_cmd.py +2 -4
- klaude_code/command/thinking_cmd.py +229 -0
- klaude_code/config/__init__.py +1 -1
- klaude_code/config/list_model.py +1 -1
- klaude_code/config/select_model.py +5 -15
- klaude_code/const/__init__.py +1 -1
- klaude_code/core/agent.py +14 -69
- klaude_code/core/executor.py +11 -10
- klaude_code/core/manager/agent_manager.py +4 -4
- klaude_code/core/manager/llm_clients.py +10 -49
- klaude_code/core/manager/llm_clients_builder.py +8 -21
- klaude_code/core/manager/sub_agent_manager.py +3 -3
- klaude_code/core/prompt.py +3 -3
- klaude_code/core/reminders.py +1 -1
- klaude_code/core/task.py +4 -5
- klaude_code/core/tool/__init__.py +16 -25
- klaude_code/core/tool/file/_utils.py +1 -1
- klaude_code/core/tool/file/apply_patch.py +17 -25
- klaude_code/core/tool/file/apply_patch_tool.py +4 -7
- klaude_code/core/tool/file/edit_tool.py +4 -11
- klaude_code/core/tool/file/multi_edit_tool.py +2 -3
- klaude_code/core/tool/file/read_tool.py +3 -4
- klaude_code/core/tool/file/write_tool.py +2 -3
- klaude_code/core/tool/memory/memory_tool.py +2 -8
- klaude_code/core/tool/memory/skill_loader.py +3 -2
- klaude_code/core/tool/shell/command_safety.py +0 -1
- klaude_code/core/tool/tool_context.py +1 -3
- klaude_code/core/tool/tool_registry.py +2 -1
- klaude_code/core/tool/tool_runner.py +1 -1
- klaude_code/core/tool/truncation.py +2 -5
- klaude_code/core/turn.py +9 -4
- klaude_code/llm/anthropic/client.py +62 -49
- klaude_code/llm/client.py +2 -20
- klaude_code/llm/codex/client.py +51 -32
- klaude_code/llm/input_common.py +2 -2
- klaude_code/llm/openai_compatible/client.py +60 -39
- klaude_code/llm/openai_compatible/stream_processor.py +2 -1
- klaude_code/llm/openrouter/client.py +79 -45
- klaude_code/llm/openrouter/reasoning_handler.py +19 -132
- klaude_code/llm/registry.py +6 -5
- klaude_code/llm/responses/client.py +65 -43
- klaude_code/llm/usage.py +1 -49
- klaude_code/protocol/commands.py +1 -0
- klaude_code/protocol/events.py +7 -0
- klaude_code/protocol/llm_param.py +1 -9
- klaude_code/protocol/model.py +10 -6
- klaude_code/protocol/sub_agent.py +2 -1
- klaude_code/session/export.py +1 -8
- klaude_code/session/selector.py +12 -7
- klaude_code/session/session.py +2 -4
- klaude_code/trace/__init__.py +1 -1
- klaude_code/trace/log.py +1 -1
- klaude_code/ui/__init__.py +4 -9
- klaude_code/ui/core/stage_manager.py +7 -4
- klaude_code/ui/modes/repl/__init__.py +1 -1
- klaude_code/ui/modes/repl/completers.py +6 -7
- klaude_code/ui/modes/repl/display.py +3 -4
- klaude_code/ui/modes/repl/event_handler.py +63 -5
- klaude_code/ui/modes/repl/key_bindings.py +2 -3
- klaude_code/ui/modes/repl/renderer.py +2 -1
- klaude_code/ui/renderers/diffs.py +1 -4
- klaude_code/ui/renderers/metadata.py +1 -12
- klaude_code/ui/rich/markdown.py +3 -3
- klaude_code/ui/rich/searchable_text.py +6 -6
- klaude_code/ui/rich/status.py +3 -4
- klaude_code/ui/rich/theme.py +1 -4
- klaude_code/ui/terminal/control.py +7 -16
- klaude_code/ui/terminal/notifier.py +2 -4
- klaude_code/ui/utils/common.py +1 -1
- klaude_code/ui/utils/debouncer.py +2 -2
- {klaude_code-1.2.11.dist-info → klaude_code-1.2.13.dist-info}/METADATA +1 -1
- {klaude_code-1.2.11.dist-info → klaude_code-1.2.13.dist-info}/RECORD +88 -87
- {klaude_code-1.2.11.dist-info → klaude_code-1.2.13.dist-info}/WHEEL +0 -0
- {klaude_code-1.2.11.dist-info → klaude_code-1.2.13.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
import questionary
|
|
5
|
+
|
|
6
|
+
from klaude_code.command.command_abc import CommandABC, CommandResult
|
|
7
|
+
from klaude_code.protocol import commands, events, llm_param, model
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from klaude_code.core.agent import Agent
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# Thinking level options for different protocols
|
|
14
|
+
RESPONSES_LEVELS = ["minimal", "low", "medium", "high"]
|
|
15
|
+
RESPONSES_GPT51_LEVELS = ["none", "minimal", "low", "medium", "high"]
|
|
16
|
+
RESPONSES_CODEX_MAX_LEVELS = ["medium", "high", "xhigh"]
|
|
17
|
+
|
|
18
|
+
ANTHROPIC_LEVELS: list[tuple[str, int | None]] = [
|
|
19
|
+
("off", 0),
|
|
20
|
+
("low (2048 tokens)", 2048),
|
|
21
|
+
("medium (8192 tokens)", 8192),
|
|
22
|
+
("high (31999 tokens)", 31999),
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _is_openrouter_model_with_reasoning_effort(model_name: str | None) -> bool:
|
|
27
|
+
"""Check if the model is GPT series, Grok or Gemini 3."""
|
|
28
|
+
if not model_name:
|
|
29
|
+
return False
|
|
30
|
+
model_lower = model_name.lower()
|
|
31
|
+
return model_lower.startswith(("openai/gpt-", "x-ai/grok-", "google/gemini-3"))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _is_gpt51_model(model_name: str | None) -> bool:
|
|
35
|
+
"""Check if the model is GPT-5.1."""
|
|
36
|
+
if not model_name:
|
|
37
|
+
return False
|
|
38
|
+
return model_name.lower() in ["gpt5.1", "openai/gpt-5.1", "gpt-5.1-codex-2025-11-13"]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _is_codex_max_model(model_name: str | None) -> bool:
|
|
42
|
+
"""Check if the model is GPT-5.1-codex-max."""
|
|
43
|
+
if not model_name:
|
|
44
|
+
return False
|
|
45
|
+
return "codex-max" in model_name.lower()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _get_levels_for_responses(model_name: str | None) -> list[str]:
|
|
49
|
+
"""Get thinking levels for responses protocol."""
|
|
50
|
+
if _is_codex_max_model(model_name):
|
|
51
|
+
return RESPONSES_CODEX_MAX_LEVELS
|
|
52
|
+
if _is_gpt51_model(model_name):
|
|
53
|
+
return RESPONSES_GPT51_LEVELS
|
|
54
|
+
return RESPONSES_LEVELS
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _format_current_thinking(config: llm_param.LLMConfigParameter) -> str:
|
|
58
|
+
"""Format the current thinking configuration for display."""
|
|
59
|
+
thinking = config.thinking
|
|
60
|
+
if not thinking:
|
|
61
|
+
return "not configured"
|
|
62
|
+
|
|
63
|
+
protocol = config.protocol
|
|
64
|
+
|
|
65
|
+
if protocol in (llm_param.LLMClientProtocol.RESPONSES, llm_param.LLMClientProtocol.CODEX):
|
|
66
|
+
if thinking.reasoning_effort:
|
|
67
|
+
return f"reasoning_effort={thinking.reasoning_effort}"
|
|
68
|
+
return "not set"
|
|
69
|
+
|
|
70
|
+
if protocol == llm_param.LLMClientProtocol.ANTHROPIC:
|
|
71
|
+
if thinking.type == "disabled":
|
|
72
|
+
return "off"
|
|
73
|
+
if thinking.type == "enabled":
|
|
74
|
+
return f"enabled (budget_tokens={thinking.budget_tokens})"
|
|
75
|
+
return "not set"
|
|
76
|
+
|
|
77
|
+
if protocol == llm_param.LLMClientProtocol.OPENROUTER:
|
|
78
|
+
if _is_openrouter_model_with_reasoning_effort(config.model):
|
|
79
|
+
if thinking.reasoning_effort:
|
|
80
|
+
return f"reasoning_effort={thinking.reasoning_effort}"
|
|
81
|
+
else:
|
|
82
|
+
if thinking.type == "disabled":
|
|
83
|
+
return "off"
|
|
84
|
+
if thinking.type == "enabled":
|
|
85
|
+
return f"enabled (budget_tokens={thinking.budget_tokens})"
|
|
86
|
+
return "not set"
|
|
87
|
+
|
|
88
|
+
if protocol == llm_param.LLMClientProtocol.OPENAI:
|
|
89
|
+
if thinking.type == "disabled":
|
|
90
|
+
return "off"
|
|
91
|
+
if thinking.type == "enabled":
|
|
92
|
+
return f"enabled (budget_tokens={thinking.budget_tokens})"
|
|
93
|
+
return "not set"
|
|
94
|
+
|
|
95
|
+
return "unknown protocol"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
SELECT_STYLE = questionary.Style(
|
|
99
|
+
[
|
|
100
|
+
("instruction", "ansibrightblack"),
|
|
101
|
+
("pointer", "ansicyan"),
|
|
102
|
+
("highlighted", "ansicyan"),
|
|
103
|
+
("text", "ansibrightblack"),
|
|
104
|
+
]
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _select_responses_thinking_sync(model_name: str | None) -> llm_param.Thinking | None:
|
|
109
|
+
"""Select thinking level for responses/codex protocol (sync version)."""
|
|
110
|
+
levels = _get_levels_for_responses(model_name)
|
|
111
|
+
choices: list[questionary.Choice] = [questionary.Choice(title=level, value=level) for level in levels]
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
result = questionary.select(
|
|
115
|
+
message="Select reasoning effort:",
|
|
116
|
+
choices=choices,
|
|
117
|
+
pointer="→",
|
|
118
|
+
instruction="Use arrow keys to move, Enter to select",
|
|
119
|
+
use_jk_keys=False,
|
|
120
|
+
style=SELECT_STYLE,
|
|
121
|
+
).ask()
|
|
122
|
+
|
|
123
|
+
if result is None:
|
|
124
|
+
return None
|
|
125
|
+
return llm_param.Thinking(reasoning_effort=result)
|
|
126
|
+
except KeyboardInterrupt:
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _select_anthropic_thinking_sync() -> llm_param.Thinking | None:
|
|
131
|
+
"""Select thinking level for anthropic/openai_compatible protocol (sync version)."""
|
|
132
|
+
choices: list[questionary.Choice] = [
|
|
133
|
+
questionary.Choice(title=label, value=tokens) for label, tokens in ANTHROPIC_LEVELS
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
result = questionary.select(
|
|
138
|
+
message="Select thinking level:",
|
|
139
|
+
choices=choices,
|
|
140
|
+
pointer="→",
|
|
141
|
+
instruction="Use arrow keys to move, Enter to select",
|
|
142
|
+
use_jk_keys=False,
|
|
143
|
+
style=SELECT_STYLE,
|
|
144
|
+
).ask()
|
|
145
|
+
if result is None:
|
|
146
|
+
return llm_param.Thinking(type="disabled", budget_tokens=0)
|
|
147
|
+
return llm_param.Thinking(type="enabled", budget_tokens=result or 0)
|
|
148
|
+
except KeyboardInterrupt:
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class ThinkingCommand(CommandABC):
|
|
153
|
+
"""Configure model thinking/reasoning level."""
|
|
154
|
+
|
|
155
|
+
@property
|
|
156
|
+
def name(self) -> commands.CommandName:
|
|
157
|
+
return commands.CommandName.THINKING
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def summary(self) -> str:
|
|
161
|
+
return "Configure model thinking/reasoning level"
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
def is_interactive(self) -> bool:
|
|
165
|
+
return True
|
|
166
|
+
|
|
167
|
+
async def run(self, raw: str, agent: "Agent") -> CommandResult:
|
|
168
|
+
if not agent.profile:
|
|
169
|
+
return self._no_change_result(agent, "No profile configured")
|
|
170
|
+
|
|
171
|
+
config = agent.profile.llm_client.get_llm_config()
|
|
172
|
+
protocol = config.protocol
|
|
173
|
+
model_name = config.model
|
|
174
|
+
|
|
175
|
+
current = _format_current_thinking(config)
|
|
176
|
+
|
|
177
|
+
# Select new thinking configuration based on protocol
|
|
178
|
+
new_thinking: llm_param.Thinking | None = None
|
|
179
|
+
|
|
180
|
+
if protocol in (llm_param.LLMClientProtocol.RESPONSES, llm_param.LLMClientProtocol.CODEX):
|
|
181
|
+
new_thinking = await asyncio.to_thread(_select_responses_thinking_sync, model_name)
|
|
182
|
+
|
|
183
|
+
elif protocol == llm_param.LLMClientProtocol.ANTHROPIC:
|
|
184
|
+
new_thinking = await asyncio.to_thread(_select_anthropic_thinking_sync)
|
|
185
|
+
|
|
186
|
+
elif protocol == llm_param.LLMClientProtocol.OPENROUTER:
|
|
187
|
+
if _is_openrouter_model_with_reasoning_effort(model_name):
|
|
188
|
+
new_thinking = await asyncio.to_thread(_select_responses_thinking_sync, model_name)
|
|
189
|
+
else:
|
|
190
|
+
new_thinking = await asyncio.to_thread(_select_anthropic_thinking_sync)
|
|
191
|
+
|
|
192
|
+
elif protocol == llm_param.LLMClientProtocol.OPENAI:
|
|
193
|
+
# openai_compatible uses anthropic style
|
|
194
|
+
new_thinking = await asyncio.to_thread(_select_anthropic_thinking_sync)
|
|
195
|
+
|
|
196
|
+
else:
|
|
197
|
+
return self._no_change_result(agent, f"Unsupported protocol: {protocol}")
|
|
198
|
+
|
|
199
|
+
if new_thinking is None:
|
|
200
|
+
return self._no_change_result(agent, "(no change)")
|
|
201
|
+
|
|
202
|
+
# Apply the new thinking configuration
|
|
203
|
+
config.thinking = new_thinking
|
|
204
|
+
new_status = _format_current_thinking(config)
|
|
205
|
+
|
|
206
|
+
return CommandResult(
|
|
207
|
+
events=[
|
|
208
|
+
events.DeveloperMessageEvent(
|
|
209
|
+
session_id=agent.session.id,
|
|
210
|
+
item=model.DeveloperMessageItem(
|
|
211
|
+
content=f"Thinking changed: {current} -> {new_status}",
|
|
212
|
+
command_output=model.CommandOutput(command_name=self.name),
|
|
213
|
+
),
|
|
214
|
+
)
|
|
215
|
+
]
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
def _no_change_result(self, agent: "Agent", message: str) -> CommandResult:
|
|
219
|
+
return CommandResult(
|
|
220
|
+
events=[
|
|
221
|
+
events.DeveloperMessageEvent(
|
|
222
|
+
session_id=agent.session.id,
|
|
223
|
+
item=model.DeveloperMessageItem(
|
|
224
|
+
content=message,
|
|
225
|
+
command_output=model.CommandOutput(command_name=self.name),
|
|
226
|
+
),
|
|
227
|
+
)
|
|
228
|
+
]
|
|
229
|
+
)
|
klaude_code/config/__init__.py
CHANGED
klaude_code/config/list_model.py
CHANGED
|
@@ -34,7 +34,7 @@ def _display_codex_status(console: Console) -> None:
|
|
|
34
34
|
)
|
|
35
35
|
)
|
|
36
36
|
else:
|
|
37
|
-
expires_dt = datetime.datetime.fromtimestamp(state.expires_at, tz=datetime.
|
|
37
|
+
expires_dt = datetime.datetime.fromtimestamp(state.expires_at, tz=datetime.UTC)
|
|
38
38
|
console.print(
|
|
39
39
|
Text.assemble(
|
|
40
40
|
("Codex Status: ", "bold"),
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from klaude_code.config.config import load_config
|
|
2
2
|
from klaude_code.trace import log
|
|
3
|
-
from klaude_code.ui.rich.searchable_text import SearchableFormattedList
|
|
4
3
|
|
|
5
4
|
|
|
6
5
|
def select_model_from_config(preferred: str | None = None) -> str | None:
|
|
@@ -16,9 +15,6 @@ def select_model_from_config(preferred: str | None = None) -> str | None:
|
|
|
16
15
|
raise ValueError("No models configured. Please update your config.yaml")
|
|
17
16
|
|
|
18
17
|
names: list[str] = [m.model_name for m in models]
|
|
19
|
-
default_name: str | None = (
|
|
20
|
-
preferred if preferred in names else (config.main_model if config.main_model in names else None)
|
|
21
|
-
)
|
|
22
18
|
|
|
23
19
|
try:
|
|
24
20
|
import questionary
|
|
@@ -28,29 +24,23 @@ def select_model_from_config(preferred: str | None = None) -> str | None:
|
|
|
28
24
|
max_model_name_length = max(len(m.model_name) for m in models)
|
|
29
25
|
for m in models:
|
|
30
26
|
star = "★ " if m.model_name == config.main_model else " "
|
|
31
|
-
|
|
32
|
-
("class:t", f"{star}{m.model_name:<{max_model_name_length}} → "),
|
|
33
|
-
("class:b", m.model_params.model or "N/A"),
|
|
34
|
-
("class:d", f" {m.provider}"),
|
|
35
|
-
]
|
|
36
|
-
# Provide a formatted title for display and a plain text for search.
|
|
37
|
-
title = SearchableFormattedList(fragments)
|
|
27
|
+
title = f"{star}{m.model_name:<{max_model_name_length}} → {m.model_params.model or 'N/A'} @ {m.provider}"
|
|
38
28
|
choices.append(questionary.Choice(title=title, value=m.model_name))
|
|
39
29
|
|
|
40
30
|
try:
|
|
41
31
|
result = questionary.select(
|
|
42
32
|
message="Select a model:",
|
|
43
33
|
choices=choices,
|
|
44
|
-
default=default_name,
|
|
45
34
|
pointer="→",
|
|
46
35
|
instruction="↑↓ to move • Enter to select",
|
|
47
36
|
use_jk_keys=False,
|
|
48
37
|
use_search_filter=True,
|
|
49
38
|
style=questionary.Style(
|
|
50
39
|
[
|
|
51
|
-
("
|
|
52
|
-
("
|
|
53
|
-
("
|
|
40
|
+
("instruction", "ansibrightblack"),
|
|
41
|
+
("pointer", "ansicyan"),
|
|
42
|
+
("highlighted", "ansicyan"),
|
|
43
|
+
("text", "ansibrightblack"),
|
|
54
44
|
# search filter colors at the bottom
|
|
55
45
|
("search_success", "noinherit fg:ansigreen"),
|
|
56
46
|
("search_none", "noinherit fg:ansired"),
|
klaude_code/const/__init__.py
CHANGED
|
@@ -62,7 +62,7 @@ BASH_DEFAULT_TIMEOUT_MS = 120000
|
|
|
62
62
|
|
|
63
63
|
# -- Tool Output --
|
|
64
64
|
# Maximum length for tool output before truncation
|
|
65
|
-
TOOL_OUTPUT_MAX_LENGTH =
|
|
65
|
+
TOOL_OUTPUT_MAX_LENGTH = 40000
|
|
66
66
|
|
|
67
67
|
# Characters to show from the beginning of truncated output
|
|
68
68
|
TOOL_OUTPUT_DISPLAY_HEAD = 10000
|
klaude_code/core/agent.py
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from collections.abc import AsyncGenerator,
|
|
3
|
+
from collections.abc import AsyncGenerator, Iterable
|
|
4
4
|
from dataclasses import dataclass
|
|
5
|
-
from typing import
|
|
5
|
+
from typing import Protocol
|
|
6
6
|
|
|
7
|
-
from klaude_code.core.prompt import
|
|
7
|
+
from klaude_code.core.prompt import load_system_prompt
|
|
8
8
|
from klaude_code.core.reminders import Reminder, load_agent_reminders
|
|
9
9
|
from klaude_code.core.task import SessionContext, TaskExecutionContext, TaskExecutor
|
|
10
10
|
from klaude_code.core.tool import build_todo_context, get_registry, load_agent_tools
|
|
@@ -14,38 +14,21 @@ from klaude_code.protocol.model import UserInputPayload
|
|
|
14
14
|
from klaude_code.session import Session
|
|
15
15
|
from klaude_code.trace import DebugType, log_debug
|
|
16
16
|
|
|
17
|
-
if TYPE_CHECKING:
|
|
18
|
-
from klaude_code.core.manager.llm_clients import LLMClients
|
|
19
|
-
|
|
20
17
|
|
|
21
18
|
@dataclass(frozen=True)
|
|
22
19
|
class AgentProfile:
|
|
23
20
|
"""Encapsulates the active LLM client plus prompts/tools/reminders."""
|
|
24
21
|
|
|
25
|
-
|
|
22
|
+
llm_client: LLMClientABC
|
|
26
23
|
system_prompt: str | None
|
|
27
24
|
tools: list[llm_param.ToolSchema]
|
|
28
25
|
reminders: list[Reminder]
|
|
29
26
|
|
|
30
|
-
_llm_client: LLMClientABC | None = None
|
|
31
|
-
|
|
32
|
-
@property
|
|
33
|
-
def llm_client(self) -> LLMClientABC:
|
|
34
|
-
if self._llm_client is None:
|
|
35
|
-
object.__setattr__(self, "_llm_client", self.llm_client_factory())
|
|
36
|
-
return self._llm_client # type: ignore[return-value]
|
|
37
|
-
|
|
38
27
|
|
|
39
28
|
class ModelProfileProvider(Protocol):
|
|
40
29
|
"""Strategy interface for constructing agent profiles."""
|
|
41
30
|
|
|
42
31
|
def build_profile(
|
|
43
|
-
self,
|
|
44
|
-
llm_clients: LLMClients,
|
|
45
|
-
sub_agent_type: tools.SubAgentType | None = None,
|
|
46
|
-
) -> AgentProfile: ...
|
|
47
|
-
|
|
48
|
-
def build_profile_eager(
|
|
49
32
|
self,
|
|
50
33
|
llm_client: LLMClientABC,
|
|
51
34
|
sub_agent_type: tools.SubAgentType | None = None,
|
|
@@ -56,26 +39,13 @@ class DefaultModelProfileProvider(ModelProfileProvider):
|
|
|
56
39
|
"""Default provider backed by global prompts/tool/reminder registries."""
|
|
57
40
|
|
|
58
41
|
def build_profile(
|
|
59
|
-
self,
|
|
60
|
-
llm_clients: LLMClients,
|
|
61
|
-
sub_agent_type: tools.SubAgentType | None = None,
|
|
62
|
-
) -> AgentProfile:
|
|
63
|
-
model_name = llm_clients.main_model_name
|
|
64
|
-
return AgentProfile(
|
|
65
|
-
llm_client_factory=lambda: llm_clients.main,
|
|
66
|
-
system_prompt=load_system_prompt(model_name, sub_agent_type),
|
|
67
|
-
tools=load_agent_tools(model_name, sub_agent_type),
|
|
68
|
-
reminders=load_agent_reminders(model_name, sub_agent_type),
|
|
69
|
-
)
|
|
70
|
-
|
|
71
|
-
def build_profile_eager(
|
|
72
42
|
self,
|
|
73
43
|
llm_client: LLMClientABC,
|
|
74
44
|
sub_agent_type: tools.SubAgentType | None = None,
|
|
75
45
|
) -> AgentProfile:
|
|
76
46
|
model_name = llm_client.model_name
|
|
77
47
|
return AgentProfile(
|
|
78
|
-
|
|
48
|
+
llm_client=llm_client,
|
|
79
49
|
system_prompt=load_system_prompt(model_name, sub_agent_type),
|
|
80
50
|
tools=load_agent_tools(model_name, sub_agent_type),
|
|
81
51
|
reminders=load_agent_reminders(model_name, sub_agent_type),
|
|
@@ -86,26 +56,13 @@ class VanillaModelProfileProvider(ModelProfileProvider):
|
|
|
86
56
|
"""Provider that strips prompts, reminders, and tools for vanilla mode."""
|
|
87
57
|
|
|
88
58
|
def build_profile(
|
|
89
|
-
self,
|
|
90
|
-
llm_clients: LLMClients,
|
|
91
|
-
sub_agent_type: tools.SubAgentType | None = None,
|
|
92
|
-
) -> AgentProfile:
|
|
93
|
-
model_name = llm_clients.main_model_name
|
|
94
|
-
return AgentProfile(
|
|
95
|
-
llm_client_factory=lambda: llm_clients.main,
|
|
96
|
-
system_prompt=None,
|
|
97
|
-
tools=load_agent_tools(model_name, vanilla=True),
|
|
98
|
-
reminders=load_agent_reminders(model_name, vanilla=True),
|
|
99
|
-
)
|
|
100
|
-
|
|
101
|
-
def build_profile_eager(
|
|
102
59
|
self,
|
|
103
60
|
llm_client: LLMClientABC,
|
|
104
61
|
sub_agent_type: tools.SubAgentType | None = None,
|
|
105
62
|
) -> AgentProfile:
|
|
106
63
|
model_name = llm_client.model_name
|
|
107
64
|
return AgentProfile(
|
|
108
|
-
|
|
65
|
+
llm_client=llm_client,
|
|
109
66
|
system_prompt=None,
|
|
110
67
|
tools=load_agent_tools(model_name, vanilla=True),
|
|
111
68
|
reminders=load_agent_reminders(model_name, vanilla=True),
|
|
@@ -117,14 +74,12 @@ class Agent:
|
|
|
117
74
|
self,
|
|
118
75
|
session: Session,
|
|
119
76
|
profile: AgentProfile,
|
|
120
|
-
model_name: str | None = None,
|
|
121
77
|
):
|
|
122
78
|
self.session: Session = session
|
|
123
79
|
self.profile: AgentProfile = profile
|
|
124
80
|
self._current_task: TaskExecutor | None = None
|
|
125
|
-
self.
|
|
126
|
-
|
|
127
|
-
self.session.model_name = model_name
|
|
81
|
+
if not self.session.model_name:
|
|
82
|
+
self.session.model_name = profile.llm_client.model_name
|
|
128
83
|
|
|
129
84
|
def cancel(self) -> Iterable[events.Event]:
|
|
130
85
|
"""Handle agent cancellation and persist an interrupt marker and tool cancellations.
|
|
@@ -137,8 +92,7 @@ class Agent:
|
|
|
137
92
|
"""
|
|
138
93
|
# First, cancel any running task so it stops emitting events.
|
|
139
94
|
if self._current_task is not None:
|
|
140
|
-
|
|
141
|
-
yield ui_event
|
|
95
|
+
yield from self._current_task.cancel()
|
|
142
96
|
self._current_task = None
|
|
143
97
|
|
|
144
98
|
# Record an interrupt marker in the session history
|
|
@@ -149,7 +103,7 @@ class Agent:
|
|
|
149
103
|
debug_type=DebugType.EXECUTION,
|
|
150
104
|
)
|
|
151
105
|
|
|
152
|
-
async def run_task(self, user_input: UserInputPayload) -> AsyncGenerator[events.Event
|
|
106
|
+
async def run_task(self, user_input: UserInputPayload) -> AsyncGenerator[events.Event]:
|
|
153
107
|
session_ctx = SessionContext(
|
|
154
108
|
session_id=self.session.id,
|
|
155
109
|
get_conversation_history=lambda: self.session.conversation_history,
|
|
@@ -170,17 +124,11 @@ class Agent:
|
|
|
170
124
|
|
|
171
125
|
try:
|
|
172
126
|
async for event in task.run(user_input):
|
|
173
|
-
# Compute context_delta for TaskMetadataEvent
|
|
174
|
-
if isinstance(event, events.TaskMetadataEvent):
|
|
175
|
-
usage = event.metadata.main.usage
|
|
176
|
-
if usage is not None and usage.context_token is not None:
|
|
177
|
-
usage.context_delta = usage.context_token - self._prev_context_token
|
|
178
|
-
self._prev_context_token = usage.context_token
|
|
179
127
|
yield event
|
|
180
128
|
finally:
|
|
181
129
|
self._current_task = None
|
|
182
130
|
|
|
183
|
-
async def replay_history(self) -> AsyncGenerator[events.Event
|
|
131
|
+
async def replay_history(self) -> AsyncGenerator[events.Event]:
|
|
184
132
|
"""Yield UI events reconstructed from saved conversation history."""
|
|
185
133
|
|
|
186
134
|
if len(self.session.conversation_history) == 0:
|
|
@@ -192,21 +140,18 @@ class Agent:
|
|
|
192
140
|
session_id=self.session.id,
|
|
193
141
|
)
|
|
194
142
|
|
|
195
|
-
async def _process_reminder(self, reminder: Reminder) -> AsyncGenerator[events.DeveloperMessageEvent
|
|
143
|
+
async def _process_reminder(self, reminder: Reminder) -> AsyncGenerator[events.DeveloperMessageEvent]:
|
|
196
144
|
"""Process a single reminder and yield events if it produces output."""
|
|
197
145
|
item = await reminder(self.session)
|
|
198
146
|
if item is not None:
|
|
199
147
|
self.session.append_history([item])
|
|
200
148
|
yield events.DeveloperMessageEvent(session_id=self.session.id, item=item)
|
|
201
149
|
|
|
202
|
-
def set_model_profile(self, profile: AgentProfile
|
|
150
|
+
def set_model_profile(self, profile: AgentProfile) -> None:
|
|
203
151
|
"""Apply a fully constructed profile to the agent."""
|
|
204
152
|
|
|
205
153
|
self.profile = profile
|
|
206
|
-
|
|
207
|
-
self.session.model_name = model_name
|
|
208
|
-
elif not self.session.model_name:
|
|
209
|
-
self.session.model_name = profile.llm_client.model_name
|
|
154
|
+
self.session.model_name = profile.llm_client.model_name
|
|
210
155
|
|
|
211
156
|
def get_llm_client(self) -> LLMClientABC:
|
|
212
157
|
return self.profile.llm_client
|
klaude_code/core/executor.py
CHANGED
|
@@ -264,14 +264,14 @@ class ExecutorContext:
|
|
|
264
264
|
import traceback
|
|
265
265
|
|
|
266
266
|
log_debug(
|
|
267
|
-
f"Agent task {task_id} failed: {
|
|
267
|
+
f"Agent task {task_id} failed: {e!s}",
|
|
268
268
|
style="red",
|
|
269
269
|
debug_type=DebugType.EXECUTION,
|
|
270
270
|
)
|
|
271
271
|
log_debug(traceback.format_exc(), style="red", debug_type=DebugType.EXECUTION)
|
|
272
272
|
await self.emit_event(
|
|
273
273
|
events.ErrorEvent(
|
|
274
|
-
error_message=f"Agent task failed: [{e.__class__.__name__}] {
|
|
274
|
+
error_message=f"Agent task failed: [{e.__class__.__name__}] {e!s}",
|
|
275
275
|
can_retry=False,
|
|
276
276
|
)
|
|
277
277
|
)
|
|
@@ -317,6 +317,7 @@ class Executor:
|
|
|
317
317
|
self.submission_queue: asyncio.Queue[op.Submission] = asyncio.Queue()
|
|
318
318
|
# Track completion events for all submissions (not just those with ActiveTask)
|
|
319
319
|
self._completion_events: dict[str, asyncio.Event] = {}
|
|
320
|
+
self._background_tasks: set[asyncio.Task[None]] = set()
|
|
320
321
|
|
|
321
322
|
async def submit(self, operation: op.Operation) -> str:
|
|
322
323
|
"""
|
|
@@ -388,12 +389,12 @@ class Executor:
|
|
|
388
389
|
except Exception as e:
|
|
389
390
|
# Handle unexpected errors
|
|
390
391
|
log_debug(
|
|
391
|
-
f"Executor error: {
|
|
392
|
+
f"Executor error: {e!s}",
|
|
392
393
|
style="red",
|
|
393
394
|
debug_type=DebugType.EXECUTION,
|
|
394
395
|
)
|
|
395
396
|
await self.context.emit_event(
|
|
396
|
-
events.ErrorEvent(error_message=f"Executor error: {
|
|
397
|
+
events.ErrorEvent(error_message=f"Executor error: {e!s}", can_retry=False)
|
|
397
398
|
)
|
|
398
399
|
|
|
399
400
|
async def stop(self) -> None:
|
|
@@ -420,7 +421,7 @@ class Executor:
|
|
|
420
421
|
await self.submission_queue.put(submission)
|
|
421
422
|
except Exception as e:
|
|
422
423
|
log_debug(
|
|
423
|
-
f"Failed to send EndOperation: {
|
|
424
|
+
f"Failed to send EndOperation: {e!s}",
|
|
424
425
|
style="red",
|
|
425
426
|
debug_type=DebugType.EXECUTION,
|
|
426
427
|
)
|
|
@@ -460,17 +461,17 @@ class Executor:
|
|
|
460
461
|
event.set()
|
|
461
462
|
else:
|
|
462
463
|
# Run in background so the submission loop can continue (e.g., to handle interrupts)
|
|
463
|
-
asyncio.create_task(_await_agent_and_complete(task))
|
|
464
|
+
background_task = asyncio.create_task(_await_agent_and_complete(task))
|
|
465
|
+
self._background_tasks.add(background_task)
|
|
466
|
+
background_task.add_done_callback(self._background_tasks.discard)
|
|
464
467
|
|
|
465
468
|
except Exception as e:
|
|
466
469
|
log_debug(
|
|
467
|
-
f"Failed to handle submission {submission.id}: {
|
|
470
|
+
f"Failed to handle submission {submission.id}: {e!s}",
|
|
468
471
|
style="red",
|
|
469
472
|
debug_type=DebugType.EXECUTION,
|
|
470
473
|
)
|
|
471
|
-
await self.context.emit_event(
|
|
472
|
-
events.ErrorEvent(error_message=f"Operation failed: {str(e)}", can_retry=False)
|
|
473
|
-
)
|
|
474
|
+
await self.context.emit_event(events.ErrorEvent(error_message=f"Operation failed: {e!s}", can_retry=False))
|
|
474
475
|
# Set completion event even on error to prevent wait_for_completion from hanging
|
|
475
476
|
event = self._completion_events.get(submission.id)
|
|
476
477
|
if event is not None:
|
|
@@ -51,8 +51,8 @@ class AgentManager:
|
|
|
51
51
|
if agent is not None:
|
|
52
52
|
return agent
|
|
53
53
|
session = Session.load(session_id)
|
|
54
|
-
profile = self._model_profile_provider.build_profile(self._llm_clients)
|
|
55
|
-
agent = Agent(session=session, profile=profile
|
|
54
|
+
profile = self._model_profile_provider.build_profile(self._llm_clients.main)
|
|
55
|
+
agent = Agent(session=session, profile=profile)
|
|
56
56
|
|
|
57
57
|
async for evt in agent.replay_history():
|
|
58
58
|
await self.emit_event(evt)
|
|
@@ -60,7 +60,7 @@ class AgentManager:
|
|
|
60
60
|
await self.emit_event(
|
|
61
61
|
events.WelcomeEvent(
|
|
62
62
|
work_dir=str(session.work_dir),
|
|
63
|
-
llm_config=self._llm_clients.get_llm_config(),
|
|
63
|
+
llm_config=self._llm_clients.main.get_llm_config(),
|
|
64
64
|
)
|
|
65
65
|
)
|
|
66
66
|
|
|
@@ -81,7 +81,7 @@ class AgentManager:
|
|
|
81
81
|
|
|
82
82
|
llm_config = config.get_model_config(model_name)
|
|
83
83
|
llm_client = create_llm_client(llm_config)
|
|
84
|
-
agent.set_model_profile(self._model_profile_provider.
|
|
84
|
+
agent.set_model_profile(self._model_profile_provider.build_profile(llm_client))
|
|
85
85
|
|
|
86
86
|
developer_item = model.DeveloperMessageItem(
|
|
87
87
|
content=f"switched to model: {model_name}",
|
|
@@ -2,66 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from dataclasses import field as dataclass_field
|
|
6
7
|
|
|
7
8
|
from klaude_code.llm.client import LLMClientABC
|
|
8
|
-
from klaude_code.protocol import llm_param
|
|
9
9
|
from klaude_code.protocol.tools import SubAgentType
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def __init__(
|
|
16
|
-
self,
|
|
17
|
-
main_factory: Callable[[], LLMClientABC],
|
|
18
|
-
main_model_name: str,
|
|
19
|
-
main_llm_config: llm_param.LLMConfigParameter,
|
|
20
|
-
) -> None:
|
|
21
|
-
self._main_factory: Callable[[], LLMClientABC] | None = main_factory
|
|
22
|
-
self._main_client: LLMClientABC | None = None
|
|
23
|
-
self._main_model_name: str = main_model_name
|
|
24
|
-
self._main_llm_config: llm_param.LLMConfigParameter = main_llm_config
|
|
25
|
-
self._sub_clients: dict[SubAgentType, LLMClientABC] = {}
|
|
26
|
-
self._sub_factories: dict[SubAgentType, Callable[[], LLMClientABC]] = {}
|
|
12
|
+
def _default_sub_clients() -> dict[SubAgentType, LLMClientABC]:
|
|
13
|
+
return {}
|
|
27
14
|
|
|
28
|
-
@property
|
|
29
|
-
def main_model_name(self) -> str:
|
|
30
|
-
return self._main_model_name
|
|
31
15
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
@property
|
|
36
|
-
def main(self) -> LLMClientABC:
|
|
37
|
-
if self._main_client is None:
|
|
38
|
-
if self._main_factory is None:
|
|
39
|
-
raise RuntimeError("Main client factory not set")
|
|
40
|
-
self._main_client = self._main_factory()
|
|
41
|
-
self._main_factory = None
|
|
42
|
-
return self._main_client
|
|
16
|
+
@dataclass
|
|
17
|
+
class LLMClients:
|
|
18
|
+
"""Container for LLM clients used by main agent and sub-agents."""
|
|
43
19
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
sub_agent_type: SubAgentType,
|
|
47
|
-
factory: Callable[[], LLMClientABC],
|
|
48
|
-
) -> None:
|
|
49
|
-
self._sub_factories[sub_agent_type] = factory
|
|
20
|
+
main: LLMClientABC
|
|
21
|
+
sub_clients: dict[SubAgentType, LLMClientABC] = dataclass_field(default_factory=_default_sub_clients)
|
|
50
22
|
|
|
51
23
|
def get_client(self, sub_agent_type: SubAgentType | None = None) -> LLMClientABC:
|
|
52
24
|
"""Return client for a sub-agent type or the main client."""
|
|
53
25
|
|
|
54
26
|
if sub_agent_type is None:
|
|
55
27
|
return self.main
|
|
56
|
-
|
|
57
|
-
existing = self._sub_clients.get(sub_agent_type)
|
|
58
|
-
if existing is not None:
|
|
59
|
-
return existing
|
|
60
|
-
|
|
61
|
-
factory = self._sub_factories.get(sub_agent_type)
|
|
62
|
-
if factory is None:
|
|
63
|
-
return self.main
|
|
64
|
-
|
|
65
|
-
client = factory()
|
|
66
|
-
self._sub_clients[sub_agent_type] = client
|
|
67
|
-
return client
|
|
28
|
+
return self.sub_clients.get(sub_agent_type) or self.main
|