klaude-code 1.2.17__py3-none-any.whl → 1.2.18__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/cli/config_cmd.py +1 -1
- klaude_code/cli/debug.py +1 -1
- klaude_code/cli/main.py +3 -9
- klaude_code/cli/runtime.py +10 -13
- klaude_code/command/__init__.py +4 -1
- klaude_code/command/clear_cmd.py +2 -7
- klaude_code/command/command_abc.py +33 -5
- klaude_code/command/debug_cmd.py +79 -0
- klaude_code/command/diff_cmd.py +2 -6
- klaude_code/command/export_cmd.py +7 -7
- klaude_code/command/export_online_cmd.py +1 -5
- klaude_code/command/help_cmd.py +4 -9
- klaude_code/command/model_cmd.py +10 -6
- klaude_code/command/prompt_command.py +2 -6
- klaude_code/command/refresh_cmd.py +2 -7
- klaude_code/command/registry.py +2 -4
- klaude_code/command/release_notes_cmd.py +2 -6
- klaude_code/command/status_cmd.py +2 -7
- klaude_code/command/terminal_setup_cmd.py +2 -6
- klaude_code/command/thinking_cmd.py +13 -8
- klaude_code/config/select_model.py +81 -5
- klaude_code/const/__init__.py +1 -1
- klaude_code/core/executor.py +236 -109
- klaude_code/core/manager/__init__.py +2 -4
- klaude_code/core/prompts/prompt-claude-code.md +1 -1
- klaude_code/core/prompts/prompt-sub-agent-web.md +8 -5
- klaude_code/core/reminders.py +9 -35
- klaude_code/core/tool/file/read_tool.py +38 -10
- klaude_code/core/tool/shell/bash_tool.py +22 -2
- klaude_code/core/tool/tool_runner.py +26 -23
- klaude_code/core/tool/truncation.py +23 -9
- klaude_code/core/tool/web/web_fetch_tool.md +1 -1
- klaude_code/core/tool/web/web_fetch_tool.py +36 -1
- klaude_code/core/turn.py +28 -0
- klaude_code/protocol/commands.py +1 -0
- klaude_code/protocol/sub_agent/web.py +3 -2
- klaude_code/session/session.py +2 -2
- klaude_code/session/templates/export_session.html +24 -13
- klaude_code/trace/__init__.py +20 -2
- klaude_code/ui/modes/repl/completers.py +19 -2
- klaude_code/ui/modes/repl/event_handler.py +8 -6
- klaude_code/ui/renderers/metadata.py +2 -4
- klaude_code/ui/renderers/thinking.py +24 -8
- klaude_code/ui/renderers/tools.py +79 -10
- klaude_code/ui/rich/code_panel.py +112 -0
- klaude_code/ui/rich/markdown.py +3 -4
- klaude_code/ui/rich/status.py +0 -2
- klaude_code/ui/rich/theme.py +10 -1
- {klaude_code-1.2.17.dist-info → klaude_code-1.2.18.dist-info}/METADATA +16 -6
- {klaude_code-1.2.17.dist-info → klaude_code-1.2.18.dist-info}/RECORD +53 -52
- klaude_code/core/manager/agent_manager.py +0 -132
- /klaude_code/{config → cli}/list_model.py +0 -0
- {klaude_code-1.2.17.dist-info → klaude_code-1.2.18.dist-info}/WHEEL +0 -0
- {klaude_code-1.2.17.dist-info → klaude_code-1.2.18.dist-info}/entry_points.txt +0 -0
|
@@ -1,35 +1,111 @@
|
|
|
1
|
-
from klaude_code.config.config import load_config
|
|
1
|
+
from klaude_code.config.config import ModelConfig, load_config
|
|
2
2
|
from klaude_code.trace import log
|
|
3
3
|
|
|
4
4
|
|
|
5
|
+
def _normalize_model_key(value: str) -> str:
|
|
6
|
+
"""Normalize a model identifier for loose matching.
|
|
7
|
+
|
|
8
|
+
This enables aliases like:
|
|
9
|
+
- gpt52 -> gpt-5.2
|
|
10
|
+
- gpt5.2 -> gpt-5.2
|
|
11
|
+
|
|
12
|
+
Strategy: case-fold + keep only alphanumeric characters.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
return "".join(ch for ch in value.casefold() if ch.isalnum())
|
|
16
|
+
|
|
17
|
+
|
|
5
18
|
def select_model_from_config(preferred: str | None = None) -> str | None:
|
|
6
19
|
"""
|
|
7
20
|
Interactive single-choice model selector.
|
|
8
21
|
for `--select-model`
|
|
22
|
+
|
|
23
|
+
If preferred is provided:
|
|
24
|
+
- Exact match: return immediately
|
|
25
|
+
- Single partial match (case-insensitive): return immediately
|
|
26
|
+
- Otherwise: fall through to interactive selection
|
|
9
27
|
"""
|
|
10
28
|
config = load_config()
|
|
11
29
|
assert config is not None
|
|
12
|
-
models = sorted(config.model_list, key=lambda m: m.model_name.lower())
|
|
30
|
+
models: list[ModelConfig] = sorted(config.model_list, key=lambda m: m.model_name.lower())
|
|
13
31
|
|
|
14
32
|
if not models:
|
|
15
33
|
raise ValueError("No models configured. Please update your config.yaml")
|
|
16
34
|
|
|
17
35
|
names: list[str] = [m.model_name for m in models]
|
|
18
36
|
|
|
37
|
+
# Try to match preferred model name
|
|
38
|
+
filtered_models = models
|
|
39
|
+
if preferred and preferred.strip():
|
|
40
|
+
preferred = preferred.strip()
|
|
41
|
+
# Exact match
|
|
42
|
+
if preferred in names:
|
|
43
|
+
return preferred
|
|
44
|
+
|
|
45
|
+
preferred_lower = preferred.lower()
|
|
46
|
+
# Case-insensitive exact match (model_name or model_params.model)
|
|
47
|
+
exact_ci_matches = [
|
|
48
|
+
m
|
|
49
|
+
for m in models
|
|
50
|
+
if preferred_lower == m.model_name.lower() or preferred_lower == (m.model_params.model or "").lower()
|
|
51
|
+
]
|
|
52
|
+
if len(exact_ci_matches) == 1:
|
|
53
|
+
return exact_ci_matches[0].model_name
|
|
54
|
+
|
|
55
|
+
# Normalized matching (e.g. gpt52 == gpt-5.2, gpt52 in gpt-5.2-2025-...)
|
|
56
|
+
preferred_norm = _normalize_model_key(preferred)
|
|
57
|
+
normalized_matches: list[ModelConfig] = []
|
|
58
|
+
if preferred_norm:
|
|
59
|
+
normalized_matches = [
|
|
60
|
+
m
|
|
61
|
+
for m in models
|
|
62
|
+
if preferred_norm == _normalize_model_key(m.model_name)
|
|
63
|
+
or preferred_norm == _normalize_model_key(m.model_params.model or "")
|
|
64
|
+
]
|
|
65
|
+
if len(normalized_matches) == 1:
|
|
66
|
+
return normalized_matches[0].model_name
|
|
67
|
+
|
|
68
|
+
if not normalized_matches and len(preferred_norm) >= 4:
|
|
69
|
+
normalized_matches = [
|
|
70
|
+
m
|
|
71
|
+
for m in models
|
|
72
|
+
if preferred_norm in _normalize_model_key(m.model_name)
|
|
73
|
+
or preferred_norm in _normalize_model_key(m.model_params.model or "")
|
|
74
|
+
]
|
|
75
|
+
if len(normalized_matches) == 1:
|
|
76
|
+
return normalized_matches[0].model_name
|
|
77
|
+
|
|
78
|
+
# Partial match (case-insensitive) on model_name or model_params.model.
|
|
79
|
+
# If normalized matching found candidates (even if multiple), prefer those as the filter set.
|
|
80
|
+
matches = normalized_matches or [
|
|
81
|
+
m
|
|
82
|
+
for m in models
|
|
83
|
+
if preferred_lower in m.model_name.lower() or preferred_lower in (m.model_params.model or "").lower()
|
|
84
|
+
]
|
|
85
|
+
if len(matches) == 1:
|
|
86
|
+
return matches[0].model_name
|
|
87
|
+
if matches:
|
|
88
|
+
# Multiple matches: filter the list for interactive selection
|
|
89
|
+
filtered_models = matches
|
|
90
|
+
else:
|
|
91
|
+
# No matches: show all models without filter hint
|
|
92
|
+
preferred = None
|
|
93
|
+
|
|
19
94
|
try:
|
|
20
95
|
import questionary
|
|
21
96
|
|
|
22
97
|
choices: list[questionary.Choice] = []
|
|
23
98
|
|
|
24
|
-
max_model_name_length = max(len(m.model_name) for m in
|
|
25
|
-
for m in
|
|
99
|
+
max_model_name_length = max(len(m.model_name) for m in filtered_models)
|
|
100
|
+
for m in filtered_models:
|
|
26
101
|
star = "★ " if m.model_name == config.main_model else " "
|
|
27
102
|
title = f"{star}{m.model_name:<{max_model_name_length}} → {m.model_params.model or 'N/A'} @ {m.provider}"
|
|
28
103
|
choices.append(questionary.Choice(title=title, value=m.model_name))
|
|
29
104
|
|
|
30
105
|
try:
|
|
106
|
+
message = f"Select a model (filtered by '{preferred}'):" if preferred else "Select a model:"
|
|
31
107
|
result = questionary.select(
|
|
32
|
-
message=
|
|
108
|
+
message=message,
|
|
33
109
|
choices=choices,
|
|
34
110
|
pointer="→",
|
|
35
111
|
instruction="↑↓ to move • Enter to select",
|
klaude_code/const/__init__.py
CHANGED
klaude_code/core/executor.py
CHANGED
|
@@ -8,15 +8,19 @@ handling operations submitted from the CLI and coordinating with agents.
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
import asyncio
|
|
11
|
+
from collections.abc import Awaitable, Callable
|
|
11
12
|
from dataclasses import dataclass
|
|
12
13
|
|
|
13
14
|
from klaude_code.command import InputAction, InputActionType, dispatch_command
|
|
15
|
+
from klaude_code.config import load_config
|
|
14
16
|
from klaude_code.core.agent import Agent, DefaultModelProfileProvider, ModelProfileProvider
|
|
15
|
-
from klaude_code.core.manager import
|
|
17
|
+
from klaude_code.core.manager import LLMClients, SubAgentManager
|
|
16
18
|
from klaude_code.core.tool import current_run_subtask_callback
|
|
17
|
-
from klaude_code.
|
|
19
|
+
from klaude_code.llm.registry import create_llm_client
|
|
20
|
+
from klaude_code.protocol import commands, events, model, op
|
|
18
21
|
from klaude_code.protocol.op_handler import OperationHandler
|
|
19
22
|
from klaude_code.protocol.sub_agent import SubAgentResult
|
|
23
|
+
from klaude_code.session.session import Session
|
|
20
24
|
from klaude_code.trace import DebugType, log_debug
|
|
21
25
|
|
|
22
26
|
|
|
@@ -72,6 +76,164 @@ class TaskManager:
|
|
|
72
76
|
self._tasks.clear()
|
|
73
77
|
|
|
74
78
|
|
|
79
|
+
class InputActionExecutor:
|
|
80
|
+
"""Execute input actions returned by the command dispatcher.
|
|
81
|
+
|
|
82
|
+
This helper encapsulates the logic for running the main agent task,
|
|
83
|
+
applying model changes, and clearing conversations so that
|
|
84
|
+
:class:`ExecutorContext` stays focused on operation dispatch.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
def __init__(
|
|
88
|
+
self,
|
|
89
|
+
task_manager: TaskManager,
|
|
90
|
+
sub_agent_manager: SubAgentManager,
|
|
91
|
+
model_profile_provider: ModelProfileProvider,
|
|
92
|
+
emit_event: Callable[[events.Event], Awaitable[None]],
|
|
93
|
+
) -> None:
|
|
94
|
+
self._task_manager = task_manager
|
|
95
|
+
self._sub_agent_manager = sub_agent_manager
|
|
96
|
+
self._model_profile_provider = model_profile_provider
|
|
97
|
+
self._emit_event = emit_event
|
|
98
|
+
|
|
99
|
+
async def run(self, action: InputAction, operation: op.UserInputOperation, agent: Agent) -> None:
|
|
100
|
+
"""Dispatch and execute a single input action."""
|
|
101
|
+
|
|
102
|
+
if operation.session_id is None:
|
|
103
|
+
raise ValueError("session_id cannot be None for input actions")
|
|
104
|
+
|
|
105
|
+
session_id = operation.session_id
|
|
106
|
+
|
|
107
|
+
if action.type == InputActionType.RUN_AGENT:
|
|
108
|
+
await self._run_agent_action(action, operation, agent, session_id)
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
if action.type == InputActionType.CHANGE_MODEL:
|
|
112
|
+
if not action.model_name:
|
|
113
|
+
raise ValueError("ChangeModel action requires model_name")
|
|
114
|
+
|
|
115
|
+
await self._apply_model_change(agent, action.model_name)
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
if action.type == InputActionType.CLEAR:
|
|
119
|
+
await self._apply_clear(agent)
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
raise ValueError(f"Unsupported input action type: {action.type}")
|
|
123
|
+
|
|
124
|
+
async def _run_agent_action(
|
|
125
|
+
self,
|
|
126
|
+
action: InputAction,
|
|
127
|
+
operation: op.UserInputOperation,
|
|
128
|
+
agent: Agent,
|
|
129
|
+
session_id: str,
|
|
130
|
+
) -> None:
|
|
131
|
+
task_input = model.UserInputPayload(text=action.text, images=operation.input.images)
|
|
132
|
+
|
|
133
|
+
existing_active = self._task_manager.get(operation.id)
|
|
134
|
+
if existing_active is not None and not existing_active.task.done():
|
|
135
|
+
raise RuntimeError(f"Active task already registered for operation {operation.id}")
|
|
136
|
+
|
|
137
|
+
task: asyncio.Task[None] = asyncio.create_task(
|
|
138
|
+
self._run_agent_task(agent, task_input, operation.id, session_id)
|
|
139
|
+
)
|
|
140
|
+
self._task_manager.register(operation.id, task, session_id)
|
|
141
|
+
|
|
142
|
+
async def _run_agent_task(
|
|
143
|
+
self,
|
|
144
|
+
agent: Agent,
|
|
145
|
+
user_input: model.UserInputPayload,
|
|
146
|
+
task_id: str,
|
|
147
|
+
session_id: str,
|
|
148
|
+
) -> None:
|
|
149
|
+
"""Run the main agent task and forward events to the UI."""
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
log_debug(
|
|
153
|
+
f"Starting agent task {task_id} for session {session_id}",
|
|
154
|
+
style="green",
|
|
155
|
+
debug_type=DebugType.EXECUTION,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
async def _runner(state: model.SubAgentState) -> SubAgentResult:
|
|
159
|
+
return await self._sub_agent_manager.run_sub_agent(agent, state)
|
|
160
|
+
|
|
161
|
+
token = current_run_subtask_callback.set(_runner)
|
|
162
|
+
try:
|
|
163
|
+
async for event in agent.run_task(user_input):
|
|
164
|
+
await self._emit_event(event)
|
|
165
|
+
finally:
|
|
166
|
+
current_run_subtask_callback.reset(token)
|
|
167
|
+
|
|
168
|
+
except asyncio.CancelledError:
|
|
169
|
+
log_debug(
|
|
170
|
+
f"Agent task {task_id} was cancelled",
|
|
171
|
+
style="yellow",
|
|
172
|
+
debug_type=DebugType.EXECUTION,
|
|
173
|
+
)
|
|
174
|
+
await self._emit_event(events.TaskFinishEvent(session_id=session_id, task_result="task cancelled"))
|
|
175
|
+
|
|
176
|
+
except Exception as e:
|
|
177
|
+
import traceback
|
|
178
|
+
|
|
179
|
+
log_debug(
|
|
180
|
+
f"Agent task {task_id} failed: {e!s}",
|
|
181
|
+
style="red",
|
|
182
|
+
debug_type=DebugType.EXECUTION,
|
|
183
|
+
)
|
|
184
|
+
log_debug(traceback.format_exc(), style="red", debug_type=DebugType.EXECUTION)
|
|
185
|
+
await self._emit_event(
|
|
186
|
+
events.ErrorEvent(
|
|
187
|
+
error_message=f"Agent task failed: [{e.__class__.__name__}] {e!s}",
|
|
188
|
+
can_retry=False,
|
|
189
|
+
)
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
finally:
|
|
193
|
+
self._task_manager.remove(task_id)
|
|
194
|
+
log_debug(
|
|
195
|
+
f"Cleaned up agent task {task_id}",
|
|
196
|
+
style="cyan",
|
|
197
|
+
debug_type=DebugType.EXECUTION,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
async def _apply_model_change(self, agent: Agent, model_name: str) -> None:
|
|
201
|
+
"""Change the model used by the active agent and notify the UI."""
|
|
202
|
+
|
|
203
|
+
config = load_config()
|
|
204
|
+
if config is None:
|
|
205
|
+
raise ValueError("Configuration must be initialized before changing model")
|
|
206
|
+
|
|
207
|
+
llm_config = config.get_model_config(model_name)
|
|
208
|
+
llm_client = create_llm_client(llm_config)
|
|
209
|
+
agent.set_model_profile(self._model_profile_provider.build_profile(llm_client))
|
|
210
|
+
|
|
211
|
+
developer_item = model.DeveloperMessageItem(
|
|
212
|
+
content=f"switched to model: {model_name}",
|
|
213
|
+
command_output=model.CommandOutput(command_name=commands.CommandName.MODEL),
|
|
214
|
+
)
|
|
215
|
+
agent.session.append_history([developer_item])
|
|
216
|
+
|
|
217
|
+
await self._emit_event(events.DeveloperMessageEvent(session_id=agent.session.id, item=developer_item))
|
|
218
|
+
await self._emit_event(events.WelcomeEvent(llm_config=llm_config, work_dir=str(agent.session.work_dir)))
|
|
219
|
+
|
|
220
|
+
async def _apply_clear(self, agent: Agent) -> None:
|
|
221
|
+
"""Start a new conversation for the agent and notify the UI."""
|
|
222
|
+
|
|
223
|
+
new_session = Session(work_dir=agent.session.work_dir)
|
|
224
|
+
new_session.model_name = agent.session.model_name
|
|
225
|
+
|
|
226
|
+
agent.session = new_session
|
|
227
|
+
agent.session.save()
|
|
228
|
+
|
|
229
|
+
developer_item = model.DeveloperMessageItem(
|
|
230
|
+
content="started new conversation",
|
|
231
|
+
command_output=model.CommandOutput(command_name=commands.CommandName.CLEAR),
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
await self._emit_event(events.DeveloperMessageEvent(session_id=agent.session.id, item=developer_item))
|
|
235
|
+
|
|
236
|
+
|
|
75
237
|
class ExecutorContext:
|
|
76
238
|
"""
|
|
77
239
|
Context object providing shared state and operations for the executor.
|
|
@@ -89,32 +251,81 @@ class ExecutorContext:
|
|
|
89
251
|
model_profile_provider: ModelProfileProvider | None = None,
|
|
90
252
|
):
|
|
91
253
|
self.event_queue: asyncio.Queue[events.Event] = event_queue
|
|
254
|
+
self.llm_clients: LLMClients = llm_clients
|
|
92
255
|
|
|
93
256
|
resolved_profile_provider = model_profile_provider or DefaultModelProfileProvider()
|
|
94
257
|
self.model_profile_provider: ModelProfileProvider = resolved_profile_provider
|
|
95
258
|
|
|
96
|
-
# Delegate responsibilities to helper components
|
|
97
|
-
self.agent_manager = AgentManager(event_queue, llm_clients, resolved_profile_provider)
|
|
98
259
|
self.task_manager = TaskManager()
|
|
99
260
|
self.sub_agent_manager = SubAgentManager(event_queue, llm_clients, resolved_profile_provider)
|
|
261
|
+
self._action_executor = InputActionExecutor(
|
|
262
|
+
task_manager=self.task_manager,
|
|
263
|
+
sub_agent_manager=self.sub_agent_manager,
|
|
264
|
+
model_profile_provider=resolved_profile_provider,
|
|
265
|
+
emit_event=self.emit_event,
|
|
266
|
+
)
|
|
267
|
+
self._agent: Agent | None = None
|
|
100
268
|
|
|
101
269
|
async def emit_event(self, event: events.Event) -> None:
|
|
102
270
|
"""Emit an event to the UI display system."""
|
|
103
271
|
await self.event_queue.put(event)
|
|
104
272
|
|
|
273
|
+
def current_session_id(self) -> str | None:
|
|
274
|
+
"""Return the primary active session id, if any.
|
|
275
|
+
|
|
276
|
+
This is a convenience wrapper used by the CLI, which conceptually
|
|
277
|
+
operates on a single interactive session per process.
|
|
278
|
+
"""
|
|
279
|
+
|
|
280
|
+
agent = self._agent
|
|
281
|
+
if agent is None:
|
|
282
|
+
return None
|
|
283
|
+
return agent.session.id
|
|
284
|
+
|
|
105
285
|
@property
|
|
106
|
-
def
|
|
107
|
-
"""
|
|
286
|
+
def current_agent(self) -> Agent | None:
|
|
287
|
+
"""Return the currently active agent, if any."""
|
|
288
|
+
|
|
289
|
+
return self._agent
|
|
108
290
|
|
|
109
|
-
|
|
110
|
-
|
|
291
|
+
async def _ensure_agent(self, session_id: str | None = None) -> Agent:
|
|
292
|
+
"""Return the active agent, creating or loading a session as needed.
|
|
293
|
+
|
|
294
|
+
If ``session_id`` is ``None``, a new session is created with an
|
|
295
|
+
auto-generated ID. If provided, the executor attempts to resume the
|
|
296
|
+
session from disk or creates a new one if not found.
|
|
111
297
|
"""
|
|
112
298
|
|
|
113
|
-
|
|
299
|
+
# Fast-path: reuse current agent when the session id already matches.
|
|
300
|
+
if session_id is not None and self._agent is not None and self._agent.session.id == session_id:
|
|
301
|
+
return self._agent
|
|
302
|
+
|
|
303
|
+
session = Session.create() if session_id is None else Session.load(session_id)
|
|
304
|
+
|
|
305
|
+
profile = self.model_profile_provider.build_profile(self.llm_clients.main)
|
|
306
|
+
agent = Agent(session=session, profile=profile)
|
|
307
|
+
|
|
308
|
+
async for evt in agent.replay_history():
|
|
309
|
+
await self.emit_event(evt)
|
|
310
|
+
|
|
311
|
+
await self.emit_event(
|
|
312
|
+
events.WelcomeEvent(
|
|
313
|
+
work_dir=str(session.work_dir),
|
|
314
|
+
llm_config=self.llm_clients.main.get_llm_config(),
|
|
315
|
+
)
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
self._agent = agent
|
|
319
|
+
log_debug(
|
|
320
|
+
f"Initialized agent for session: {session.id}",
|
|
321
|
+
style="cyan",
|
|
322
|
+
debug_type=DebugType.EXECUTION,
|
|
323
|
+
)
|
|
324
|
+
return agent
|
|
114
325
|
|
|
115
326
|
async def handle_init_agent(self, operation: op.InitAgentOperation) -> None:
|
|
116
327
|
"""Initialize an agent for a session and replay history to UI."""
|
|
117
|
-
await self.
|
|
328
|
+
await self._ensure_agent(operation.session_id)
|
|
118
329
|
|
|
119
330
|
async def handle_user_input(self, operation: op.UserInputOperation) -> None:
|
|
120
331
|
"""Handle a user input operation by running it through an agent."""
|
|
@@ -123,7 +334,7 @@ class ExecutorContext:
|
|
|
123
334
|
raise ValueError("session_id cannot be None")
|
|
124
335
|
|
|
125
336
|
session_id = operation.session_id
|
|
126
|
-
agent = await self.
|
|
337
|
+
agent = await self._ensure_agent(session_id)
|
|
127
338
|
user_input = operation.input
|
|
128
339
|
|
|
129
340
|
# emit user input event
|
|
@@ -148,39 +359,7 @@ class ExecutorContext:
|
|
|
148
359
|
await self.emit_event(evt)
|
|
149
360
|
|
|
150
361
|
for action in actions:
|
|
151
|
-
await self.
|
|
152
|
-
|
|
153
|
-
async def _run_input_action(self, action: InputAction, operation: op.UserInputOperation, agent: Agent) -> None:
|
|
154
|
-
if operation.session_id is None:
|
|
155
|
-
raise ValueError("session_id cannot be None for input actions")
|
|
156
|
-
|
|
157
|
-
session_id = operation.session_id
|
|
158
|
-
|
|
159
|
-
if action.type == InputActionType.RUN_AGENT:
|
|
160
|
-
task_input = model.UserInputPayload(text=action.text, images=operation.input.images)
|
|
161
|
-
|
|
162
|
-
existing_active = self.task_manager.get(operation.id)
|
|
163
|
-
if existing_active is not None and not existing_active.task.done():
|
|
164
|
-
raise RuntimeError(f"Active task already registered for operation {operation.id}")
|
|
165
|
-
|
|
166
|
-
task: asyncio.Task[None] = asyncio.create_task(
|
|
167
|
-
self._run_agent_task(agent, task_input, operation.id, session_id)
|
|
168
|
-
)
|
|
169
|
-
self.task_manager.register(operation.id, task, session_id)
|
|
170
|
-
return
|
|
171
|
-
|
|
172
|
-
if action.type == InputActionType.CHANGE_MODEL:
|
|
173
|
-
if not action.model_name:
|
|
174
|
-
raise ValueError("ChangeModel action requires model_name")
|
|
175
|
-
|
|
176
|
-
await self.agent_manager.apply_model_change(agent, action.model_name)
|
|
177
|
-
return
|
|
178
|
-
|
|
179
|
-
if action.type == InputActionType.CLEAR:
|
|
180
|
-
await self.agent_manager.apply_clear(agent)
|
|
181
|
-
return
|
|
182
|
-
|
|
183
|
-
raise ValueError(f"Unsupported input action type: {action.type}")
|
|
362
|
+
await self._action_executor.run(action, operation, agent)
|
|
184
363
|
|
|
185
364
|
async def handle_interrupt(self, operation: op.InterruptOperation) -> None:
|
|
186
365
|
"""Handle an interrupt by invoking agent.cancel() and cancelling tasks."""
|
|
@@ -189,11 +368,12 @@ class ExecutorContext:
|
|
|
189
368
|
if operation.target_session_id is not None:
|
|
190
369
|
session_ids: list[str] = [operation.target_session_id]
|
|
191
370
|
else:
|
|
192
|
-
|
|
371
|
+
agent = self._agent
|
|
372
|
+
session_ids = [agent.session.id] if agent is not None else []
|
|
193
373
|
|
|
194
374
|
# Call cancel() on each affected agent to persist an interrupt marker
|
|
195
375
|
for sid in session_ids:
|
|
196
|
-
agent = self.
|
|
376
|
+
agent = self._get_active_agent(sid)
|
|
197
377
|
if agent is not None:
|
|
198
378
|
for evt in agent.cancel():
|
|
199
379
|
await self.emit_event(evt)
|
|
@@ -222,69 +402,6 @@ class ExecutorContext:
|
|
|
222
402
|
# Remove from active tasks immediately
|
|
223
403
|
self.task_manager.remove(task_id)
|
|
224
404
|
|
|
225
|
-
async def _run_agent_task(
|
|
226
|
-
self, agent: Agent, user_input: model.UserInputPayload, task_id: str, session_id: str
|
|
227
|
-
) -> None:
|
|
228
|
-
"""
|
|
229
|
-
Run an agent task and forward all events to the UI.
|
|
230
|
-
|
|
231
|
-
This method wraps the agent's run_task method and handles any exceptions
|
|
232
|
-
that might occur during execution.
|
|
233
|
-
"""
|
|
234
|
-
try:
|
|
235
|
-
log_debug(
|
|
236
|
-
f"Starting agent task {task_id} for session {session_id}",
|
|
237
|
-
style="green",
|
|
238
|
-
debug_type=DebugType.EXECUTION,
|
|
239
|
-
)
|
|
240
|
-
|
|
241
|
-
# Inject subtask runner into tool context for nested Task tool usage
|
|
242
|
-
async def _runner(state: model.SubAgentState) -> SubAgentResult:
|
|
243
|
-
return await self.sub_agent_manager.run_sub_agent(agent, state)
|
|
244
|
-
|
|
245
|
-
token = current_run_subtask_callback.set(_runner)
|
|
246
|
-
try:
|
|
247
|
-
# Forward all events from the agent to the UI
|
|
248
|
-
async for event in agent.run_task(user_input):
|
|
249
|
-
await self.emit_event(event)
|
|
250
|
-
finally:
|
|
251
|
-
current_run_subtask_callback.reset(token)
|
|
252
|
-
|
|
253
|
-
except asyncio.CancelledError:
|
|
254
|
-
# Task was cancelled (likely due to interrupt)
|
|
255
|
-
log_debug(
|
|
256
|
-
f"Agent task {task_id} was cancelled",
|
|
257
|
-
style="yellow",
|
|
258
|
-
debug_type=DebugType.EXECUTION,
|
|
259
|
-
)
|
|
260
|
-
await self.emit_event(events.TaskFinishEvent(session_id=session_id, task_result="task cancelled"))
|
|
261
|
-
|
|
262
|
-
except Exception as e:
|
|
263
|
-
# Handle any other exceptions
|
|
264
|
-
import traceback
|
|
265
|
-
|
|
266
|
-
log_debug(
|
|
267
|
-
f"Agent task {task_id} failed: {e!s}",
|
|
268
|
-
style="red",
|
|
269
|
-
debug_type=DebugType.EXECUTION,
|
|
270
|
-
)
|
|
271
|
-
log_debug(traceback.format_exc(), style="red", debug_type=DebugType.EXECUTION)
|
|
272
|
-
await self.emit_event(
|
|
273
|
-
events.ErrorEvent(
|
|
274
|
-
error_message=f"Agent task failed: [{e.__class__.__name__}] {e!s}",
|
|
275
|
-
can_retry=False,
|
|
276
|
-
)
|
|
277
|
-
)
|
|
278
|
-
|
|
279
|
-
finally:
|
|
280
|
-
# Clean up the task from active tasks
|
|
281
|
-
self.task_manager.remove(task_id)
|
|
282
|
-
log_debug(
|
|
283
|
-
f"Cleaned up agent task {task_id}",
|
|
284
|
-
style="cyan",
|
|
285
|
-
debug_type=DebugType.EXECUTION,
|
|
286
|
-
)
|
|
287
|
-
|
|
288
405
|
def get_active_task(self, submission_id: str) -> asyncio.Task[None] | None:
|
|
289
406
|
"""Return the asyncio.Task for a submission id if one is registered."""
|
|
290
407
|
|
|
@@ -298,6 +415,16 @@ class ExecutorContext:
|
|
|
298
415
|
|
|
299
416
|
return self.task_manager.get(submission_id) is not None
|
|
300
417
|
|
|
418
|
+
def _get_active_agent(self, session_id: str) -> Agent | None:
|
|
419
|
+
"""Return the active agent if its session id matches ``session_id``."""
|
|
420
|
+
|
|
421
|
+
agent = self._agent
|
|
422
|
+
if agent is None:
|
|
423
|
+
return None
|
|
424
|
+
if agent.session.id != session_id:
|
|
425
|
+
return None
|
|
426
|
+
return agent
|
|
427
|
+
|
|
301
428
|
|
|
302
429
|
class Executor:
|
|
303
430
|
"""
|
|
@@ -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",
|
|
@@ -7,7 +7,7 @@ You are an interactive CLI tool that helps users with software engineering tasks
|
|
|
7
7
|
- NEVER create files unless they're absolutely necessary for achieving your goal. ALWAYS prefer editing an existing file to creating a new one. This includes markdown files.
|
|
8
8
|
|
|
9
9
|
## Professional objectivity
|
|
10
|
-
Prioritize technical accuracy and truthfulness over validating the user's beliefs. Focus on facts and problem-solving, providing direct, objective technical info without any unnecessary superlatives, praise, or emotional validation. It is best for the user if
|
|
10
|
+
Prioritize technical accuracy and truthfulness over validating the user's beliefs. Focus on facts and problem-solving, providing direct, objective technical info without any unnecessary superlatives, praise, or emotional validation. It is best for the user if you honestly applies the same rigorous standards to all ideas and disagrees when necessary, even if it may not be what the user wants to hear. Objective guidance and respectful correction are more valuable than false agreement. Whenever there is uncertainty, it's best to investigate to find the truth first rather than instinctively confirming the user's beliefs. Avoid using over-the-top validation or excessive praise when responding to users such as "You're absolutely right" or similar phrases.
|
|
11
11
|
|
|
12
12
|
## Planning without timelines
|
|
13
13
|
When planning tasks, provide concrete implementation steps without time estimates. Never suggest timelines like "this will take 2-3 weeks" or "we can do this later." Focus on what needs to be done, not when. Break work into actionable steps and let users decide scheduling.
|
|
@@ -11,6 +11,7 @@ You are a web research agent that searches and fetches web content to provide up
|
|
|
11
11
|
- HTML pages are automatically converted to Markdown
|
|
12
12
|
- JSON responses are auto-formatted with indentation
|
|
13
13
|
- Other text content returned as-is
|
|
14
|
+
- **Content is always saved to a local file** - check `<file_saved>` tag for the path
|
|
14
15
|
|
|
15
16
|
## Tool Usage Strategy
|
|
16
17
|
|
|
@@ -26,7 +27,7 @@ Balance efficiency with thoroughness. For open-ended questions (e.g., "recommend
|
|
|
26
27
|
- Keep queries concise (1-6 words). Start broad, then narrow if needed
|
|
27
28
|
- Avoid repeating similar queries - they won't yield new results
|
|
28
29
|
- NEVER use '-', 'site:', or quotes unless explicitly asked
|
|
29
|
-
- Include year/date for time-sensitive queries (check "Today's date" in <env>)
|
|
30
|
+
- Include year/date for time-sensitive queries (check "Today's date" in <env>), don't limit yourself to your knowledge cutoff date
|
|
30
31
|
- Use WebFetch to get full content - search snippets are often insufficient
|
|
31
32
|
- Follow relevant links on pages with WebFetch
|
|
32
33
|
- If truncated results are saved to local files, use grep/read to explore
|
|
@@ -34,15 +35,17 @@ Balance efficiency with thoroughness. For open-ended questions (e.g., "recommend
|
|
|
34
35
|
## Response Guidelines
|
|
35
36
|
|
|
36
37
|
- Only your last message is returned to the main agent
|
|
37
|
-
-
|
|
38
|
+
- **DO NOT copy full web page content** - the main agent can read the saved files directly
|
|
39
|
+
- Provide a concise summary/analysis of key findings
|
|
40
|
+
- Include the file path from `<file_saved>` so the main agent can access full content if needed
|
|
38
41
|
- Lead with the most recent info for evolving topics
|
|
39
42
|
- Favor original sources (company blogs, papers, gov sites) over aggregators
|
|
40
43
|
- Note conflicting sources when they exist
|
|
41
44
|
|
|
42
45
|
## Sources (REQUIRED)
|
|
43
46
|
|
|
44
|
-
You MUST end every response with a "Sources:" section listing all URLs
|
|
47
|
+
You MUST end every response with a "Sources:" section listing all URLs with their saved file paths:
|
|
45
48
|
|
|
46
49
|
Sources:
|
|
47
|
-
- [Source Title](https://example.com)
|
|
48
|
-
- [Another Source](https://example.com/page)
|
|
50
|
+
- [Source Title](https://example.com) -> /tmp/klaude/web/example_com-123456.md
|
|
51
|
+
- [Another Source](https://example.com/page) -> /tmp/klaude/web/example_com_page-123456.md
|