klaude-code 1.2.6__py3-none-any.whl → 1.8.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. klaude_code/auth/__init__.py +24 -0
  2. klaude_code/auth/codex/__init__.py +20 -0
  3. klaude_code/auth/codex/exceptions.py +17 -0
  4. klaude_code/auth/codex/jwt_utils.py +45 -0
  5. klaude_code/auth/codex/oauth.py +229 -0
  6. klaude_code/auth/codex/token_manager.py +84 -0
  7. klaude_code/cli/auth_cmd.py +73 -0
  8. klaude_code/cli/config_cmd.py +91 -0
  9. klaude_code/cli/cost_cmd.py +338 -0
  10. klaude_code/cli/debug.py +78 -0
  11. klaude_code/cli/list_model.py +307 -0
  12. klaude_code/cli/main.py +233 -134
  13. klaude_code/cli/runtime.py +309 -117
  14. klaude_code/{version.py → cli/self_update.py} +114 -5
  15. klaude_code/cli/session_cmd.py +37 -21
  16. klaude_code/command/__init__.py +88 -27
  17. klaude_code/command/clear_cmd.py +8 -7
  18. klaude_code/command/command_abc.py +31 -31
  19. klaude_code/command/debug_cmd.py +79 -0
  20. klaude_code/command/export_cmd.py +19 -53
  21. klaude_code/command/export_online_cmd.py +154 -0
  22. klaude_code/command/fork_session_cmd.py +267 -0
  23. klaude_code/command/help_cmd.py +7 -8
  24. klaude_code/command/model_cmd.py +60 -10
  25. klaude_code/command/model_select.py +84 -0
  26. klaude_code/command/prompt-jj-describe.md +32 -0
  27. klaude_code/command/prompt_command.py +19 -11
  28. klaude_code/command/refresh_cmd.py +8 -10
  29. klaude_code/command/registry.py +139 -40
  30. klaude_code/command/release_notes_cmd.py +84 -0
  31. klaude_code/command/resume_cmd.py +111 -0
  32. klaude_code/command/status_cmd.py +104 -60
  33. klaude_code/command/terminal_setup_cmd.py +7 -9
  34. klaude_code/command/thinking_cmd.py +98 -0
  35. klaude_code/config/__init__.py +14 -6
  36. klaude_code/config/assets/__init__.py +1 -0
  37. klaude_code/config/assets/builtin_config.yaml +303 -0
  38. klaude_code/config/builtin_config.py +38 -0
  39. klaude_code/config/config.py +378 -109
  40. klaude_code/config/select_model.py +117 -53
  41. klaude_code/config/thinking.py +269 -0
  42. klaude_code/{const/__init__.py → const.py} +50 -19
  43. klaude_code/core/agent.py +20 -28
  44. klaude_code/core/executor.py +327 -112
  45. klaude_code/core/manager/__init__.py +2 -4
  46. klaude_code/core/manager/llm_clients.py +1 -15
  47. klaude_code/core/manager/llm_clients_builder.py +10 -11
  48. klaude_code/core/manager/sub_agent_manager.py +37 -6
  49. klaude_code/core/prompt.py +63 -44
  50. klaude_code/core/prompts/prompt-claude-code.md +2 -13
  51. klaude_code/core/prompts/prompt-codex-gpt-5-1-codex-max.md +117 -0
  52. klaude_code/core/prompts/prompt-codex-gpt-5-2-codex.md +117 -0
  53. klaude_code/core/prompts/prompt-codex.md +9 -42
  54. klaude_code/core/prompts/prompt-minimal.md +12 -0
  55. klaude_code/core/prompts/{prompt-subagent-explore.md → prompt-sub-agent-explore.md} +16 -3
  56. klaude_code/core/prompts/{prompt-subagent-oracle.md → prompt-sub-agent-oracle.md} +1 -2
  57. klaude_code/core/prompts/prompt-sub-agent-web.md +51 -0
  58. klaude_code/core/reminders.py +283 -95
  59. klaude_code/core/task.py +113 -75
  60. klaude_code/core/tool/__init__.py +24 -31
  61. klaude_code/core/tool/file/_utils.py +36 -0
  62. klaude_code/core/tool/file/apply_patch.py +17 -25
  63. klaude_code/core/tool/file/apply_patch_tool.py +57 -77
  64. klaude_code/core/tool/file/diff_builder.py +151 -0
  65. klaude_code/core/tool/file/edit_tool.py +50 -63
  66. klaude_code/core/tool/file/move_tool.md +41 -0
  67. klaude_code/core/tool/file/move_tool.py +435 -0
  68. klaude_code/core/tool/file/read_tool.md +1 -1
  69. klaude_code/core/tool/file/read_tool.py +86 -86
  70. klaude_code/core/tool/file/write_tool.py +59 -69
  71. klaude_code/core/tool/report_back_tool.py +84 -0
  72. klaude_code/core/tool/shell/bash_tool.py +265 -22
  73. klaude_code/core/tool/shell/command_safety.py +3 -6
  74. klaude_code/core/tool/{memory → skill}/skill_tool.py +16 -26
  75. klaude_code/core/tool/sub_agent_tool.py +13 -2
  76. klaude_code/core/tool/todo/todo_write_tool.md +0 -157
  77. klaude_code/core/tool/todo/todo_write_tool.py +1 -1
  78. klaude_code/core/tool/todo/todo_write_tool_raw.md +182 -0
  79. klaude_code/core/tool/todo/update_plan_tool.py +1 -1
  80. klaude_code/core/tool/tool_abc.py +18 -0
  81. klaude_code/core/tool/tool_context.py +27 -12
  82. klaude_code/core/tool/tool_registry.py +7 -7
  83. klaude_code/core/tool/tool_runner.py +44 -36
  84. klaude_code/core/tool/truncation.py +29 -14
  85. klaude_code/core/tool/web/mermaid_tool.md +43 -0
  86. klaude_code/core/tool/web/mermaid_tool.py +2 -5
  87. klaude_code/core/tool/web/web_fetch_tool.md +1 -1
  88. klaude_code/core/tool/web/web_fetch_tool.py +112 -22
  89. klaude_code/core/tool/web/web_search_tool.md +23 -0
  90. klaude_code/core/tool/web/web_search_tool.py +130 -0
  91. klaude_code/core/turn.py +168 -66
  92. klaude_code/llm/__init__.py +2 -10
  93. klaude_code/llm/anthropic/client.py +190 -178
  94. klaude_code/llm/anthropic/input.py +39 -15
  95. klaude_code/llm/bedrock/__init__.py +3 -0
  96. klaude_code/llm/bedrock/client.py +60 -0
  97. klaude_code/llm/client.py +7 -21
  98. klaude_code/llm/codex/__init__.py +5 -0
  99. klaude_code/llm/codex/client.py +149 -0
  100. klaude_code/llm/google/__init__.py +3 -0
  101. klaude_code/llm/google/client.py +309 -0
  102. klaude_code/llm/google/input.py +215 -0
  103. klaude_code/llm/input_common.py +3 -9
  104. klaude_code/llm/openai_compatible/client.py +72 -164
  105. klaude_code/llm/openai_compatible/input.py +6 -4
  106. klaude_code/llm/openai_compatible/stream.py +273 -0
  107. klaude_code/llm/openai_compatible/tool_call_accumulator.py +17 -1
  108. klaude_code/llm/openrouter/client.py +89 -160
  109. klaude_code/llm/openrouter/input.py +18 -30
  110. klaude_code/llm/openrouter/reasoning.py +118 -0
  111. klaude_code/llm/registry.py +39 -7
  112. klaude_code/llm/responses/client.py +184 -171
  113. klaude_code/llm/responses/input.py +20 -1
  114. klaude_code/llm/usage.py +17 -12
  115. klaude_code/protocol/commands.py +17 -1
  116. klaude_code/protocol/events.py +31 -4
  117. klaude_code/protocol/llm_param.py +13 -10
  118. klaude_code/protocol/model.py +232 -29
  119. klaude_code/protocol/op.py +90 -1
  120. klaude_code/protocol/op_handler.py +35 -1
  121. klaude_code/protocol/sub_agent/__init__.py +117 -0
  122. klaude_code/protocol/sub_agent/explore.py +63 -0
  123. klaude_code/protocol/sub_agent/oracle.py +91 -0
  124. klaude_code/protocol/sub_agent/task.py +61 -0
  125. klaude_code/protocol/sub_agent/web.py +79 -0
  126. klaude_code/protocol/tools.py +4 -2
  127. klaude_code/session/__init__.py +2 -2
  128. klaude_code/session/codec.py +71 -0
  129. klaude_code/session/export.py +293 -86
  130. klaude_code/session/selector.py +89 -67
  131. klaude_code/session/session.py +320 -309
  132. klaude_code/session/store.py +220 -0
  133. klaude_code/session/templates/export_session.html +595 -83
  134. klaude_code/session/templates/mermaid_viewer.html +926 -0
  135. klaude_code/skill/__init__.py +27 -0
  136. klaude_code/skill/assets/deslop/SKILL.md +17 -0
  137. klaude_code/skill/assets/dev-docs/SKILL.md +108 -0
  138. klaude_code/skill/assets/handoff/SKILL.md +39 -0
  139. klaude_code/skill/assets/jj-workspace/SKILL.md +20 -0
  140. klaude_code/skill/assets/skill-creator/SKILL.md +139 -0
  141. klaude_code/{core/tool/memory/skill_loader.py → skill/loader.py} +55 -15
  142. klaude_code/skill/manager.py +70 -0
  143. klaude_code/skill/system_skills.py +192 -0
  144. klaude_code/trace/__init__.py +20 -2
  145. klaude_code/trace/log.py +150 -5
  146. klaude_code/ui/__init__.py +4 -9
  147. klaude_code/ui/core/input.py +1 -1
  148. klaude_code/ui/core/stage_manager.py +7 -7
  149. klaude_code/ui/modes/debug/display.py +2 -1
  150. klaude_code/ui/modes/repl/__init__.py +3 -48
  151. klaude_code/ui/modes/repl/clipboard.py +5 -5
  152. klaude_code/ui/modes/repl/completers.py +487 -123
  153. klaude_code/ui/modes/repl/display.py +5 -4
  154. klaude_code/ui/modes/repl/event_handler.py +370 -117
  155. klaude_code/ui/modes/repl/input_prompt_toolkit.py +552 -105
  156. klaude_code/ui/modes/repl/key_bindings.py +146 -23
  157. klaude_code/ui/modes/repl/renderer.py +189 -99
  158. klaude_code/ui/renderers/assistant.py +9 -2
  159. klaude_code/ui/renderers/bash_syntax.py +178 -0
  160. klaude_code/ui/renderers/common.py +78 -0
  161. klaude_code/ui/renderers/developer.py +104 -48
  162. klaude_code/ui/renderers/diffs.py +87 -6
  163. klaude_code/ui/renderers/errors.py +11 -6
  164. klaude_code/ui/renderers/mermaid_viewer.py +57 -0
  165. klaude_code/ui/renderers/metadata.py +112 -76
  166. klaude_code/ui/renderers/sub_agent.py +92 -7
  167. klaude_code/ui/renderers/thinking.py +40 -18
  168. klaude_code/ui/renderers/tools.py +405 -227
  169. klaude_code/ui/renderers/user_input.py +73 -13
  170. klaude_code/ui/rich/__init__.py +10 -1
  171. klaude_code/ui/rich/cjk_wrap.py +228 -0
  172. klaude_code/ui/rich/code_panel.py +131 -0
  173. klaude_code/ui/rich/live.py +17 -0
  174. klaude_code/ui/rich/markdown.py +305 -170
  175. klaude_code/ui/rich/searchable_text.py +10 -13
  176. klaude_code/ui/rich/status.py +190 -49
  177. klaude_code/ui/rich/theme.py +135 -39
  178. klaude_code/ui/terminal/__init__.py +55 -0
  179. klaude_code/ui/terminal/color.py +1 -1
  180. klaude_code/ui/terminal/control.py +13 -22
  181. klaude_code/ui/terminal/notifier.py +44 -4
  182. klaude_code/ui/terminal/selector.py +658 -0
  183. klaude_code/ui/utils/common.py +0 -18
  184. klaude_code-1.8.0.dist-info/METADATA +377 -0
  185. klaude_code-1.8.0.dist-info/RECORD +219 -0
  186. {klaude_code-1.2.6.dist-info → klaude_code-1.8.0.dist-info}/entry_points.txt +1 -0
  187. klaude_code/command/diff_cmd.py +0 -138
  188. klaude_code/command/prompt-dev-docs-update.md +0 -56
  189. klaude_code/command/prompt-dev-docs.md +0 -46
  190. klaude_code/config/list_model.py +0 -162
  191. klaude_code/core/manager/agent_manager.py +0 -127
  192. klaude_code/core/prompts/prompt-subagent-webfetch.md +0 -46
  193. klaude_code/core/tool/file/multi_edit_tool.md +0 -42
  194. klaude_code/core/tool/file/multi_edit_tool.py +0 -199
  195. klaude_code/core/tool/memory/memory_tool.md +0 -16
  196. klaude_code/core/tool/memory/memory_tool.py +0 -462
  197. klaude_code/llm/openrouter/reasoning_handler.py +0 -209
  198. klaude_code/protocol/sub_agent.py +0 -348
  199. klaude_code/ui/utils/debouncer.py +0 -42
  200. klaude_code-1.2.6.dist-info/METADATA +0 -178
  201. klaude_code-1.2.6.dist-info/RECORD +0 -167
  202. /klaude_code/core/prompts/{prompt-subagent.md → prompt-sub-agent.md} +0 -0
  203. /klaude_code/core/tool/{memory → skill}/__init__.py +0 -0
  204. /klaude_code/core/tool/{memory → skill}/skill_tool.md +0 -0
  205. {klaude_code-1.2.6.dist-info → klaude_code-1.8.0.dist-info}/WHEEL +0 -0
@@ -8,15 +8,23 @@ handling operations submitted from the CLI and coordinating with agents.
8
8
  from __future__ import annotations
9
9
 
10
10
  import asyncio
11
+ import subprocess
12
+ import sys
13
+ from collections.abc import Callable
11
14
  from dataclasses import dataclass
15
+ from pathlib import Path
12
16
 
13
- from klaude_code.command import InputAction, InputActionType, dispatch_command
17
+ from klaude_code.config import load_config
14
18
  from klaude_code.core.agent import Agent, DefaultModelProfileProvider, ModelProfileProvider
15
- from klaude_code.core.manager import AgentManager, LLMClients, SubAgentManager
19
+ from klaude_code.core.manager import LLMClients, SubAgentManager
16
20
  from klaude_code.core.tool import current_run_subtask_callback
17
- from klaude_code.protocol import events, model, op
21
+ from klaude_code.llm.registry import create_llm_client
22
+ from klaude_code.protocol import commands, events, model, op
23
+ from klaude_code.protocol.llm_param import Thinking
18
24
  from klaude_code.protocol.op_handler import OperationHandler
19
25
  from klaude_code.protocol.sub_agent import SubAgentResult
26
+ from klaude_code.session.export import build_export_html, get_default_export_path
27
+ from klaude_code.session.session import Session
20
28
  from klaude_code.trace import DebugType, log_debug
21
29
 
22
30
 
@@ -87,153 +95,272 @@ class ExecutorContext:
87
95
  event_queue: asyncio.Queue[events.Event],
88
96
  llm_clients: LLMClients,
89
97
  model_profile_provider: ModelProfileProvider | None = None,
98
+ on_model_change: Callable[[str], None] | None = None,
90
99
  ):
91
100
  self.event_queue: asyncio.Queue[events.Event] = event_queue
101
+ self.llm_clients: LLMClients = llm_clients
92
102
 
93
103
  resolved_profile_provider = model_profile_provider or DefaultModelProfileProvider()
94
104
  self.model_profile_provider: ModelProfileProvider = resolved_profile_provider
95
105
 
96
- # Delegate responsibilities to helper components
97
- self.agent_manager = AgentManager(event_queue, llm_clients, resolved_profile_provider)
98
106
  self.task_manager = TaskManager()
99
- self.subagent_manager = SubAgentManager(event_queue, llm_clients, resolved_profile_provider)
107
+ self.sub_agent_manager = SubAgentManager(event_queue, llm_clients, resolved_profile_provider)
108
+ self._on_model_change = on_model_change
109
+ self._agent: Agent | None = None
100
110
 
101
111
  async def emit_event(self, event: events.Event) -> None:
102
112
  """Emit an event to the UI display system."""
103
113
  await self.event_queue.put(event)
104
114
 
115
+ def current_session_id(self) -> str | None:
116
+ """Return the primary active session id, if any.
117
+
118
+ This is a convenience wrapper used by the CLI, which conceptually
119
+ operates on a single interactive session per process.
120
+ """
121
+
122
+ agent = self._agent
123
+ if agent is None:
124
+ return None
125
+ return agent.session.id
126
+
105
127
  @property
106
- def active_agents(self) -> dict[str, Agent]:
107
- """Expose currently active agents keyed by session id.
128
+ def current_agent(self) -> Agent | None:
129
+ """Return the currently active agent, if any."""
130
+
131
+ return self._agent
108
132
 
109
- This property preserves the previous public attribute used by the
110
- CLI status provider while delegating storage to :class:`AgentManager`.
133
+ async def _ensure_agent(self, session_id: str | None = None) -> Agent:
134
+ """Return the active agent, creating or loading a session as needed.
135
+
136
+ If ``session_id`` is ``None``, a new session is created with an
137
+ auto-generated ID. If provided, the executor attempts to resume the
138
+ session from disk or creates a new one if not found.
111
139
  """
112
140
 
113
- return self.agent_manager.all_active_agents()
141
+ # Fast-path: reuse current agent when the session id already matches.
142
+ if session_id is not None and self._agent is not None and self._agent.session.id == session_id:
143
+ return self._agent
144
+
145
+ session = Session.create() if session_id is None else Session.load(session_id)
146
+
147
+ if (
148
+ session.model_thinking is not None
149
+ and session.model_name
150
+ and session.model_name == self.llm_clients.main.model_name
151
+ ):
152
+ self.llm_clients.main.get_llm_config().thinking = session.model_thinking
153
+
154
+ profile = self.model_profile_provider.build_profile(self.llm_clients.main)
155
+ agent = Agent(session=session, profile=profile)
156
+
157
+ async for evt in agent.replay_history():
158
+ await self.emit_event(evt)
159
+
160
+ await self.emit_event(
161
+ events.WelcomeEvent(
162
+ work_dir=str(session.work_dir),
163
+ llm_config=self.llm_clients.main.get_llm_config(),
164
+ )
165
+ )
166
+
167
+ self._agent = agent
168
+ log_debug(
169
+ f"Initialized agent for session: {session.id}",
170
+ style="cyan",
171
+ debug_type=DebugType.EXECUTION,
172
+ )
173
+ return agent
114
174
 
115
175
  async def handle_init_agent(self, operation: op.InitAgentOperation) -> None:
116
176
  """Initialize an agent for a session and replay history to UI."""
117
- if operation.session_id is None:
118
- raise ValueError("session_id cannot be None")
119
-
120
- await self.agent_manager.ensure_agent(operation.session_id)
177
+ await self._ensure_agent(operation.session_id)
121
178
 
122
179
  async def handle_user_input(self, operation: op.UserInputOperation) -> None:
123
- """Handle a user input operation by running it through an agent."""
180
+ """Handle a user input operation.
181
+
182
+ Core should not parse slash commands. The UI/CLI layer is responsible for
183
+ turning raw user input into one or more operations.
184
+ """
124
185
 
125
186
  if operation.session_id is None:
126
187
  raise ValueError("session_id cannot be None")
127
188
 
128
189
  session_id = operation.session_id
129
- agent = await self.agent_manager.ensure_agent(session_id)
190
+ agent = await self._ensure_agent(session_id)
130
191
  user_input = operation.input
131
192
 
132
- # emit user input event
133
193
  await self.emit_event(
134
194
  events.UserMessageEvent(content=user_input.text, session_id=session_id, images=user_input.images)
135
195
  )
196
+ agent.session.append_history([model.UserMessageItem(content=user_input.text, images=user_input.images)])
136
197
 
137
- result = await dispatch_command(user_input.text, agent)
138
-
139
- actions: list[InputAction] = list(result.actions or [])
140
-
141
- has_run_agent_action = any(action.type is InputActionType.RUN_AGENT for action in actions)
142
- if not has_run_agent_action:
143
- # No async agent task will run, append user message directly
144
- agent.session.append_history([model.UserMessageItem(content=user_input.text, images=user_input.images)])
145
-
146
- if result.events:
147
- agent.session.append_history(
148
- [evt.item for evt in result.events if isinstance(evt, events.DeveloperMessageEvent)]
198
+ await self.handle_run_agent(
199
+ op.RunAgentOperation(
200
+ id=operation.id,
201
+ session_id=session_id,
202
+ input=user_input,
149
203
  )
150
- for evt in result.events:
151
- await self.emit_event(evt)
204
+ )
152
205
 
153
- for action in actions:
154
- await self._run_input_action(action, operation, agent)
206
+ async def handle_run_agent(self, operation: op.RunAgentOperation) -> None:
207
+ agent = await self._ensure_agent(operation.session_id)
208
+ existing_active = self.task_manager.get(operation.id)
209
+ if existing_active is not None and not existing_active.task.done():
210
+ raise RuntimeError(f"Active task already registered for operation {operation.id}")
211
+ task: asyncio.Task[None] = asyncio.create_task(
212
+ self._run_agent_task(agent, operation.input, operation.id, operation.session_id)
213
+ )
214
+ self.task_manager.register(operation.id, task, operation.session_id)
155
215
 
156
- async def _run_input_action(self, action: InputAction, operation: op.UserInputOperation, agent: Agent) -> None:
157
- if operation.session_id is None:
158
- raise ValueError("session_id cannot be None for input actions")
216
+ async def handle_change_model(self, operation: op.ChangeModelOperation) -> None:
217
+ agent = await self._ensure_agent(operation.session_id)
218
+ config = load_config()
159
219
 
160
- session_id = operation.session_id
220
+ llm_config = config.get_model_config(operation.model_name)
221
+ llm_client = create_llm_client(llm_config)
222
+ agent.set_model_profile(self.model_profile_provider.build_profile(llm_client))
161
223
 
162
- if action.type == InputActionType.RUN_AGENT:
163
- task_input = model.UserInputPayload(text=action.text, images=operation.input.images)
224
+ agent.session.model_config_name = operation.model_name
225
+ agent.session.model_thinking = llm_config.thinking
164
226
 
165
- existing_active = self.task_manager.get(operation.id)
166
- if existing_active is not None and not existing_active.task.done():
167
- raise RuntimeError(f"Active task already registered for operation {operation.id}")
227
+ if operation.save_as_default:
228
+ config.main_model = operation.model_name
229
+ await config.save()
168
230
 
169
- task: asyncio.Task[None] = asyncio.create_task(
170
- self._run_agent_task(agent, task_input, operation.id, session_id)
231
+ if operation.emit_switch_message:
232
+ default_note = " (saved as default)" if operation.save_as_default else ""
233
+ developer_item = model.DeveloperMessageItem(
234
+ content=f"Switched to: {llm_config.model}{default_note}",
235
+ command_output=model.CommandOutput(command_name=commands.CommandName.MODEL),
171
236
  )
172
- self.task_manager.register(operation.id, task, session_id)
173
- return
237
+ agent.session.append_history([developer_item])
238
+ await self.emit_event(events.DeveloperMessageEvent(session_id=agent.session.id, item=developer_item))
174
239
 
175
- if action.type == InputActionType.CHANGE_MODEL:
176
- if not action.model_name:
177
- raise ValueError("ChangeModel action requires model_name")
240
+ if self._on_model_change is not None:
241
+ self._on_model_change(llm_client.model_name)
178
242
 
179
- await self.agent_manager.apply_model_change(agent, action.model_name)
180
- return
243
+ if operation.emit_welcome_event:
244
+ await self.emit_event(events.WelcomeEvent(llm_config=llm_config, work_dir=str(agent.session.work_dir)))
181
245
 
182
- if action.type == InputActionType.CLEAR:
183
- await self.agent_manager.apply_clear(agent)
184
- return
246
+ async def handle_change_thinking(self, operation: op.ChangeThinkingOperation) -> None:
247
+ """Handle a change thinking operation.
185
248
 
186
- raise ValueError(f"Unsupported input action type: {action.type}")
249
+ Interactive thinking selection must happen in the UI/CLI layer. Core only
250
+ applies a concrete thinking configuration.
251
+ """
252
+ agent = await self._ensure_agent(operation.session_id)
253
+
254
+ config = agent.profile.llm_client.get_llm_config()
255
+
256
+ def _format_thinking_for_display(thinking: Thinking | None) -> str:
257
+ if thinking is None:
258
+ return "not configured"
259
+ if thinking.reasoning_effort:
260
+ return f"reasoning_effort={thinking.reasoning_effort}"
261
+ if thinking.type == "disabled":
262
+ return "off"
263
+ if thinking.type == "enabled":
264
+ if thinking.budget_tokens is None:
265
+ return "enabled"
266
+ return f"enabled (budget_tokens={thinking.budget_tokens})"
267
+ return "not set"
268
+
269
+ if operation.thinking is None:
270
+ raise ValueError("thinking must be provided; interactive selection belongs to UI")
271
+
272
+ current = _format_thinking_for_display(config.thinking)
273
+ config.thinking = operation.thinking
274
+ agent.session.model_thinking = operation.thinking
275
+ new_status = _format_thinking_for_display(config.thinking)
276
+
277
+ if operation.emit_switch_message:
278
+ developer_item = model.DeveloperMessageItem(
279
+ content=f"Thinking changed: {current} -> {new_status}",
280
+ command_output=model.CommandOutput(command_name=commands.CommandName.THINKING),
281
+ )
282
+ agent.session.append_history([developer_item])
283
+ await self.emit_event(events.DeveloperMessageEvent(session_id=agent.session.id, item=developer_item))
284
+
285
+ if operation.emit_welcome_event:
286
+ await self.emit_event(events.WelcomeEvent(work_dir=str(agent.session.work_dir), llm_config=config))
287
+
288
+ async def handle_clear_session(self, operation: op.ClearSessionOperation) -> None:
289
+ agent = await self._ensure_agent(operation.session_id)
290
+ new_session = Session.create(work_dir=agent.session.work_dir)
291
+ new_session.model_name = agent.session.model_name
292
+ new_session.model_config_name = agent.session.model_config_name
293
+ new_session.model_thinking = agent.session.model_thinking
294
+ agent.session = new_session
295
+
296
+ developer_item = model.DeveloperMessageItem(
297
+ content="started new conversation",
298
+ command_output=model.CommandOutput(command_name=commands.CommandName.CLEAR),
299
+ )
300
+ await self.emit_event(events.DeveloperMessageEvent(session_id=agent.session.id, item=developer_item))
187
301
 
188
- async def handle_interrupt(self, operation: op.InterruptOperation) -> None:
189
- """Handle an interrupt by invoking agent.cancel() and cancelling tasks."""
302
+ async def handle_resume_session(self, operation: op.ResumeSessionOperation) -> None:
303
+ target_session = Session.load(operation.target_session_id)
304
+ if (
305
+ target_session.model_thinking is not None
306
+ and target_session.model_name
307
+ and target_session.model_name == self.llm_clients.main.model_name
308
+ ):
309
+ self.llm_clients.main.get_llm_config().thinking = target_session.model_thinking
190
310
 
191
- # Determine affected sessions
192
- if operation.target_session_id is not None:
193
- session_ids: list[str] = [operation.target_session_id]
194
- else:
195
- session_ids = self.agent_manager.active_session_ids()
311
+ profile = self.model_profile_provider.build_profile(self.llm_clients.main)
312
+ from klaude_code.core.agent import Agent
196
313
 
197
- # Call cancel() on each affected agent to persist an interrupt marker
198
- for sid in session_ids:
199
- agent = self.agent_manager.get_active_agent(sid)
200
- if agent is not None:
201
- for evt in agent.cancel():
202
- await self.emit_event(evt)
314
+ agent = Agent(session=target_session, profile=profile)
203
315
 
204
- # emit interrupt event
205
- await self.emit_event(events.InterruptEvent(session_id=operation.target_session_id or "all"))
316
+ async for evt in agent.replay_history():
317
+ await self.emit_event(evt)
206
318
 
207
- # Find tasks to cancel (filter by target sessions if provided)
208
- if operation.target_session_id is None:
209
- session_filter: set[str] | None = None
210
- else:
211
- session_filter = {operation.target_session_id}
212
-
213
- tasks_to_cancel = self.task_manager.cancel_tasks_for_sessions(session_filter)
319
+ await self.emit_event(
320
+ events.WelcomeEvent(
321
+ work_dir=str(target_session.work_dir),
322
+ llm_config=self.llm_clients.main.get_llm_config(),
323
+ )
324
+ )
214
325
 
215
- scope = operation.target_session_id or "all"
326
+ self._agent = agent
216
327
  log_debug(
217
- f"Interrupting {len(tasks_to_cancel)} task(s) for: {scope}",
218
- style="yellow",
328
+ f"Resumed session: {target_session.id}",
329
+ style="cyan",
219
330
  debug_type=DebugType.EXECUTION,
220
331
  )
221
332
 
222
- # Cancel the tasks
223
- for task_id, task in tasks_to_cancel:
224
- task.cancel()
225
- # Remove from active tasks immediately
226
- self.task_manager.remove(task_id)
333
+ async def handle_export_session(self, operation: op.ExportSessionOperation) -> None:
334
+ agent = await self._ensure_agent(operation.session_id)
335
+ try:
336
+ output_path = self._resolve_export_output_path(operation.output_path, agent.session)
337
+ html_doc = self._build_export_html(agent)
338
+ await asyncio.to_thread(output_path.parent.mkdir, parents=True, exist_ok=True)
339
+ await asyncio.to_thread(output_path.write_text, html_doc, "utf-8")
340
+ await asyncio.to_thread(self._open_file, output_path)
341
+ developer_item = model.DeveloperMessageItem(
342
+ content=f"Session exported and opened: {output_path}",
343
+ command_output=model.CommandOutput(command_name=commands.CommandName.EXPORT),
344
+ )
345
+ agent.session.append_history([developer_item])
346
+ await self.emit_event(events.DeveloperMessageEvent(session_id=agent.session.id, item=developer_item))
347
+ except Exception as exc: # pragma: no cover
348
+ import traceback
349
+
350
+ developer_item = model.DeveloperMessageItem(
351
+ content=f"Failed to export session: {exc}\n{traceback.format_exc()}",
352
+ command_output=model.CommandOutput(command_name=commands.CommandName.EXPORT, is_error=True),
353
+ )
354
+ agent.session.append_history([developer_item])
355
+ await self.emit_event(events.DeveloperMessageEvent(session_id=agent.session.id, item=developer_item))
227
356
 
228
357
  async def _run_agent_task(
229
- self, agent: Agent, user_input: model.UserInputPayload, task_id: str, session_id: str
358
+ self,
359
+ agent: Agent,
360
+ user_input: model.UserInputPayload,
361
+ task_id: str,
362
+ session_id: str,
230
363
  ) -> None:
231
- """
232
- Run an agent task and forward all events to the UI.
233
-
234
- This method wraps the agent's run_task method and handles any exceptions
235
- that might occur during execution.
236
- """
237
364
  try:
238
365
  log_debug(
239
366
  f"Starting agent task {task_id} for session {session_id}",
@@ -241,20 +368,17 @@ class ExecutorContext:
241
368
  debug_type=DebugType.EXECUTION,
242
369
  )
243
370
 
244
- # Inject subtask runner into tool context for nested Task tool usage
245
371
  async def _runner(state: model.SubAgentState) -> SubAgentResult:
246
- return await self.subagent_manager.run_subagent(agent, state)
372
+ return await self.sub_agent_manager.run_sub_agent(agent, state)
247
373
 
248
374
  token = current_run_subtask_callback.set(_runner)
249
375
  try:
250
- # Forward all events from the agent to the UI
251
376
  async for event in agent.run_task(user_input):
252
377
  await self.emit_event(event)
253
378
  finally:
254
379
  current_run_subtask_callback.reset(token)
255
380
 
256
381
  except asyncio.CancelledError:
257
- # Task was cancelled (likely due to interrupt)
258
382
  log_debug(
259
383
  f"Agent task {task_id} was cancelled",
260
384
  style="yellow",
@@ -263,24 +387,21 @@ class ExecutorContext:
263
387
  await self.emit_event(events.TaskFinishEvent(session_id=session_id, task_result="task cancelled"))
264
388
 
265
389
  except Exception as e:
266
- # Handle any other exceptions
267
390
  import traceback
268
391
 
269
392
  log_debug(
270
- f"Agent task {task_id} failed: {str(e)}",
393
+ f"Agent task {task_id} failed: {e!s}",
271
394
  style="red",
272
395
  debug_type=DebugType.EXECUTION,
273
396
  )
274
397
  log_debug(traceback.format_exc(), style="red", debug_type=DebugType.EXECUTION)
275
398
  await self.emit_event(
276
399
  events.ErrorEvent(
277
- error_message=f"Agent task failed: [{e.__class__.__name__}] {str(e)}",
400
+ error_message=f"Agent task failed: [{e.__class__.__name__}] {e!s} {traceback.format_exc()}",
278
401
  can_retry=False,
279
402
  )
280
403
  )
281
-
282
404
  finally:
283
- # Clean up the task from active tasks
284
405
  self.task_manager.remove(task_id)
285
406
  log_debug(
286
407
  f"Cleaned up agent task {task_id}",
@@ -288,6 +409,88 @@ class ExecutorContext:
288
409
  debug_type=DebugType.EXECUTION,
289
410
  )
290
411
 
412
+ def _resolve_export_output_path(self, raw: str | None, session: Session) -> Path:
413
+ trimmed = (raw or "").strip()
414
+ if trimmed:
415
+ candidate = Path(trimmed).expanduser()
416
+ if not candidate.is_absolute():
417
+ candidate = Path(session.work_dir) / candidate
418
+ if candidate.suffix.lower() != ".html":
419
+ candidate = candidate.with_suffix(".html")
420
+ return candidate
421
+ return get_default_export_path(session)
422
+
423
+ def _build_export_html(self, agent: Agent) -> str:
424
+ profile = agent.profile
425
+ system_prompt = (profile.system_prompt if profile else "") or ""
426
+ tool_schemas = profile.tools if profile else []
427
+ model_name = profile.llm_client.model_name if profile else "unknown"
428
+ return build_export_html(agent.session, system_prompt, tool_schemas, model_name)
429
+
430
+ def _open_file(self, path: Path) -> None:
431
+ # Select platform-appropriate command
432
+ if sys.platform == "darwin":
433
+ cmd = "open"
434
+ elif sys.platform == "win32":
435
+ cmd = "start"
436
+ else:
437
+ cmd = "xdg-open"
438
+
439
+ try:
440
+ # Detach stdin to prevent interference with prompt_toolkit's terminal state
441
+ if sys.platform == "win32":
442
+ # Windows 'start' requires shell=True
443
+ subprocess.run(f'start "" "{path}"', shell=True, stdin=subprocess.DEVNULL, check=True)
444
+ else:
445
+ subprocess.run([cmd, str(path)], stdin=subprocess.DEVNULL, check=True)
446
+ except FileNotFoundError as exc: # pragma: no cover
447
+ msg = f"`{cmd}` command not found; please open the HTML manually."
448
+ raise RuntimeError(msg) from exc
449
+ except subprocess.CalledProcessError as exc: # pragma: no cover
450
+ msg = f"Failed to open HTML with `{cmd}`: {exc}"
451
+ raise RuntimeError(msg) from exc
452
+
453
+ async def handle_interrupt(self, operation: op.InterruptOperation) -> None:
454
+ """Handle an interrupt by invoking agent.cancel() and cancelling tasks."""
455
+
456
+ # Determine affected sessions
457
+ if operation.target_session_id is not None:
458
+ session_ids: list[str] = [operation.target_session_id]
459
+ else:
460
+ agent = self._agent
461
+ session_ids = [agent.session.id] if agent is not None else []
462
+
463
+ # Call cancel() on each affected agent to persist an interrupt marker
464
+ for sid in session_ids:
465
+ agent = self._get_active_agent(sid)
466
+ if agent is not None:
467
+ for evt in agent.cancel():
468
+ await self.emit_event(evt)
469
+
470
+ # emit interrupt event
471
+ await self.emit_event(events.InterruptEvent(session_id=operation.target_session_id or "all"))
472
+
473
+ # Find tasks to cancel (filter by target sessions if provided)
474
+ if operation.target_session_id is None:
475
+ session_filter: set[str] | None = None
476
+ else:
477
+ session_filter = {operation.target_session_id}
478
+
479
+ tasks_to_cancel = self.task_manager.cancel_tasks_for_sessions(session_filter)
480
+
481
+ scope = operation.target_session_id or "all"
482
+ log_debug(
483
+ f"Interrupting {len(tasks_to_cancel)} task(s) for: {scope}",
484
+ style="yellow",
485
+ debug_type=DebugType.EXECUTION,
486
+ )
487
+
488
+ # Cancel the tasks
489
+ for task_id, task in tasks_to_cancel:
490
+ task.cancel()
491
+ # Remove from active tasks immediately
492
+ self.task_manager.remove(task_id)
493
+
291
494
  def get_active_task(self, submission_id: str) -> asyncio.Task[None] | None:
292
495
  """Return the asyncio.Task for a submission id if one is registered."""
293
496
 
@@ -301,6 +504,16 @@ class ExecutorContext:
301
504
 
302
505
  return self.task_manager.get(submission_id) is not None
303
506
 
507
+ def _get_active_agent(self, session_id: str) -> Agent | None:
508
+ """Return the active agent if its session id matches ``session_id``."""
509
+
510
+ agent = self._agent
511
+ if agent is None:
512
+ return None
513
+ if agent.session.id != session_id:
514
+ return None
515
+ return agent
516
+
304
517
 
305
518
  class Executor:
306
519
  """
@@ -315,11 +528,13 @@ class Executor:
315
528
  event_queue: asyncio.Queue[events.Event],
316
529
  llm_clients: LLMClients,
317
530
  model_profile_provider: ModelProfileProvider | None = None,
531
+ on_model_change: Callable[[str], None] | None = None,
318
532
  ):
319
- self.context = ExecutorContext(event_queue, llm_clients, model_profile_provider)
533
+ self.context = ExecutorContext(event_queue, llm_clients, model_profile_provider, on_model_change)
320
534
  self.submission_queue: asyncio.Queue[op.Submission] = asyncio.Queue()
321
535
  # Track completion events for all submissions (not just those with ActiveTask)
322
536
  self._completion_events: dict[str, asyncio.Event] = {}
537
+ self._background_tasks: set[asyncio.Task[None]] = set()
323
538
 
324
539
  async def submit(self, operation: op.Operation) -> str:
325
540
  """
@@ -391,12 +606,12 @@ class Executor:
391
606
  except Exception as e:
392
607
  # Handle unexpected errors
393
608
  log_debug(
394
- f"Executor error: {str(e)}",
609
+ f"Executor error: {e!s}",
395
610
  style="red",
396
611
  debug_type=DebugType.EXECUTION,
397
612
  )
398
613
  await self.context.emit_event(
399
- events.ErrorEvent(error_message=f"Executor error: {str(e)}", can_retry=False)
614
+ events.ErrorEvent(error_message=f"Executor error: {e!s}", can_retry=False)
400
615
  )
401
616
 
402
617
  async def stop(self) -> None:
@@ -423,7 +638,7 @@ class Executor:
423
638
  await self.submission_queue.put(submission)
424
639
  except Exception as e:
425
640
  log_debug(
426
- f"Failed to send EndOperation: {str(e)}",
641
+ f"Failed to send EndOperation: {e!s}",
427
642
  style="red",
428
643
  debug_type=DebugType.EXECUTION,
429
644
  )
@@ -463,17 +678,17 @@ class Executor:
463
678
  event.set()
464
679
  else:
465
680
  # Run in background so the submission loop can continue (e.g., to handle interrupts)
466
- asyncio.create_task(_await_agent_and_complete(task))
681
+ background_task = asyncio.create_task(_await_agent_and_complete(task))
682
+ self._background_tasks.add(background_task)
683
+ background_task.add_done_callback(self._background_tasks.discard)
467
684
 
468
685
  except Exception as e:
469
686
  log_debug(
470
- f"Failed to handle submission {submission.id}: {str(e)}",
687
+ f"Failed to handle submission {submission.id}: {e!s}",
471
688
  style="red",
472
689
  debug_type=DebugType.EXECUTION,
473
690
  )
474
- await self.context.emit_event(
475
- events.ErrorEvent(error_message=f"Operation failed: {str(e)}", can_retry=False)
476
- )
691
+ await self.context.emit_event(events.ErrorEvent(error_message=f"Operation failed: {e!s}", can_retry=False))
477
692
  # Set completion event even on error to prevent wait_for_completion from hanging
478
693
  event = self._completion_events.get(submission.id)
479
694
  if event is not None:
@@ -482,4 +697,4 @@ class Executor:
482
697
 
483
698
  # Static type check: ExecutorContext must satisfy OperationHandler protocol.
484
699
  # If this line causes a type error, ExecutorContext is missing required methods.
485
- _: type[OperationHandler] = ExecutorContext # pyright: ignore[reportUnusedVariable]
700
+ _: type[OperationHandler] = ExecutorContext
@@ -1,18 +1,16 @@
1
1
  """Core runtime and state management components.
2
2
 
3
3
  Expose the manager layer via package imports to reduce module churn in
4
- callers. This keeps long-lived runtime state helpers (agents, tasks,
5
- LLM clients, sub-agents) distinct from per-session execution logic in
4
+ callers. This keeps long-lived runtime state helpers (LLM clients and
5
+ sub-agents) distinct from per-session execution logic in
6
6
  ``klaude_code.core``.
7
7
  """
8
8
 
9
- from klaude_code.core.manager.agent_manager import AgentManager
10
9
  from klaude_code.core.manager.llm_clients import LLMClients
11
10
  from klaude_code.core.manager.llm_clients_builder import build_llm_clients
12
11
  from klaude_code.core.manager.sub_agent_manager import SubAgentManager
13
12
 
14
13
  __all__ = [
15
- "AgentManager",
16
14
  "LLMClients",
17
15
  "SubAgentManager",
18
16
  "build_llm_clients",
@@ -10,12 +10,6 @@ from klaude_code.protocol.tools import SubAgentType
10
10
 
11
11
 
12
12
  def _default_sub_clients() -> dict[SubAgentType, LLMClientABC]:
13
- """Return an empty mapping for sub-agent clients.
14
-
15
- Defined separately so static type checkers can infer the dictionary
16
- key and value types instead of treating them as ``Unknown``.
17
- """
18
-
19
13
  return {}
20
14
 
21
15
 
@@ -27,15 +21,7 @@ class LLMClients:
27
21
  sub_clients: dict[SubAgentType, LLMClientABC] = dataclass_field(default_factory=_default_sub_clients)
28
22
 
29
23
  def get_client(self, sub_agent_type: SubAgentType | None = None) -> LLMClientABC:
30
- """Return client for a sub-agent type or the main client.
31
-
32
- Args:
33
- sub_agent_type: Optional sub-agent type whose client should be returned.
34
-
35
- Returns:
36
- The LLM client corresponding to the sub-agent type, or the main client
37
- when no specialized client is available.
38
- """
24
+ """Return client for a sub-agent type or the main client."""
39
25
 
40
26
  if sub_agent_type is None:
41
27
  return self.main