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.
Files changed (88) hide show
  1. klaude_code/auth/codex/oauth.py +3 -3
  2. klaude_code/cli/main.py +5 -5
  3. klaude_code/cli/runtime.py +19 -27
  4. klaude_code/cli/session_cmd.py +6 -8
  5. klaude_code/command/__init__.py +31 -28
  6. klaude_code/command/clear_cmd.py +0 -2
  7. klaude_code/command/diff_cmd.py +0 -2
  8. klaude_code/command/export_cmd.py +3 -5
  9. klaude_code/command/help_cmd.py +0 -2
  10. klaude_code/command/model_cmd.py +0 -2
  11. klaude_code/command/refresh_cmd.py +0 -2
  12. klaude_code/command/registry.py +5 -9
  13. klaude_code/command/release_notes_cmd.py +0 -2
  14. klaude_code/command/status_cmd.py +2 -4
  15. klaude_code/command/terminal_setup_cmd.py +2 -4
  16. klaude_code/command/thinking_cmd.py +229 -0
  17. klaude_code/config/__init__.py +1 -1
  18. klaude_code/config/list_model.py +1 -1
  19. klaude_code/config/select_model.py +5 -15
  20. klaude_code/const/__init__.py +1 -1
  21. klaude_code/core/agent.py +14 -69
  22. klaude_code/core/executor.py +11 -10
  23. klaude_code/core/manager/agent_manager.py +4 -4
  24. klaude_code/core/manager/llm_clients.py +10 -49
  25. klaude_code/core/manager/llm_clients_builder.py +8 -21
  26. klaude_code/core/manager/sub_agent_manager.py +3 -3
  27. klaude_code/core/prompt.py +3 -3
  28. klaude_code/core/reminders.py +1 -1
  29. klaude_code/core/task.py +4 -5
  30. klaude_code/core/tool/__init__.py +16 -25
  31. klaude_code/core/tool/file/_utils.py +1 -1
  32. klaude_code/core/tool/file/apply_patch.py +17 -25
  33. klaude_code/core/tool/file/apply_patch_tool.py +4 -7
  34. klaude_code/core/tool/file/edit_tool.py +4 -11
  35. klaude_code/core/tool/file/multi_edit_tool.py +2 -3
  36. klaude_code/core/tool/file/read_tool.py +3 -4
  37. klaude_code/core/tool/file/write_tool.py +2 -3
  38. klaude_code/core/tool/memory/memory_tool.py +2 -8
  39. klaude_code/core/tool/memory/skill_loader.py +3 -2
  40. klaude_code/core/tool/shell/command_safety.py +0 -1
  41. klaude_code/core/tool/tool_context.py +1 -3
  42. klaude_code/core/tool/tool_registry.py +2 -1
  43. klaude_code/core/tool/tool_runner.py +1 -1
  44. klaude_code/core/tool/truncation.py +2 -5
  45. klaude_code/core/turn.py +9 -4
  46. klaude_code/llm/anthropic/client.py +62 -49
  47. klaude_code/llm/client.py +2 -20
  48. klaude_code/llm/codex/client.py +51 -32
  49. klaude_code/llm/input_common.py +2 -2
  50. klaude_code/llm/openai_compatible/client.py +60 -39
  51. klaude_code/llm/openai_compatible/stream_processor.py +2 -1
  52. klaude_code/llm/openrouter/client.py +79 -45
  53. klaude_code/llm/openrouter/reasoning_handler.py +19 -132
  54. klaude_code/llm/registry.py +6 -5
  55. klaude_code/llm/responses/client.py +65 -43
  56. klaude_code/llm/usage.py +1 -49
  57. klaude_code/protocol/commands.py +1 -0
  58. klaude_code/protocol/events.py +7 -0
  59. klaude_code/protocol/llm_param.py +1 -9
  60. klaude_code/protocol/model.py +10 -6
  61. klaude_code/protocol/sub_agent.py +2 -1
  62. klaude_code/session/export.py +1 -8
  63. klaude_code/session/selector.py +12 -7
  64. klaude_code/session/session.py +2 -4
  65. klaude_code/trace/__init__.py +1 -1
  66. klaude_code/trace/log.py +1 -1
  67. klaude_code/ui/__init__.py +4 -9
  68. klaude_code/ui/core/stage_manager.py +7 -4
  69. klaude_code/ui/modes/repl/__init__.py +1 -1
  70. klaude_code/ui/modes/repl/completers.py +6 -7
  71. klaude_code/ui/modes/repl/display.py +3 -4
  72. klaude_code/ui/modes/repl/event_handler.py +63 -5
  73. klaude_code/ui/modes/repl/key_bindings.py +2 -3
  74. klaude_code/ui/modes/repl/renderer.py +2 -1
  75. klaude_code/ui/renderers/diffs.py +1 -4
  76. klaude_code/ui/renderers/metadata.py +1 -12
  77. klaude_code/ui/rich/markdown.py +3 -3
  78. klaude_code/ui/rich/searchable_text.py +6 -6
  79. klaude_code/ui/rich/status.py +3 -4
  80. klaude_code/ui/rich/theme.py +1 -4
  81. klaude_code/ui/terminal/control.py +7 -16
  82. klaude_code/ui/terminal/notifier.py +2 -4
  83. klaude_code/ui/utils/common.py +1 -1
  84. klaude_code/ui/utils/debouncer.py +2 -2
  85. {klaude_code-1.2.11.dist-info → klaude_code-1.2.13.dist-info}/METADATA +1 -1
  86. {klaude_code-1.2.11.dist-info → klaude_code-1.2.13.dist-info}/RECORD +88 -87
  87. {klaude_code-1.2.11.dist-info → klaude_code-1.2.13.dist-info}/WHEEL +0 -0
  88. {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
+ )
@@ -4,8 +4,8 @@ from .select_model import select_model_from_config
4
4
 
5
5
  __all__ = [
6
6
  "Config",
7
- "load_config",
8
7
  "config_path",
9
8
  "display_models_and_providers",
9
+ "load_config",
10
10
  "select_model_from_config",
11
11
  ]
@@ -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.timezone.utc)
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
- fragments = [
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
- ("t", ""),
52
- ("b", "bold"),
53
- ("d", "dim"),
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"),
@@ -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 = 50000
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, Callable, Iterable
3
+ from collections.abc import AsyncGenerator, Iterable
4
4
  from dataclasses import dataclass
5
- from typing import TYPE_CHECKING, Protocol
5
+ from typing import Protocol
6
6
 
7
- from klaude_code.core.prompt import get_system_prompt as load_system_prompt
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
- llm_client_factory: Callable[[], LLMClientABC]
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
- llm_client_factory=lambda: llm_client,
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
- llm_client_factory=lambda: llm_client,
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._prev_context_token: int = 0 # Track context size from previous task for delta calculation
126
- if not self.session.model_name and model_name:
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
- for ui_event in self._current_task.cancel():
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, None]:
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, None]:
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, None]:
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, model_name: str | None = None) -> None:
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
- if model_name:
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
@@ -264,14 +264,14 @@ class ExecutorContext:
264
264
  import traceback
265
265
 
266
266
  log_debug(
267
- f"Agent task {task_id} failed: {str(e)}",
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__}] {str(e)}",
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: {str(e)}",
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: {str(e)}", can_retry=False)
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: {str(e)}",
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}: {str(e)}",
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, model_name=self._llm_clients.main_model_name)
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.build_profile_eager(llm_client), model_name=model_name)
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 collections.abc import Callable
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
- class LLMClients:
13
- """Container for LLM clients used by main agent and sub-agents."""
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
- def get_llm_config(self) -> llm_param.LLMConfigParameter:
33
- return self._main_llm_config
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
- def register_sub_client_factory(
45
- self,
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