klaude-code 2.0.2__py3-none-any.whl → 2.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- klaude_code/app/__init__.py +12 -0
- klaude_code/app/runtime.py +215 -0
- klaude_code/cli/auth_cmd.py +2 -2
- klaude_code/cli/config_cmd.py +2 -2
- klaude_code/cli/cost_cmd.py +1 -1
- klaude_code/cli/debug.py +12 -36
- klaude_code/cli/list_model.py +3 -3
- klaude_code/cli/main.py +17 -60
- klaude_code/cli/self_update.py +2 -187
- klaude_code/cli/session_cmd.py +2 -2
- klaude_code/config/config.py +1 -1
- klaude_code/config/select_model.py +1 -1
- klaude_code/const.py +9 -1
- klaude_code/core/agent.py +9 -62
- klaude_code/core/agent_profile.py +284 -0
- klaude_code/core/executor.py +335 -230
- klaude_code/core/manager/llm_clients_builder.py +1 -1
- klaude_code/core/manager/sub_agent_manager.py +16 -29
- klaude_code/core/reminders.py +64 -99
- klaude_code/core/task.py +12 -20
- klaude_code/core/tool/__init__.py +5 -17
- klaude_code/core/tool/context.py +84 -0
- klaude_code/core/tool/file/apply_patch_tool.py +18 -21
- klaude_code/core/tool/file/edit_tool.py +39 -42
- klaude_code/core/tool/file/read_tool.py +14 -9
- klaude_code/core/tool/file/write_tool.py +12 -13
- klaude_code/core/tool/report_back_tool.py +4 -1
- klaude_code/core/tool/shell/bash_tool.py +6 -11
- klaude_code/core/tool/skill/skill_tool.py +3 -1
- klaude_code/core/tool/sub_agent_tool.py +8 -7
- klaude_code/core/tool/todo/todo_write_tool.py +3 -9
- klaude_code/core/tool/todo/update_plan_tool.py +3 -5
- klaude_code/core/tool/tool_abc.py +2 -1
- klaude_code/core/tool/tool_registry.py +2 -33
- klaude_code/core/tool/tool_runner.py +13 -10
- klaude_code/core/tool/web/mermaid_tool.py +3 -1
- klaude_code/core/tool/web/web_fetch_tool.py +5 -3
- klaude_code/core/tool/web/web_search_tool.py +5 -3
- klaude_code/core/turn.py +86 -26
- klaude_code/llm/anthropic/client.py +1 -1
- klaude_code/llm/bedrock/client.py +1 -1
- klaude_code/llm/claude/client.py +1 -1
- klaude_code/llm/codex/client.py +1 -1
- klaude_code/llm/google/client.py +1 -1
- klaude_code/llm/openai_compatible/client.py +1 -1
- klaude_code/llm/openai_compatible/tool_call_accumulator.py +1 -1
- klaude_code/llm/openrouter/client.py +1 -1
- klaude_code/llm/openrouter/reasoning.py +1 -1
- klaude_code/llm/responses/client.py +1 -1
- klaude_code/protocol/events/__init__.py +57 -0
- klaude_code/protocol/events/base.py +18 -0
- klaude_code/protocol/events/chat.py +20 -0
- klaude_code/protocol/events/lifecycle.py +22 -0
- klaude_code/protocol/events/metadata.py +15 -0
- klaude_code/protocol/events/streaming.py +43 -0
- klaude_code/protocol/events/system.py +53 -0
- klaude_code/protocol/events/tools.py +23 -0
- klaude_code/protocol/op.py +5 -0
- klaude_code/session/session.py +6 -5
- klaude_code/skill/assets/create-plan/SKILL.md +76 -0
- klaude_code/skill/loader.py +1 -1
- klaude_code/skill/system_skills.py +1 -1
- klaude_code/tui/__init__.py +8 -0
- klaude_code/{command → tui/command}/clear_cmd.py +2 -1
- klaude_code/{command → tui/command}/debug_cmd.py +3 -2
- klaude_code/{command → tui/command}/export_cmd.py +2 -1
- klaude_code/{command → tui/command}/export_online_cmd.py +2 -1
- klaude_code/{command → tui/command}/fork_session_cmd.py +4 -3
- klaude_code/{command → tui/command}/help_cmd.py +2 -1
- klaude_code/{command → tui/command}/model_cmd.py +4 -3
- klaude_code/{command → tui/command}/model_select.py +2 -2
- klaude_code/{command → tui/command}/prompt_command.py +4 -3
- klaude_code/{command → tui/command}/refresh_cmd.py +3 -1
- klaude_code/{command → tui/command}/registry.py +6 -5
- klaude_code/{command → tui/command}/release_notes_cmd.py +2 -1
- klaude_code/{command → tui/command}/resume_cmd.py +4 -3
- klaude_code/{command → tui/command}/status_cmd.py +2 -1
- klaude_code/{command → tui/command}/terminal_setup_cmd.py +2 -1
- klaude_code/{command → tui/command}/thinking_cmd.py +3 -2
- klaude_code/tui/commands.py +164 -0
- klaude_code/{ui/renderers → tui/components}/assistant.py +3 -3
- klaude_code/{ui/renderers → tui/components}/bash_syntax.py +2 -2
- klaude_code/{ui/renderers → tui/components}/common.py +1 -1
- klaude_code/{ui/renderers → tui/components}/developer.py +4 -4
- klaude_code/{ui/renderers → tui/components}/diffs.py +2 -2
- klaude_code/{ui/renderers → tui/components}/errors.py +2 -2
- klaude_code/{ui/renderers → tui/components}/metadata.py +7 -7
- klaude_code/{ui → tui/components}/rich/markdown.py +9 -23
- klaude_code/{ui → tui/components}/rich/status.py +2 -2
- klaude_code/{ui → tui/components}/rich/theme.py +3 -1
- klaude_code/{ui/renderers → tui/components}/sub_agent.py +23 -43
- klaude_code/{ui/renderers → tui/components}/thinking.py +3 -3
- klaude_code/{ui/renderers → tui/components}/tools.py +9 -9
- klaude_code/{ui/renderers → tui/components}/user_input.py +3 -20
- klaude_code/tui/display.py +85 -0
- klaude_code/{ui/modes/repl → tui/input}/__init__.py +1 -1
- klaude_code/{ui/modes/repl → tui/input}/completers.py +1 -1
- klaude_code/{ui/modes/repl/input_prompt_toolkit.py → tui/input/prompt_toolkit.py} +6 -6
- klaude_code/tui/machine.py +606 -0
- klaude_code/tui/renderer.py +707 -0
- klaude_code/tui/runner.py +321 -0
- klaude_code/tui/terminal/__init__.py +56 -0
- klaude_code/{ui → tui}/terminal/color.py +1 -1
- klaude_code/{ui → tui}/terminal/control.py +1 -1
- klaude_code/{ui → tui}/terminal/notifier.py +1 -1
- klaude_code/ui/__init__.py +6 -50
- klaude_code/ui/core/display.py +3 -3
- klaude_code/ui/core/input.py +2 -1
- klaude_code/ui/{modes/debug/display.py → debug_mode.py} +1 -1
- klaude_code/ui/{modes/exec/display.py → exec_mode.py} +0 -2
- klaude_code/ui/terminal/__init__.py +6 -54
- klaude_code/ui/terminal/title.py +31 -0
- klaude_code/update.py +163 -0
- {klaude_code-2.0.2.dist-info → klaude_code-2.1.0.dist-info}/METADATA +1 -1
- klaude_code-2.1.0.dist-info/RECORD +235 -0
- klaude_code/cli/runtime.py +0 -518
- klaude_code/core/prompt.py +0 -108
- klaude_code/core/tool/tool_context.py +0 -148
- klaude_code/protocol/events.py +0 -195
- klaude_code/skill/assets/dev-docs/SKILL.md +0 -108
- klaude_code/trace/__init__.py +0 -21
- klaude_code/ui/core/stage_manager.py +0 -48
- klaude_code/ui/modes/__init__.py +0 -1
- klaude_code/ui/modes/debug/__init__.py +0 -1
- klaude_code/ui/modes/exec/__init__.py +0 -1
- klaude_code/ui/modes/repl/display.py +0 -61
- klaude_code/ui/modes/repl/event_handler.py +0 -629
- klaude_code/ui/modes/repl/renderer.py +0 -464
- klaude_code/ui/utils/__init__.py +0 -1
- klaude_code-2.0.2.dist-info/RECORD +0 -227
- /klaude_code/{trace/log.py → log.py} +0 -0
- /klaude_code/{command → tui/command}/__init__.py +0 -0
- /klaude_code/{command → tui/command}/command_abc.py +0 -0
- /klaude_code/{command → tui/command}/prompt-commit.md +0 -0
- /klaude_code/{command → tui/command}/prompt-init.md +0 -0
- /klaude_code/{ui/renderers → tui/components}/__init__.py +0 -0
- /klaude_code/{ui/renderers → tui/components}/mermaid_viewer.py +0 -0
- /klaude_code/{ui → tui/components}/rich/__init__.py +0 -0
- /klaude_code/{ui → tui/components}/rich/cjk_wrap.py +0 -0
- /klaude_code/{ui → tui/components}/rich/code_panel.py +0 -0
- /klaude_code/{ui → tui/components}/rich/live.py +0 -0
- /klaude_code/{ui → tui/components}/rich/quote.py +0 -0
- /klaude_code/{ui → tui/components}/rich/searchable_text.py +0 -0
- /klaude_code/{ui/modes/repl → tui/input}/clipboard.py +0 -0
- /klaude_code/{ui/modes/repl → tui/input}/key_bindings.py +0 -0
- /klaude_code/{ui → tui}/terminal/image.py +0 -0
- /klaude_code/{ui → tui}/terminal/progress_bar.py +0 -0
- /klaude_code/{ui → tui}/terminal/selector.py +0 -0
- /klaude_code/ui/{utils/common.py → common.py} +0 -0
- {klaude_code-2.0.2.dist-info → klaude_code-2.1.0.dist-info}/WHEEL +0 -0
- {klaude_code-2.0.2.dist-info → klaude_code-2.1.0.dist-info}/entry_points.txt +0 -0
klaude_code/core/executor.py
CHANGED
|
@@ -10,22 +10,22 @@ from __future__ import annotations
|
|
|
10
10
|
import asyncio
|
|
11
11
|
import subprocess
|
|
12
12
|
import sys
|
|
13
|
-
from collections.abc import Callable
|
|
13
|
+
from collections.abc import Awaitable, Callable
|
|
14
14
|
from dataclasses import dataclass
|
|
15
15
|
from pathlib import Path
|
|
16
16
|
|
|
17
17
|
from klaude_code.config import load_config
|
|
18
|
-
from klaude_code.core.agent import Agent
|
|
18
|
+
from klaude_code.core.agent import Agent
|
|
19
|
+
from klaude_code.core.agent_profile import DefaultModelProfileProvider, ModelProfileProvider
|
|
19
20
|
from klaude_code.core.manager import LLMClients, SubAgentManager
|
|
20
|
-
from klaude_code.core.tool import current_run_subtask_callback
|
|
21
21
|
from klaude_code.llm.registry import create_llm_client
|
|
22
|
+
from klaude_code.log import DebugType, log_debug
|
|
22
23
|
from klaude_code.protocol import commands, events, message, model, op
|
|
23
|
-
from klaude_code.protocol.llm_param import Thinking
|
|
24
|
+
from klaude_code.protocol.llm_param import LLMConfigParameter, Thinking
|
|
24
25
|
from klaude_code.protocol.op_handler import OperationHandler
|
|
25
26
|
from klaude_code.protocol.sub_agent import SubAgentResult
|
|
26
27
|
from klaude_code.session.export import build_export_html, get_default_export_path
|
|
27
28
|
from klaude_code.session.session import Session
|
|
28
|
-
from klaude_code.trace import DebugType, log_debug
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
@dataclass
|
|
@@ -80,45 +80,26 @@ class TaskManager:
|
|
|
80
80
|
self._tasks.clear()
|
|
81
81
|
|
|
82
82
|
|
|
83
|
-
class
|
|
84
|
-
"""
|
|
85
|
-
Context object providing shared state and operations for the executor.
|
|
86
|
-
|
|
87
|
-
This context is passed to operations when they execute, allowing them
|
|
88
|
-
to access shared resources like the event queue and active sessions.
|
|
89
|
-
|
|
90
|
-
Implements the OperationHandler protocol via structural subtyping.
|
|
91
|
-
"""
|
|
83
|
+
class AgentRuntime:
|
|
84
|
+
"""Coordinate agent lifecycle and in-flight tasks for the executor."""
|
|
92
85
|
|
|
93
86
|
def __init__(
|
|
94
87
|
self,
|
|
95
|
-
|
|
88
|
+
*,
|
|
89
|
+
emit_event: Callable[[events.Event], Awaitable[None]],
|
|
96
90
|
llm_clients: LLMClients,
|
|
97
|
-
model_profile_provider: ModelProfileProvider
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
self.
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
self.
|
|
105
|
-
|
|
106
|
-
self.task_manager = TaskManager()
|
|
107
|
-
self.sub_agent_manager = SubAgentManager(event_queue, llm_clients, resolved_profile_provider)
|
|
108
|
-
self._on_model_change = on_model_change
|
|
91
|
+
model_profile_provider: ModelProfileProvider,
|
|
92
|
+
task_manager: TaskManager,
|
|
93
|
+
sub_agent_manager: SubAgentManager,
|
|
94
|
+
) -> None:
|
|
95
|
+
self._emit_event = emit_event
|
|
96
|
+
self._llm_clients = llm_clients
|
|
97
|
+
self._model_profile_provider = model_profile_provider
|
|
98
|
+
self._task_manager = task_manager
|
|
99
|
+
self._sub_agent_manager = sub_agent_manager
|
|
109
100
|
self._agent: Agent | None = None
|
|
110
101
|
|
|
111
|
-
async def emit_event(self, event: events.Event) -> None:
|
|
112
|
-
"""Emit an event to the UI display system."""
|
|
113
|
-
await self.event_queue.put(event)
|
|
114
|
-
|
|
115
102
|
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
103
|
agent = self._agent
|
|
123
104
|
if agent is None:
|
|
124
105
|
return None
|
|
@@ -126,19 +107,11 @@ class ExecutorContext:
|
|
|
126
107
|
|
|
127
108
|
@property
|
|
128
109
|
def current_agent(self) -> Agent | None:
|
|
129
|
-
"""Return the currently active agent, if any."""
|
|
130
|
-
|
|
131
110
|
return self._agent
|
|
132
111
|
|
|
133
|
-
async def
|
|
134
|
-
"""Return the active agent, creating or loading a session as needed.
|
|
112
|
+
async def ensure_agent(self, session_id: str | None = None) -> Agent:
|
|
113
|
+
"""Return the active agent, creating or loading a session as needed."""
|
|
135
114
|
|
|
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.
|
|
139
|
-
"""
|
|
140
|
-
|
|
141
|
-
# Fast-path: reuse current agent when the session id already matches.
|
|
142
115
|
if session_id is not None and self._agent is not None and self._agent.session.id == session_id:
|
|
143
116
|
return self._agent
|
|
144
117
|
|
|
@@ -147,20 +120,21 @@ class ExecutorContext:
|
|
|
147
120
|
if (
|
|
148
121
|
session.model_thinking is not None
|
|
149
122
|
and session.model_name
|
|
150
|
-
and session.model_name == self.
|
|
123
|
+
and session.model_name == self._llm_clients.main.model_name
|
|
151
124
|
):
|
|
152
|
-
self.
|
|
125
|
+
self._llm_clients.main.get_llm_config().thinking = session.model_thinking
|
|
153
126
|
|
|
154
|
-
profile = self.
|
|
127
|
+
profile = self._model_profile_provider.build_profile(self._llm_clients.main)
|
|
155
128
|
agent = Agent(session=session, profile=profile)
|
|
156
129
|
|
|
157
130
|
async for evt in agent.replay_history():
|
|
158
|
-
await self.
|
|
131
|
+
await self._emit_event(evt)
|
|
159
132
|
|
|
160
|
-
await self.
|
|
133
|
+
await self._emit_event(
|
|
161
134
|
events.WelcomeEvent(
|
|
135
|
+
session_id=session.id,
|
|
162
136
|
work_dir=str(session.work_dir),
|
|
163
|
-
llm_config=self.
|
|
137
|
+
llm_config=self._llm_clients.main.get_llm_config(),
|
|
164
138
|
)
|
|
165
139
|
)
|
|
166
140
|
|
|
@@ -172,35 +146,296 @@ class ExecutorContext:
|
|
|
172
146
|
)
|
|
173
147
|
return agent
|
|
174
148
|
|
|
175
|
-
async def
|
|
176
|
-
|
|
177
|
-
await self._ensure_agent(operation.session_id)
|
|
149
|
+
async def init_agent(self, session_id: str | None) -> None:
|
|
150
|
+
await self.ensure_agent(session_id)
|
|
178
151
|
|
|
179
|
-
async def
|
|
180
|
-
agent = await self.
|
|
181
|
-
|
|
152
|
+
async def run_agent(self, operation: op.RunAgentOperation) -> None:
|
|
153
|
+
agent = await self.ensure_agent(operation.session_id)
|
|
154
|
+
|
|
155
|
+
if operation.emit_user_message_event:
|
|
156
|
+
await self._emit_event(
|
|
157
|
+
events.UserMessageEvent(
|
|
158
|
+
content=operation.input.text,
|
|
159
|
+
session_id=agent.session.id,
|
|
160
|
+
images=operation.input.images,
|
|
161
|
+
)
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
if operation.persist_user_input:
|
|
165
|
+
agent.session.append_history(
|
|
166
|
+
[
|
|
167
|
+
message.UserMessage(
|
|
168
|
+
parts=message.parts_from_text_and_images(
|
|
169
|
+
operation.input.text,
|
|
170
|
+
operation.input.images,
|
|
171
|
+
)
|
|
172
|
+
)
|
|
173
|
+
]
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
existing_active = self._task_manager.get(operation.id)
|
|
182
177
|
if existing_active is not None and not existing_active.task.done():
|
|
183
178
|
raise RuntimeError(f"Active task already registered for operation {operation.id}")
|
|
179
|
+
|
|
184
180
|
task: asyncio.Task[None] = asyncio.create_task(
|
|
185
181
|
self._run_agent_task(agent, operation.input, operation.id, operation.session_id)
|
|
186
182
|
)
|
|
187
|
-
self.
|
|
183
|
+
self._task_manager.register(operation.id, task, operation.session_id)
|
|
188
184
|
|
|
189
|
-
async def
|
|
190
|
-
agent = await self.
|
|
191
|
-
|
|
185
|
+
async def clear_session(self, session_id: str) -> None:
|
|
186
|
+
agent = await self.ensure_agent(session_id)
|
|
187
|
+
new_session = Session.create(work_dir=agent.session.work_dir)
|
|
188
|
+
new_session.model_name = agent.session.model_name
|
|
189
|
+
new_session.model_config_name = agent.session.model_config_name
|
|
190
|
+
new_session.model_thinking = agent.session.model_thinking
|
|
191
|
+
agent.session = new_session
|
|
192
|
+
|
|
193
|
+
developer_item = message.DeveloperMessage(
|
|
194
|
+
parts=message.text_parts_from_str("started new conversation"),
|
|
195
|
+
ui_extra=model.build_command_output_extra(commands.CommandName.CLEAR),
|
|
196
|
+
)
|
|
197
|
+
await self._emit_event(events.DeveloperMessageEvent(session_id=agent.session.id, item=developer_item))
|
|
198
|
+
await self._emit_event(
|
|
199
|
+
events.WelcomeEvent(
|
|
200
|
+
session_id=agent.session.id,
|
|
201
|
+
work_dir=str(agent.session.work_dir),
|
|
202
|
+
llm_config=self._llm_clients.main.get_llm_config(),
|
|
203
|
+
)
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
async def resume_session(self, target_session_id: str) -> None:
|
|
207
|
+
target_session = Session.load(target_session_id)
|
|
208
|
+
if (
|
|
209
|
+
target_session.model_thinking is not None
|
|
210
|
+
and target_session.model_name
|
|
211
|
+
and target_session.model_name == self._llm_clients.main.model_name
|
|
212
|
+
):
|
|
213
|
+
self._llm_clients.main.get_llm_config().thinking = target_session.model_thinking
|
|
214
|
+
|
|
215
|
+
profile = self._model_profile_provider.build_profile(self._llm_clients.main)
|
|
216
|
+
agent = Agent(session=target_session, profile=profile)
|
|
217
|
+
|
|
218
|
+
async for evt in agent.replay_history():
|
|
219
|
+
await self._emit_event(evt)
|
|
220
|
+
|
|
221
|
+
await self._emit_event(
|
|
222
|
+
events.WelcomeEvent(
|
|
223
|
+
session_id=target_session.id,
|
|
224
|
+
work_dir=str(target_session.work_dir),
|
|
225
|
+
llm_config=self._llm_clients.main.get_llm_config(),
|
|
226
|
+
)
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
self._agent = agent
|
|
230
|
+
log_debug(
|
|
231
|
+
f"Resumed session: {target_session.id}",
|
|
232
|
+
style="cyan",
|
|
233
|
+
debug_type=DebugType.EXECUTION,
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
async def interrupt(self, target_session_id: str | None) -> None:
|
|
237
|
+
if target_session_id is not None:
|
|
238
|
+
session_ids: list[str] = [target_session_id]
|
|
239
|
+
else:
|
|
240
|
+
agent = self._agent
|
|
241
|
+
session_ids = [agent.session.id] if agent is not None else []
|
|
242
|
+
|
|
243
|
+
for sid in session_ids:
|
|
244
|
+
agent = self._get_active_agent(sid)
|
|
245
|
+
if agent is not None:
|
|
246
|
+
for evt in agent.cancel():
|
|
247
|
+
await self._emit_event(evt)
|
|
248
|
+
|
|
249
|
+
await self._emit_event(events.InterruptEvent(session_id=target_session_id or "all"))
|
|
250
|
+
|
|
251
|
+
if target_session_id is None:
|
|
252
|
+
session_filter: set[str] | None = None
|
|
253
|
+
else:
|
|
254
|
+
session_filter = {target_session_id}
|
|
255
|
+
|
|
256
|
+
tasks_to_cancel = self._task_manager.cancel_tasks_for_sessions(session_filter)
|
|
257
|
+
|
|
258
|
+
scope = target_session_id or "all"
|
|
259
|
+
log_debug(
|
|
260
|
+
f"Interrupting {len(tasks_to_cancel)} task(s) for: {scope}",
|
|
261
|
+
style="yellow",
|
|
262
|
+
debug_type=DebugType.EXECUTION,
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
for task_id, task in tasks_to_cancel:
|
|
266
|
+
task.cancel()
|
|
267
|
+
self._task_manager.remove(task_id)
|
|
268
|
+
|
|
269
|
+
async def _run_agent_task(
|
|
270
|
+
self,
|
|
271
|
+
agent: Agent,
|
|
272
|
+
user_input: message.UserInputPayload,
|
|
273
|
+
task_id: str,
|
|
274
|
+
session_id: str,
|
|
275
|
+
) -> None:
|
|
276
|
+
try:
|
|
277
|
+
log_debug(
|
|
278
|
+
f"Starting agent task {task_id} for session {session_id}",
|
|
279
|
+
style="green",
|
|
280
|
+
debug_type=DebugType.EXECUTION,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
async def _runner(
|
|
284
|
+
state: model.SubAgentState,
|
|
285
|
+
record_session_id: Callable[[str], None] | None,
|
|
286
|
+
) -> SubAgentResult:
|
|
287
|
+
return await self._sub_agent_manager.run_sub_agent(agent, state, record_session_id=record_session_id)
|
|
288
|
+
|
|
289
|
+
async for event in agent.run_task(user_input, run_subtask=_runner):
|
|
290
|
+
await self._emit_event(event)
|
|
291
|
+
|
|
292
|
+
except asyncio.CancelledError:
|
|
293
|
+
log_debug(
|
|
294
|
+
f"Agent task {task_id} was cancelled",
|
|
295
|
+
style="yellow",
|
|
296
|
+
debug_type=DebugType.EXECUTION,
|
|
297
|
+
)
|
|
298
|
+
await self._emit_event(events.TaskFinishEvent(session_id=session_id, task_result="task cancelled"))
|
|
299
|
+
|
|
300
|
+
except Exception as e:
|
|
301
|
+
import traceback
|
|
192
302
|
|
|
193
|
-
|
|
303
|
+
log_debug(
|
|
304
|
+
f"Agent task {task_id} failed: {e!s}",
|
|
305
|
+
style="red",
|
|
306
|
+
debug_type=DebugType.EXECUTION,
|
|
307
|
+
)
|
|
308
|
+
log_debug(traceback.format_exc(), style="red", debug_type=DebugType.EXECUTION)
|
|
309
|
+
await self._emit_event(
|
|
310
|
+
events.ErrorEvent(
|
|
311
|
+
error_message=f"Agent task failed: [{e.__class__.__name__}] {e!s} {traceback.format_exc()}",
|
|
312
|
+
can_retry=False,
|
|
313
|
+
session_id=session_id,
|
|
314
|
+
)
|
|
315
|
+
)
|
|
316
|
+
finally:
|
|
317
|
+
self._task_manager.remove(task_id)
|
|
318
|
+
log_debug(
|
|
319
|
+
f"Cleaned up agent task {task_id}",
|
|
320
|
+
style="cyan",
|
|
321
|
+
debug_type=DebugType.EXECUTION,
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
def _get_active_agent(self, session_id: str) -> Agent | None:
|
|
325
|
+
agent = self._agent
|
|
326
|
+
if agent is None:
|
|
327
|
+
return None
|
|
328
|
+
if agent.session.id != session_id:
|
|
329
|
+
return None
|
|
330
|
+
return agent
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
class ModelSwitcher:
|
|
334
|
+
"""Apply model changes to an agent session."""
|
|
335
|
+
|
|
336
|
+
def __init__(self, model_profile_provider: ModelProfileProvider) -> None:
|
|
337
|
+
self._model_profile_provider = model_profile_provider
|
|
338
|
+
|
|
339
|
+
async def change_model(
|
|
340
|
+
self,
|
|
341
|
+
agent: Agent,
|
|
342
|
+
*,
|
|
343
|
+
model_name: str,
|
|
344
|
+
save_as_default: bool,
|
|
345
|
+
) -> tuple[LLMConfigParameter, str]:
|
|
346
|
+
config = load_config()
|
|
347
|
+
llm_config = config.get_model_config(model_name)
|
|
194
348
|
llm_client = create_llm_client(llm_config)
|
|
195
|
-
agent.set_model_profile(self.
|
|
349
|
+
agent.set_model_profile(self._model_profile_provider.build_profile(llm_client))
|
|
196
350
|
|
|
197
|
-
agent.session.model_config_name =
|
|
351
|
+
agent.session.model_config_name = model_name
|
|
198
352
|
agent.session.model_thinking = llm_config.thinking
|
|
199
353
|
|
|
200
|
-
if
|
|
201
|
-
config.main_model =
|
|
354
|
+
if save_as_default:
|
|
355
|
+
config.main_model = model_name
|
|
202
356
|
await config.save()
|
|
203
357
|
|
|
358
|
+
return llm_config, llm_client.model_name
|
|
359
|
+
|
|
360
|
+
def change_thinking(self, agent: Agent, *, thinking: Thinking) -> Thinking | None:
|
|
361
|
+
"""Apply thinking configuration to the agent's active LLM config and persisted session."""
|
|
362
|
+
|
|
363
|
+
config = agent.profile.llm_client.get_llm_config()
|
|
364
|
+
previous = config.thinking
|
|
365
|
+
config.thinking = thinking
|
|
366
|
+
agent.session.model_thinking = thinking
|
|
367
|
+
return previous
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
class ExecutorContext:
|
|
371
|
+
"""
|
|
372
|
+
Context object providing shared state and operations for the executor.
|
|
373
|
+
|
|
374
|
+
This context is passed to operations when they execute, allowing them
|
|
375
|
+
to access shared resources like the event queue and active sessions.
|
|
376
|
+
|
|
377
|
+
Implements the OperationHandler protocol via structural subtyping.
|
|
378
|
+
"""
|
|
379
|
+
|
|
380
|
+
def __init__(
|
|
381
|
+
self,
|
|
382
|
+
event_queue: asyncio.Queue[events.Event],
|
|
383
|
+
llm_clients: LLMClients,
|
|
384
|
+
model_profile_provider: ModelProfileProvider | None = None,
|
|
385
|
+
on_model_change: Callable[[str], None] | None = None,
|
|
386
|
+
):
|
|
387
|
+
self.event_queue: asyncio.Queue[events.Event] = event_queue
|
|
388
|
+
self.llm_clients: LLMClients = llm_clients
|
|
389
|
+
|
|
390
|
+
resolved_profile_provider = model_profile_provider or DefaultModelProfileProvider()
|
|
391
|
+
self.model_profile_provider: ModelProfileProvider = resolved_profile_provider
|
|
392
|
+
|
|
393
|
+
self.task_manager = TaskManager()
|
|
394
|
+
self.sub_agent_manager = SubAgentManager(event_queue, llm_clients, resolved_profile_provider)
|
|
395
|
+
self._on_model_change = on_model_change
|
|
396
|
+
self._agent_runtime = AgentRuntime(
|
|
397
|
+
emit_event=self.emit_event,
|
|
398
|
+
llm_clients=llm_clients,
|
|
399
|
+
model_profile_provider=resolved_profile_provider,
|
|
400
|
+
task_manager=self.task_manager,
|
|
401
|
+
sub_agent_manager=self.sub_agent_manager,
|
|
402
|
+
)
|
|
403
|
+
self._model_switcher = ModelSwitcher(resolved_profile_provider)
|
|
404
|
+
|
|
405
|
+
async def emit_event(self, event: events.Event) -> None:
|
|
406
|
+
"""Emit an event to the UI display system."""
|
|
407
|
+
await self.event_queue.put(event)
|
|
408
|
+
|
|
409
|
+
def current_session_id(self) -> str | None:
|
|
410
|
+
"""Return the primary active session id, if any.
|
|
411
|
+
|
|
412
|
+
This is a convenience wrapper used by the CLI, which conceptually
|
|
413
|
+
operates on a single interactive session per process.
|
|
414
|
+
"""
|
|
415
|
+
|
|
416
|
+
return self._agent_runtime.current_session_id()
|
|
417
|
+
|
|
418
|
+
@property
|
|
419
|
+
def current_agent(self) -> Agent | None:
|
|
420
|
+
"""Return the currently active agent, if any."""
|
|
421
|
+
|
|
422
|
+
return self._agent_runtime.current_agent
|
|
423
|
+
|
|
424
|
+
async def handle_init_agent(self, operation: op.InitAgentOperation) -> None:
|
|
425
|
+
"""Initialize an agent for a session and replay history to UI."""
|
|
426
|
+
await self._agent_runtime.init_agent(operation.session_id)
|
|
427
|
+
|
|
428
|
+
async def handle_run_agent(self, operation: op.RunAgentOperation) -> None:
|
|
429
|
+
await self._agent_runtime.run_agent(operation)
|
|
430
|
+
|
|
431
|
+
async def handle_change_model(self, operation: op.ChangeModelOperation) -> None:
|
|
432
|
+
agent = await self._agent_runtime.ensure_agent(operation.session_id)
|
|
433
|
+
llm_config, llm_client_name = await self._model_switcher.change_model(
|
|
434
|
+
agent,
|
|
435
|
+
model_name=operation.model_name,
|
|
436
|
+
save_as_default=operation.save_as_default,
|
|
437
|
+
)
|
|
438
|
+
|
|
204
439
|
if operation.emit_switch_message:
|
|
205
440
|
default_note = " (saved as default)" if operation.save_as_default else ""
|
|
206
441
|
developer_item = message.DeveloperMessage(
|
|
@@ -211,12 +446,15 @@ class ExecutorContext:
|
|
|
211
446
|
await self.emit_event(events.DeveloperMessageEvent(session_id=agent.session.id, item=developer_item))
|
|
212
447
|
|
|
213
448
|
if self._on_model_change is not None:
|
|
214
|
-
self._on_model_change(
|
|
449
|
+
self._on_model_change(llm_client_name)
|
|
215
450
|
|
|
216
451
|
if operation.emit_welcome_event:
|
|
217
452
|
await self.emit_event(
|
|
218
453
|
events.WelcomeEvent(
|
|
219
|
-
|
|
454
|
+
session_id=agent.session.id,
|
|
455
|
+
llm_config=llm_config,
|
|
456
|
+
work_dir=str(agent.session.work_dir),
|
|
457
|
+
show_klaude_code_info=False,
|
|
220
458
|
)
|
|
221
459
|
)
|
|
222
460
|
|
|
@@ -226,9 +464,7 @@ class ExecutorContext:
|
|
|
226
464
|
Interactive thinking selection must happen in the UI/CLI layer. Core only
|
|
227
465
|
applies a concrete thinking configuration.
|
|
228
466
|
"""
|
|
229
|
-
agent = await self.
|
|
230
|
-
|
|
231
|
-
config = agent.profile.llm_client.get_llm_config()
|
|
467
|
+
agent = await self._agent_runtime.ensure_agent(operation.session_id)
|
|
232
468
|
|
|
233
469
|
def _format_thinking_for_display(thinking: Thinking | None) -> str:
|
|
234
470
|
if thinking is None:
|
|
@@ -246,10 +482,9 @@ class ExecutorContext:
|
|
|
246
482
|
if operation.thinking is None:
|
|
247
483
|
raise ValueError("thinking must be provided; interactive selection belongs to UI")
|
|
248
484
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
new_status = _format_thinking_for_display(config.thinking)
|
|
485
|
+
previous = self._model_switcher.change_thinking(agent, thinking=operation.thinking)
|
|
486
|
+
current = _format_thinking_for_display(previous)
|
|
487
|
+
new_status = _format_thinking_for_display(operation.thinking)
|
|
253
488
|
|
|
254
489
|
if operation.emit_switch_message:
|
|
255
490
|
developer_item = message.DeveloperMessage(
|
|
@@ -262,63 +497,21 @@ class ExecutorContext:
|
|
|
262
497
|
if operation.emit_welcome_event:
|
|
263
498
|
await self.emit_event(
|
|
264
499
|
events.WelcomeEvent(
|
|
265
|
-
|
|
500
|
+
session_id=agent.session.id,
|
|
501
|
+
work_dir=str(agent.session.work_dir),
|
|
502
|
+
llm_config=agent.profile.llm_client.get_llm_config(),
|
|
503
|
+
show_klaude_code_info=False,
|
|
266
504
|
)
|
|
267
505
|
)
|
|
268
506
|
|
|
269
507
|
async def handle_clear_session(self, operation: op.ClearSessionOperation) -> None:
|
|
270
|
-
|
|
271
|
-
new_session = Session.create(work_dir=agent.session.work_dir)
|
|
272
|
-
new_session.model_name = agent.session.model_name
|
|
273
|
-
new_session.model_config_name = agent.session.model_config_name
|
|
274
|
-
new_session.model_thinking = agent.session.model_thinking
|
|
275
|
-
agent.session = new_session
|
|
276
|
-
|
|
277
|
-
developer_item = message.DeveloperMessage(
|
|
278
|
-
parts=message.text_parts_from_str("started new conversation"),
|
|
279
|
-
ui_extra=model.build_command_output_extra(commands.CommandName.CLEAR),
|
|
280
|
-
)
|
|
281
|
-
await self.emit_event(events.DeveloperMessageEvent(session_id=agent.session.id, item=developer_item))
|
|
282
|
-
await self.emit_event(
|
|
283
|
-
events.WelcomeEvent(
|
|
284
|
-
work_dir=str(agent.session.work_dir),
|
|
285
|
-
llm_config=self.llm_clients.main.get_llm_config(),
|
|
286
|
-
)
|
|
287
|
-
)
|
|
508
|
+
await self._agent_runtime.clear_session(operation.session_id)
|
|
288
509
|
|
|
289
510
|
async def handle_resume_session(self, operation: op.ResumeSessionOperation) -> None:
|
|
290
|
-
|
|
291
|
-
if (
|
|
292
|
-
target_session.model_thinking is not None
|
|
293
|
-
and target_session.model_name
|
|
294
|
-
and target_session.model_name == self.llm_clients.main.model_name
|
|
295
|
-
):
|
|
296
|
-
self.llm_clients.main.get_llm_config().thinking = target_session.model_thinking
|
|
297
|
-
|
|
298
|
-
profile = self.model_profile_provider.build_profile(self.llm_clients.main)
|
|
299
|
-
from klaude_code.core.agent import Agent
|
|
300
|
-
|
|
301
|
-
agent = Agent(session=target_session, profile=profile)
|
|
302
|
-
|
|
303
|
-
async for evt in agent.replay_history():
|
|
304
|
-
await self.emit_event(evt)
|
|
305
|
-
|
|
306
|
-
await self.emit_event(
|
|
307
|
-
events.WelcomeEvent(
|
|
308
|
-
work_dir=str(target_session.work_dir),
|
|
309
|
-
llm_config=self.llm_clients.main.get_llm_config(),
|
|
310
|
-
)
|
|
311
|
-
)
|
|
312
|
-
|
|
313
|
-
self._agent = agent
|
|
314
|
-
log_debug(
|
|
315
|
-
f"Resumed session: {target_session.id}",
|
|
316
|
-
style="cyan",
|
|
317
|
-
debug_type=DebugType.EXECUTION,
|
|
318
|
-
)
|
|
511
|
+
await self._agent_runtime.resume_session(operation.target_session_id)
|
|
319
512
|
|
|
320
513
|
async def handle_export_session(self, operation: op.ExportSessionOperation) -> None:
|
|
321
|
-
agent = await self.
|
|
514
|
+
agent = await self._agent_runtime.ensure_agent(operation.session_id)
|
|
322
515
|
try:
|
|
323
516
|
output_path = self._resolve_export_output_path(operation.output_path, agent.session)
|
|
324
517
|
html_doc = self._build_export_html(agent)
|
|
@@ -341,61 +534,6 @@ class ExecutorContext:
|
|
|
341
534
|
agent.session.append_history([developer_item])
|
|
342
535
|
await self.emit_event(events.DeveloperMessageEvent(session_id=agent.session.id, item=developer_item))
|
|
343
536
|
|
|
344
|
-
async def _run_agent_task(
|
|
345
|
-
self,
|
|
346
|
-
agent: Agent,
|
|
347
|
-
user_input: message.UserInputPayload,
|
|
348
|
-
task_id: str,
|
|
349
|
-
session_id: str,
|
|
350
|
-
) -> None:
|
|
351
|
-
try:
|
|
352
|
-
log_debug(
|
|
353
|
-
f"Starting agent task {task_id} for session {session_id}",
|
|
354
|
-
style="green",
|
|
355
|
-
debug_type=DebugType.EXECUTION,
|
|
356
|
-
)
|
|
357
|
-
|
|
358
|
-
async def _runner(state: model.SubAgentState) -> SubAgentResult:
|
|
359
|
-
return await self.sub_agent_manager.run_sub_agent(agent, state)
|
|
360
|
-
|
|
361
|
-
token = current_run_subtask_callback.set(_runner)
|
|
362
|
-
try:
|
|
363
|
-
async for event in agent.run_task(user_input):
|
|
364
|
-
await self.emit_event(event)
|
|
365
|
-
finally:
|
|
366
|
-
current_run_subtask_callback.reset(token)
|
|
367
|
-
|
|
368
|
-
except asyncio.CancelledError:
|
|
369
|
-
log_debug(
|
|
370
|
-
f"Agent task {task_id} was cancelled",
|
|
371
|
-
style="yellow",
|
|
372
|
-
debug_type=DebugType.EXECUTION,
|
|
373
|
-
)
|
|
374
|
-
await self.emit_event(events.TaskFinishEvent(session_id=session_id, task_result="task cancelled"))
|
|
375
|
-
|
|
376
|
-
except Exception as e:
|
|
377
|
-
import traceback
|
|
378
|
-
|
|
379
|
-
log_debug(
|
|
380
|
-
f"Agent task {task_id} failed: {e!s}",
|
|
381
|
-
style="red",
|
|
382
|
-
debug_type=DebugType.EXECUTION,
|
|
383
|
-
)
|
|
384
|
-
log_debug(traceback.format_exc(), style="red", debug_type=DebugType.EXECUTION)
|
|
385
|
-
await self.emit_event(
|
|
386
|
-
events.ErrorEvent(
|
|
387
|
-
error_message=f"Agent task failed: [{e.__class__.__name__}] {e!s} {traceback.format_exc()}",
|
|
388
|
-
can_retry=False,
|
|
389
|
-
)
|
|
390
|
-
)
|
|
391
|
-
finally:
|
|
392
|
-
self.task_manager.remove(task_id)
|
|
393
|
-
log_debug(
|
|
394
|
-
f"Cleaned up agent task {task_id}",
|
|
395
|
-
style="cyan",
|
|
396
|
-
debug_type=DebugType.EXECUTION,
|
|
397
|
-
)
|
|
398
|
-
|
|
399
537
|
def _resolve_export_output_path(self, raw: str | None, session: Session) -> Path:
|
|
400
538
|
trimmed = (raw or "").strip()
|
|
401
539
|
if trimmed:
|
|
@@ -440,43 +578,7 @@ class ExecutorContext:
|
|
|
440
578
|
async def handle_interrupt(self, operation: op.InterruptOperation) -> None:
|
|
441
579
|
"""Handle an interrupt by invoking agent.cancel() and cancelling tasks."""
|
|
442
580
|
|
|
443
|
-
|
|
444
|
-
if operation.target_session_id is not None:
|
|
445
|
-
session_ids: list[str] = [operation.target_session_id]
|
|
446
|
-
else:
|
|
447
|
-
agent = self._agent
|
|
448
|
-
session_ids = [agent.session.id] if agent is not None else []
|
|
449
|
-
|
|
450
|
-
# Call cancel() on each affected agent to persist an interrupt marker
|
|
451
|
-
for sid in session_ids:
|
|
452
|
-
agent = self._get_active_agent(sid)
|
|
453
|
-
if agent is not None:
|
|
454
|
-
for evt in agent.cancel():
|
|
455
|
-
await self.emit_event(evt)
|
|
456
|
-
|
|
457
|
-
# emit interrupt event
|
|
458
|
-
await self.emit_event(events.InterruptEvent(session_id=operation.target_session_id or "all"))
|
|
459
|
-
|
|
460
|
-
# Find tasks to cancel (filter by target sessions if provided)
|
|
461
|
-
if operation.target_session_id is None:
|
|
462
|
-
session_filter: set[str] | None = None
|
|
463
|
-
else:
|
|
464
|
-
session_filter = {operation.target_session_id}
|
|
465
|
-
|
|
466
|
-
tasks_to_cancel = self.task_manager.cancel_tasks_for_sessions(session_filter)
|
|
467
|
-
|
|
468
|
-
scope = operation.target_session_id or "all"
|
|
469
|
-
log_debug(
|
|
470
|
-
f"Interrupting {len(tasks_to_cancel)} task(s) for: {scope}",
|
|
471
|
-
style="yellow",
|
|
472
|
-
debug_type=DebugType.EXECUTION,
|
|
473
|
-
)
|
|
474
|
-
|
|
475
|
-
# Cancel the tasks
|
|
476
|
-
for task_id, task in tasks_to_cancel:
|
|
477
|
-
task.cancel()
|
|
478
|
-
# Remove from active tasks immediately
|
|
479
|
-
self.task_manager.remove(task_id)
|
|
581
|
+
await self._agent_runtime.interrupt(operation.target_session_id)
|
|
480
582
|
|
|
481
583
|
def get_active_task(self, submission_id: str) -> asyncio.Task[None] | None:
|
|
482
584
|
"""Return the asyncio.Task for a submission id if one is registered."""
|
|
@@ -491,16 +593,6 @@ class ExecutorContext:
|
|
|
491
593
|
|
|
492
594
|
return self.task_manager.get(submission_id) is not None
|
|
493
595
|
|
|
494
|
-
def _get_active_agent(self, session_id: str) -> Agent | None:
|
|
495
|
-
"""Return the active agent if its session id matches ``session_id``."""
|
|
496
|
-
|
|
497
|
-
agent = self._agent
|
|
498
|
-
if agent is None:
|
|
499
|
-
return None
|
|
500
|
-
if agent.session.id != session_id:
|
|
501
|
-
return None
|
|
502
|
-
return agent
|
|
503
|
-
|
|
504
596
|
|
|
505
597
|
class Executor:
|
|
506
598
|
"""
|
|
@@ -598,7 +690,11 @@ class Executor:
|
|
|
598
690
|
debug_type=DebugType.EXECUTION,
|
|
599
691
|
)
|
|
600
692
|
await self.context.emit_event(
|
|
601
|
-
events.ErrorEvent(
|
|
693
|
+
events.ErrorEvent(
|
|
694
|
+
error_message=f"Executor error: {e!s}",
|
|
695
|
+
can_retry=False,
|
|
696
|
+
session_id="__app__",
|
|
697
|
+
)
|
|
602
698
|
)
|
|
603
699
|
|
|
604
700
|
async def stop(self) -> None:
|
|
@@ -675,7 +771,16 @@ class Executor:
|
|
|
675
771
|
style="red",
|
|
676
772
|
debug_type=DebugType.EXECUTION,
|
|
677
773
|
)
|
|
678
|
-
|
|
774
|
+
session_id = getattr(submission.operation, "session_id", None) or getattr(
|
|
775
|
+
submission.operation, "target_session_id", None
|
|
776
|
+
)
|
|
777
|
+
await self.context.emit_event(
|
|
778
|
+
events.ErrorEvent(
|
|
779
|
+
error_message=f"Operation failed: {e!s}",
|
|
780
|
+
can_retry=False,
|
|
781
|
+
session_id=session_id or "__app__",
|
|
782
|
+
)
|
|
783
|
+
)
|
|
679
784
|
# Set completion event even on error to prevent wait_for_completion from hanging
|
|
680
785
|
event = self._completion_events.get(submission.id)
|
|
681
786
|
if event is not None:
|