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.
- klaude_code/auth/__init__.py +24 -0
- klaude_code/auth/codex/__init__.py +20 -0
- klaude_code/auth/codex/exceptions.py +17 -0
- klaude_code/auth/codex/jwt_utils.py +45 -0
- klaude_code/auth/codex/oauth.py +229 -0
- klaude_code/auth/codex/token_manager.py +84 -0
- klaude_code/cli/auth_cmd.py +73 -0
- klaude_code/cli/config_cmd.py +91 -0
- klaude_code/cli/cost_cmd.py +338 -0
- klaude_code/cli/debug.py +78 -0
- klaude_code/cli/list_model.py +307 -0
- klaude_code/cli/main.py +233 -134
- klaude_code/cli/runtime.py +309 -117
- klaude_code/{version.py → cli/self_update.py} +114 -5
- klaude_code/cli/session_cmd.py +37 -21
- klaude_code/command/__init__.py +88 -27
- klaude_code/command/clear_cmd.py +8 -7
- klaude_code/command/command_abc.py +31 -31
- klaude_code/command/debug_cmd.py +79 -0
- klaude_code/command/export_cmd.py +19 -53
- klaude_code/command/export_online_cmd.py +154 -0
- klaude_code/command/fork_session_cmd.py +267 -0
- klaude_code/command/help_cmd.py +7 -8
- klaude_code/command/model_cmd.py +60 -10
- klaude_code/command/model_select.py +84 -0
- klaude_code/command/prompt-jj-describe.md +32 -0
- klaude_code/command/prompt_command.py +19 -11
- klaude_code/command/refresh_cmd.py +8 -10
- klaude_code/command/registry.py +139 -40
- klaude_code/command/release_notes_cmd.py +84 -0
- klaude_code/command/resume_cmd.py +111 -0
- klaude_code/command/status_cmd.py +104 -60
- klaude_code/command/terminal_setup_cmd.py +7 -9
- klaude_code/command/thinking_cmd.py +98 -0
- klaude_code/config/__init__.py +14 -6
- klaude_code/config/assets/__init__.py +1 -0
- klaude_code/config/assets/builtin_config.yaml +303 -0
- klaude_code/config/builtin_config.py +38 -0
- klaude_code/config/config.py +378 -109
- klaude_code/config/select_model.py +117 -53
- klaude_code/config/thinking.py +269 -0
- klaude_code/{const/__init__.py → const.py} +50 -19
- klaude_code/core/agent.py +20 -28
- klaude_code/core/executor.py +327 -112
- klaude_code/core/manager/__init__.py +2 -4
- klaude_code/core/manager/llm_clients.py +1 -15
- klaude_code/core/manager/llm_clients_builder.py +10 -11
- klaude_code/core/manager/sub_agent_manager.py +37 -6
- klaude_code/core/prompt.py +63 -44
- klaude_code/core/prompts/prompt-claude-code.md +2 -13
- klaude_code/core/prompts/prompt-codex-gpt-5-1-codex-max.md +117 -0
- klaude_code/core/prompts/prompt-codex-gpt-5-2-codex.md +117 -0
- klaude_code/core/prompts/prompt-codex.md +9 -42
- klaude_code/core/prompts/prompt-minimal.md +12 -0
- klaude_code/core/prompts/{prompt-subagent-explore.md → prompt-sub-agent-explore.md} +16 -3
- klaude_code/core/prompts/{prompt-subagent-oracle.md → prompt-sub-agent-oracle.md} +1 -2
- klaude_code/core/prompts/prompt-sub-agent-web.md +51 -0
- klaude_code/core/reminders.py +283 -95
- klaude_code/core/task.py +113 -75
- klaude_code/core/tool/__init__.py +24 -31
- klaude_code/core/tool/file/_utils.py +36 -0
- klaude_code/core/tool/file/apply_patch.py +17 -25
- klaude_code/core/tool/file/apply_patch_tool.py +57 -77
- klaude_code/core/tool/file/diff_builder.py +151 -0
- klaude_code/core/tool/file/edit_tool.py +50 -63
- klaude_code/core/tool/file/move_tool.md +41 -0
- klaude_code/core/tool/file/move_tool.py +435 -0
- klaude_code/core/tool/file/read_tool.md +1 -1
- klaude_code/core/tool/file/read_tool.py +86 -86
- klaude_code/core/tool/file/write_tool.py +59 -69
- klaude_code/core/tool/report_back_tool.py +84 -0
- klaude_code/core/tool/shell/bash_tool.py +265 -22
- klaude_code/core/tool/shell/command_safety.py +3 -6
- klaude_code/core/tool/{memory → skill}/skill_tool.py +16 -26
- klaude_code/core/tool/sub_agent_tool.py +13 -2
- klaude_code/core/tool/todo/todo_write_tool.md +0 -157
- klaude_code/core/tool/todo/todo_write_tool.py +1 -1
- klaude_code/core/tool/todo/todo_write_tool_raw.md +182 -0
- klaude_code/core/tool/todo/update_plan_tool.py +1 -1
- klaude_code/core/tool/tool_abc.py +18 -0
- klaude_code/core/tool/tool_context.py +27 -12
- klaude_code/core/tool/tool_registry.py +7 -7
- klaude_code/core/tool/tool_runner.py +44 -36
- klaude_code/core/tool/truncation.py +29 -14
- klaude_code/core/tool/web/mermaid_tool.md +43 -0
- klaude_code/core/tool/web/mermaid_tool.py +2 -5
- klaude_code/core/tool/web/web_fetch_tool.md +1 -1
- klaude_code/core/tool/web/web_fetch_tool.py +112 -22
- klaude_code/core/tool/web/web_search_tool.md +23 -0
- klaude_code/core/tool/web/web_search_tool.py +130 -0
- klaude_code/core/turn.py +168 -66
- klaude_code/llm/__init__.py +2 -10
- klaude_code/llm/anthropic/client.py +190 -178
- klaude_code/llm/anthropic/input.py +39 -15
- klaude_code/llm/bedrock/__init__.py +3 -0
- klaude_code/llm/bedrock/client.py +60 -0
- klaude_code/llm/client.py +7 -21
- klaude_code/llm/codex/__init__.py +5 -0
- klaude_code/llm/codex/client.py +149 -0
- klaude_code/llm/google/__init__.py +3 -0
- klaude_code/llm/google/client.py +309 -0
- klaude_code/llm/google/input.py +215 -0
- klaude_code/llm/input_common.py +3 -9
- klaude_code/llm/openai_compatible/client.py +72 -164
- klaude_code/llm/openai_compatible/input.py +6 -4
- klaude_code/llm/openai_compatible/stream.py +273 -0
- klaude_code/llm/openai_compatible/tool_call_accumulator.py +17 -1
- klaude_code/llm/openrouter/client.py +89 -160
- klaude_code/llm/openrouter/input.py +18 -30
- klaude_code/llm/openrouter/reasoning.py +118 -0
- klaude_code/llm/registry.py +39 -7
- klaude_code/llm/responses/client.py +184 -171
- klaude_code/llm/responses/input.py +20 -1
- klaude_code/llm/usage.py +17 -12
- klaude_code/protocol/commands.py +17 -1
- klaude_code/protocol/events.py +31 -4
- klaude_code/protocol/llm_param.py +13 -10
- klaude_code/protocol/model.py +232 -29
- klaude_code/protocol/op.py +90 -1
- klaude_code/protocol/op_handler.py +35 -1
- klaude_code/protocol/sub_agent/__init__.py +117 -0
- klaude_code/protocol/sub_agent/explore.py +63 -0
- klaude_code/protocol/sub_agent/oracle.py +91 -0
- klaude_code/protocol/sub_agent/task.py +61 -0
- klaude_code/protocol/sub_agent/web.py +79 -0
- klaude_code/protocol/tools.py +4 -2
- klaude_code/session/__init__.py +2 -2
- klaude_code/session/codec.py +71 -0
- klaude_code/session/export.py +293 -86
- klaude_code/session/selector.py +89 -67
- klaude_code/session/session.py +320 -309
- klaude_code/session/store.py +220 -0
- klaude_code/session/templates/export_session.html +595 -83
- klaude_code/session/templates/mermaid_viewer.html +926 -0
- klaude_code/skill/__init__.py +27 -0
- klaude_code/skill/assets/deslop/SKILL.md +17 -0
- klaude_code/skill/assets/dev-docs/SKILL.md +108 -0
- klaude_code/skill/assets/handoff/SKILL.md +39 -0
- klaude_code/skill/assets/jj-workspace/SKILL.md +20 -0
- klaude_code/skill/assets/skill-creator/SKILL.md +139 -0
- klaude_code/{core/tool/memory/skill_loader.py → skill/loader.py} +55 -15
- klaude_code/skill/manager.py +70 -0
- klaude_code/skill/system_skills.py +192 -0
- klaude_code/trace/__init__.py +20 -2
- klaude_code/trace/log.py +150 -5
- klaude_code/ui/__init__.py +4 -9
- klaude_code/ui/core/input.py +1 -1
- klaude_code/ui/core/stage_manager.py +7 -7
- klaude_code/ui/modes/debug/display.py +2 -1
- klaude_code/ui/modes/repl/__init__.py +3 -48
- klaude_code/ui/modes/repl/clipboard.py +5 -5
- klaude_code/ui/modes/repl/completers.py +487 -123
- klaude_code/ui/modes/repl/display.py +5 -4
- klaude_code/ui/modes/repl/event_handler.py +370 -117
- klaude_code/ui/modes/repl/input_prompt_toolkit.py +552 -105
- klaude_code/ui/modes/repl/key_bindings.py +146 -23
- klaude_code/ui/modes/repl/renderer.py +189 -99
- klaude_code/ui/renderers/assistant.py +9 -2
- klaude_code/ui/renderers/bash_syntax.py +178 -0
- klaude_code/ui/renderers/common.py +78 -0
- klaude_code/ui/renderers/developer.py +104 -48
- klaude_code/ui/renderers/diffs.py +87 -6
- klaude_code/ui/renderers/errors.py +11 -6
- klaude_code/ui/renderers/mermaid_viewer.py +57 -0
- klaude_code/ui/renderers/metadata.py +112 -76
- klaude_code/ui/renderers/sub_agent.py +92 -7
- klaude_code/ui/renderers/thinking.py +40 -18
- klaude_code/ui/renderers/tools.py +405 -227
- klaude_code/ui/renderers/user_input.py +73 -13
- klaude_code/ui/rich/__init__.py +10 -1
- klaude_code/ui/rich/cjk_wrap.py +228 -0
- klaude_code/ui/rich/code_panel.py +131 -0
- klaude_code/ui/rich/live.py +17 -0
- klaude_code/ui/rich/markdown.py +305 -170
- klaude_code/ui/rich/searchable_text.py +10 -13
- klaude_code/ui/rich/status.py +190 -49
- klaude_code/ui/rich/theme.py +135 -39
- klaude_code/ui/terminal/__init__.py +55 -0
- klaude_code/ui/terminal/color.py +1 -1
- klaude_code/ui/terminal/control.py +13 -22
- klaude_code/ui/terminal/notifier.py +44 -4
- klaude_code/ui/terminal/selector.py +658 -0
- klaude_code/ui/utils/common.py +0 -18
- klaude_code-1.8.0.dist-info/METADATA +377 -0
- klaude_code-1.8.0.dist-info/RECORD +219 -0
- {klaude_code-1.2.6.dist-info → klaude_code-1.8.0.dist-info}/entry_points.txt +1 -0
- klaude_code/command/diff_cmd.py +0 -138
- klaude_code/command/prompt-dev-docs-update.md +0 -56
- klaude_code/command/prompt-dev-docs.md +0 -46
- klaude_code/config/list_model.py +0 -162
- klaude_code/core/manager/agent_manager.py +0 -127
- klaude_code/core/prompts/prompt-subagent-webfetch.md +0 -46
- klaude_code/core/tool/file/multi_edit_tool.md +0 -42
- klaude_code/core/tool/file/multi_edit_tool.py +0 -199
- klaude_code/core/tool/memory/memory_tool.md +0 -16
- klaude_code/core/tool/memory/memory_tool.py +0 -462
- klaude_code/llm/openrouter/reasoning_handler.py +0 -209
- klaude_code/protocol/sub_agent.py +0 -348
- klaude_code/ui/utils/debouncer.py +0 -42
- klaude_code-1.2.6.dist-info/METADATA +0 -178
- klaude_code-1.2.6.dist-info/RECORD +0 -167
- /klaude_code/core/prompts/{prompt-subagent.md → prompt-sub-agent.md} +0 -0
- /klaude_code/core/tool/{memory → skill}/__init__.py +0 -0
- /klaude_code/core/tool/{memory → skill}/skill_tool.md +0 -0
- {klaude_code-1.2.6.dist-info → klaude_code-1.8.0.dist-info}/WHEEL +0 -0
klaude_code/core/executor.py
CHANGED
|
@@ -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.
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
107
|
-
"""
|
|
128
|
+
def current_agent(self) -> Agent | None:
|
|
129
|
+
"""Return the currently active agent, if any."""
|
|
130
|
+
|
|
131
|
+
return self._agent
|
|
108
132
|
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
151
|
-
await self.emit_event(evt)
|
|
204
|
+
)
|
|
152
205
|
|
|
153
|
-
|
|
154
|
-
|
|
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
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
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
|
-
|
|
163
|
-
|
|
224
|
+
agent.session.model_config_name = operation.model_name
|
|
225
|
+
agent.session.model_thinking = llm_config.thinking
|
|
164
226
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
227
|
+
if operation.save_as_default:
|
|
228
|
+
config.main_model = operation.model_name
|
|
229
|
+
await config.save()
|
|
168
230
|
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
173
|
-
|
|
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
|
|
176
|
-
|
|
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
|
-
|
|
180
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
return
|
|
246
|
+
async def handle_change_thinking(self, operation: op.ChangeThinkingOperation) -> None:
|
|
247
|
+
"""Handle a change thinking operation.
|
|
185
248
|
|
|
186
|
-
|
|
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
|
|
189
|
-
|
|
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
|
-
|
|
192
|
-
|
|
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
|
-
|
|
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
|
-
|
|
205
|
-
|
|
316
|
+
async for evt in agent.replay_history():
|
|
317
|
+
await self.emit_event(evt)
|
|
206
318
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
326
|
+
self._agent = agent
|
|
216
327
|
log_debug(
|
|
217
|
-
f"
|
|
218
|
-
style="
|
|
328
|
+
f"Resumed session: {target_session.id}",
|
|
329
|
+
style="cyan",
|
|
219
330
|
debug_type=DebugType.EXECUTION,
|
|
220
331
|
)
|
|
221
332
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
self.
|
|
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,
|
|
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.
|
|
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: {
|
|
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__}] {
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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}: {
|
|
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
|
|
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 (
|
|
5
|
-
|
|
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
|