klaude-code 1.2.17__py3-none-any.whl → 1.2.19__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 +45 -31
- klaude_code/cli/runtime.py +49 -13
- klaude_code/{version.py → cli/self_update.py} +110 -2
- 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 +9 -8
- 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 +69 -26
- 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 +16 -10
- klaude_code/config/select_model.py +81 -5
- klaude_code/const/__init__.py +1 -1
- klaude_code/core/executor.py +257 -110
- 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-explore.md +14 -2
- klaude_code/core/prompts/prompt-sub-agent-web.md +8 -5
- klaude_code/core/reminders.py +9 -35
- klaude_code/core/task.py +9 -7
- klaude_code/core/tool/file/read_tool.md +1 -1
- klaude_code/core/tool/file/read_tool.py +41 -12
- klaude_code/core/tool/memory/skill_loader.py +12 -10
- klaude_code/core/tool/shell/bash_tool.py +22 -2
- klaude_code/core/tool/tool_registry.py +1 -1
- 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/llm/anthropic/client.py +25 -9
- klaude_code/llm/openai_compatible/client.py +5 -2
- klaude_code/llm/openrouter/client.py +7 -3
- klaude_code/llm/responses/client.py +6 -1
- klaude_code/protocol/commands.py +1 -0
- klaude_code/protocol/sub_agent/web.py +3 -2
- klaude_code/session/session.py +35 -15
- klaude_code/session/templates/export_session.html +45 -32
- klaude_code/trace/__init__.py +20 -2
- klaude_code/ui/modes/repl/completers.py +231 -73
- klaude_code/ui/modes/repl/event_handler.py +8 -6
- klaude_code/ui/modes/repl/input_prompt_toolkit.py +1 -1
- klaude_code/ui/modes/repl/renderer.py +2 -2
- klaude_code/ui/renderers/common.py +54 -0
- klaude_code/ui/renderers/developer.py +2 -3
- klaude_code/ui/renderers/errors.py +1 -1
- klaude_code/ui/renderers/metadata.py +12 -5
- klaude_code/ui/renderers/thinking.py +24 -8
- klaude_code/ui/renderers/tools.py +82 -14
- 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/ui/utils/common.py +0 -18
- {klaude_code-1.2.17.dist-info → klaude_code-1.2.19.dist-info}/METADATA +32 -7
- {klaude_code-1.2.17.dist-info → klaude_code-1.2.19.dist-info}/RECORD +69 -68
- 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.19.dist-info}/WHEEL +0 -0
- {klaude_code-1.2.17.dist-info → klaude_code-1.2.19.dist-info}/entry_points.txt +0 -0
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,174 @@ 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
|
+
on_model_change: Callable[[str], None] | None = None,
|
|
94
|
+
) -> None:
|
|
95
|
+
self._task_manager = task_manager
|
|
96
|
+
self._sub_agent_manager = sub_agent_manager
|
|
97
|
+
self._model_profile_provider = model_profile_provider
|
|
98
|
+
self._emit_event = emit_event
|
|
99
|
+
self._on_model_change = on_model_change
|
|
100
|
+
|
|
101
|
+
async def run(self, action: InputAction, operation: op.UserInputOperation, agent: Agent) -> None:
|
|
102
|
+
"""Dispatch and execute a single input action."""
|
|
103
|
+
|
|
104
|
+
if operation.session_id is None:
|
|
105
|
+
raise ValueError("session_id cannot be None for input actions")
|
|
106
|
+
|
|
107
|
+
session_id = operation.session_id
|
|
108
|
+
|
|
109
|
+
if action.type == InputActionType.RUN_AGENT:
|
|
110
|
+
await self._run_agent_action(action, operation, agent, session_id)
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
if action.type == InputActionType.CHANGE_MODEL:
|
|
114
|
+
if not action.model_name:
|
|
115
|
+
raise ValueError("ChangeModel action requires model_name")
|
|
116
|
+
|
|
117
|
+
await self._apply_model_change(agent, action.model_name)
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
if action.type == InputActionType.CLEAR:
|
|
121
|
+
await self._apply_clear(agent)
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
raise ValueError(f"Unsupported input action type: {action.type}")
|
|
125
|
+
|
|
126
|
+
async def _run_agent_action(
|
|
127
|
+
self,
|
|
128
|
+
action: InputAction,
|
|
129
|
+
operation: op.UserInputOperation,
|
|
130
|
+
agent: Agent,
|
|
131
|
+
session_id: str,
|
|
132
|
+
) -> None:
|
|
133
|
+
task_input = model.UserInputPayload(text=action.text, images=operation.input.images)
|
|
134
|
+
|
|
135
|
+
existing_active = self._task_manager.get(operation.id)
|
|
136
|
+
if existing_active is not None and not existing_active.task.done():
|
|
137
|
+
raise RuntimeError(f"Active task already registered for operation {operation.id}")
|
|
138
|
+
|
|
139
|
+
task: asyncio.Task[None] = asyncio.create_task(
|
|
140
|
+
self._run_agent_task(agent, task_input, operation.id, session_id)
|
|
141
|
+
)
|
|
142
|
+
self._task_manager.register(operation.id, task, session_id)
|
|
143
|
+
|
|
144
|
+
async def _run_agent_task(
|
|
145
|
+
self,
|
|
146
|
+
agent: Agent,
|
|
147
|
+
user_input: model.UserInputPayload,
|
|
148
|
+
task_id: str,
|
|
149
|
+
session_id: str,
|
|
150
|
+
) -> None:
|
|
151
|
+
"""Run the main agent task and forward events to the UI."""
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
log_debug(
|
|
155
|
+
f"Starting agent task {task_id} for session {session_id}",
|
|
156
|
+
style="green",
|
|
157
|
+
debug_type=DebugType.EXECUTION,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
async def _runner(state: model.SubAgentState) -> SubAgentResult:
|
|
161
|
+
return await self._sub_agent_manager.run_sub_agent(agent, state)
|
|
162
|
+
|
|
163
|
+
token = current_run_subtask_callback.set(_runner)
|
|
164
|
+
try:
|
|
165
|
+
async for event in agent.run_task(user_input):
|
|
166
|
+
await self._emit_event(event)
|
|
167
|
+
finally:
|
|
168
|
+
current_run_subtask_callback.reset(token)
|
|
169
|
+
|
|
170
|
+
except asyncio.CancelledError:
|
|
171
|
+
log_debug(
|
|
172
|
+
f"Agent task {task_id} was cancelled",
|
|
173
|
+
style="yellow",
|
|
174
|
+
debug_type=DebugType.EXECUTION,
|
|
175
|
+
)
|
|
176
|
+
await self._emit_event(events.TaskFinishEvent(session_id=session_id, task_result="task cancelled"))
|
|
177
|
+
|
|
178
|
+
except Exception as e:
|
|
179
|
+
import traceback
|
|
180
|
+
|
|
181
|
+
log_debug(
|
|
182
|
+
f"Agent task {task_id} failed: {e!s}",
|
|
183
|
+
style="red",
|
|
184
|
+
debug_type=DebugType.EXECUTION,
|
|
185
|
+
)
|
|
186
|
+
log_debug(traceback.format_exc(), style="red", debug_type=DebugType.EXECUTION)
|
|
187
|
+
await self._emit_event(
|
|
188
|
+
events.ErrorEvent(
|
|
189
|
+
error_message=f"Agent task failed: [{e.__class__.__name__}] {e!s}",
|
|
190
|
+
can_retry=False,
|
|
191
|
+
)
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
finally:
|
|
195
|
+
self._task_manager.remove(task_id)
|
|
196
|
+
log_debug(
|
|
197
|
+
f"Cleaned up agent task {task_id}",
|
|
198
|
+
style="cyan",
|
|
199
|
+
debug_type=DebugType.EXECUTION,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
async def _apply_model_change(self, agent: Agent, model_name: str) -> None:
|
|
203
|
+
"""Change the model used by the active agent and notify the UI."""
|
|
204
|
+
|
|
205
|
+
config = load_config()
|
|
206
|
+
if config is None:
|
|
207
|
+
raise ValueError("Configuration must be initialized before changing model")
|
|
208
|
+
|
|
209
|
+
llm_config = config.get_model_config(model_name)
|
|
210
|
+
llm_client = create_llm_client(llm_config)
|
|
211
|
+
agent.set_model_profile(self._model_profile_provider.build_profile(llm_client))
|
|
212
|
+
|
|
213
|
+
agent.session.model_config_name = model_name
|
|
214
|
+
agent.session.model_thinking = llm_config.thinking
|
|
215
|
+
|
|
216
|
+
developer_item = model.DeveloperMessageItem(
|
|
217
|
+
content=f"switched to model: {model_name}",
|
|
218
|
+
command_output=model.CommandOutput(command_name=commands.CommandName.MODEL),
|
|
219
|
+
)
|
|
220
|
+
agent.session.append_history([developer_item])
|
|
221
|
+
|
|
222
|
+
await self._emit_event(events.DeveloperMessageEvent(session_id=agent.session.id, item=developer_item))
|
|
223
|
+
await self._emit_event(events.WelcomeEvent(llm_config=llm_config, work_dir=str(agent.session.work_dir)))
|
|
224
|
+
|
|
225
|
+
if self._on_model_change is not None:
|
|
226
|
+
self._on_model_change(llm_client.model_name)
|
|
227
|
+
|
|
228
|
+
async def _apply_clear(self, agent: Agent) -> None:
|
|
229
|
+
"""Start a new conversation for the agent and notify the UI."""
|
|
230
|
+
|
|
231
|
+
new_session = Session(work_dir=agent.session.work_dir)
|
|
232
|
+
new_session.model_name = agent.session.model_name
|
|
233
|
+
new_session.model_config_name = agent.session.model_config_name
|
|
234
|
+
new_session.model_thinking = agent.session.model_thinking
|
|
235
|
+
|
|
236
|
+
agent.session = new_session
|
|
237
|
+
agent.session.save()
|
|
238
|
+
|
|
239
|
+
developer_item = model.DeveloperMessageItem(
|
|
240
|
+
content="started new conversation",
|
|
241
|
+
command_output=model.CommandOutput(command_name=commands.CommandName.CLEAR),
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
await self._emit_event(events.DeveloperMessageEvent(session_id=agent.session.id, item=developer_item))
|
|
245
|
+
|
|
246
|
+
|
|
75
247
|
class ExecutorContext:
|
|
76
248
|
"""
|
|
77
249
|
Context object providing shared state and operations for the executor.
|
|
@@ -87,34 +259,92 @@ class ExecutorContext:
|
|
|
87
259
|
event_queue: asyncio.Queue[events.Event],
|
|
88
260
|
llm_clients: LLMClients,
|
|
89
261
|
model_profile_provider: ModelProfileProvider | None = None,
|
|
262
|
+
on_model_change: Callable[[str], None] | None = None,
|
|
90
263
|
):
|
|
91
264
|
self.event_queue: asyncio.Queue[events.Event] = event_queue
|
|
265
|
+
self.llm_clients: LLMClients = llm_clients
|
|
92
266
|
|
|
93
267
|
resolved_profile_provider = model_profile_provider or DefaultModelProfileProvider()
|
|
94
268
|
self.model_profile_provider: ModelProfileProvider = resolved_profile_provider
|
|
95
269
|
|
|
96
|
-
# Delegate responsibilities to helper components
|
|
97
|
-
self.agent_manager = AgentManager(event_queue, llm_clients, resolved_profile_provider)
|
|
98
270
|
self.task_manager = TaskManager()
|
|
99
271
|
self.sub_agent_manager = SubAgentManager(event_queue, llm_clients, resolved_profile_provider)
|
|
272
|
+
self._action_executor = InputActionExecutor(
|
|
273
|
+
task_manager=self.task_manager,
|
|
274
|
+
sub_agent_manager=self.sub_agent_manager,
|
|
275
|
+
model_profile_provider=resolved_profile_provider,
|
|
276
|
+
emit_event=self.emit_event,
|
|
277
|
+
on_model_change=on_model_change,
|
|
278
|
+
)
|
|
279
|
+
self._agent: Agent | None = None
|
|
100
280
|
|
|
101
281
|
async def emit_event(self, event: events.Event) -> None:
|
|
102
282
|
"""Emit an event to the UI display system."""
|
|
103
283
|
await self.event_queue.put(event)
|
|
104
284
|
|
|
285
|
+
def current_session_id(self) -> str | None:
|
|
286
|
+
"""Return the primary active session id, if any.
|
|
287
|
+
|
|
288
|
+
This is a convenience wrapper used by the CLI, which conceptually
|
|
289
|
+
operates on a single interactive session per process.
|
|
290
|
+
"""
|
|
291
|
+
|
|
292
|
+
agent = self._agent
|
|
293
|
+
if agent is None:
|
|
294
|
+
return None
|
|
295
|
+
return agent.session.id
|
|
296
|
+
|
|
105
297
|
@property
|
|
106
|
-
def
|
|
107
|
-
"""
|
|
298
|
+
def current_agent(self) -> Agent | None:
|
|
299
|
+
"""Return the currently active agent, if any."""
|
|
108
300
|
|
|
109
|
-
|
|
110
|
-
|
|
301
|
+
return self._agent
|
|
302
|
+
|
|
303
|
+
async def _ensure_agent(self, session_id: str | None = None) -> Agent:
|
|
304
|
+
"""Return the active agent, creating or loading a session as needed.
|
|
305
|
+
|
|
306
|
+
If ``session_id`` is ``None``, a new session is created with an
|
|
307
|
+
auto-generated ID. If provided, the executor attempts to resume the
|
|
308
|
+
session from disk or creates a new one if not found.
|
|
111
309
|
"""
|
|
112
310
|
|
|
113
|
-
|
|
311
|
+
# Fast-path: reuse current agent when the session id already matches.
|
|
312
|
+
if session_id is not None and self._agent is not None and self._agent.session.id == session_id:
|
|
313
|
+
return self._agent
|
|
314
|
+
|
|
315
|
+
session = Session.create() if session_id is None else Session.load(session_id)
|
|
316
|
+
|
|
317
|
+
if (
|
|
318
|
+
session.model_thinking is not None
|
|
319
|
+
and session.model_name
|
|
320
|
+
and session.model_name == self.llm_clients.main.model_name
|
|
321
|
+
):
|
|
322
|
+
self.llm_clients.main.get_llm_config().thinking = session.model_thinking
|
|
323
|
+
|
|
324
|
+
profile = self.model_profile_provider.build_profile(self.llm_clients.main)
|
|
325
|
+
agent = Agent(session=session, profile=profile)
|
|
326
|
+
|
|
327
|
+
async for evt in agent.replay_history():
|
|
328
|
+
await self.emit_event(evt)
|
|
329
|
+
|
|
330
|
+
await self.emit_event(
|
|
331
|
+
events.WelcomeEvent(
|
|
332
|
+
work_dir=str(session.work_dir),
|
|
333
|
+
llm_config=self.llm_clients.main.get_llm_config(),
|
|
334
|
+
)
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
self._agent = agent
|
|
338
|
+
log_debug(
|
|
339
|
+
f"Initialized agent for session: {session.id}",
|
|
340
|
+
style="cyan",
|
|
341
|
+
debug_type=DebugType.EXECUTION,
|
|
342
|
+
)
|
|
343
|
+
return agent
|
|
114
344
|
|
|
115
345
|
async def handle_init_agent(self, operation: op.InitAgentOperation) -> None:
|
|
116
346
|
"""Initialize an agent for a session and replay history to UI."""
|
|
117
|
-
await self.
|
|
347
|
+
await self._ensure_agent(operation.session_id)
|
|
118
348
|
|
|
119
349
|
async def handle_user_input(self, operation: op.UserInputOperation) -> None:
|
|
120
350
|
"""Handle a user input operation by running it through an agent."""
|
|
@@ -123,7 +353,7 @@ class ExecutorContext:
|
|
|
123
353
|
raise ValueError("session_id cannot be None")
|
|
124
354
|
|
|
125
355
|
session_id = operation.session_id
|
|
126
|
-
agent = await self.
|
|
356
|
+
agent = await self._ensure_agent(session_id)
|
|
127
357
|
user_input = operation.input
|
|
128
358
|
|
|
129
359
|
# emit user input event
|
|
@@ -148,39 +378,7 @@ class ExecutorContext:
|
|
|
148
378
|
await self.emit_event(evt)
|
|
149
379
|
|
|
150
380
|
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}")
|
|
381
|
+
await self._action_executor.run(action, operation, agent)
|
|
184
382
|
|
|
185
383
|
async def handle_interrupt(self, operation: op.InterruptOperation) -> None:
|
|
186
384
|
"""Handle an interrupt by invoking agent.cancel() and cancelling tasks."""
|
|
@@ -189,11 +387,12 @@ class ExecutorContext:
|
|
|
189
387
|
if operation.target_session_id is not None:
|
|
190
388
|
session_ids: list[str] = [operation.target_session_id]
|
|
191
389
|
else:
|
|
192
|
-
|
|
390
|
+
agent = self._agent
|
|
391
|
+
session_ids = [agent.session.id] if agent is not None else []
|
|
193
392
|
|
|
194
393
|
# Call cancel() on each affected agent to persist an interrupt marker
|
|
195
394
|
for sid in session_ids:
|
|
196
|
-
agent = self.
|
|
395
|
+
agent = self._get_active_agent(sid)
|
|
197
396
|
if agent is not None:
|
|
198
397
|
for evt in agent.cancel():
|
|
199
398
|
await self.emit_event(evt)
|
|
@@ -222,69 +421,6 @@ class ExecutorContext:
|
|
|
222
421
|
# Remove from active tasks immediately
|
|
223
422
|
self.task_manager.remove(task_id)
|
|
224
423
|
|
|
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
424
|
def get_active_task(self, submission_id: str) -> asyncio.Task[None] | None:
|
|
289
425
|
"""Return the asyncio.Task for a submission id if one is registered."""
|
|
290
426
|
|
|
@@ -298,6 +434,16 @@ class ExecutorContext:
|
|
|
298
434
|
|
|
299
435
|
return self.task_manager.get(submission_id) is not None
|
|
300
436
|
|
|
437
|
+
def _get_active_agent(self, session_id: str) -> Agent | None:
|
|
438
|
+
"""Return the active agent if its session id matches ``session_id``."""
|
|
439
|
+
|
|
440
|
+
agent = self._agent
|
|
441
|
+
if agent is None:
|
|
442
|
+
return None
|
|
443
|
+
if agent.session.id != session_id:
|
|
444
|
+
return None
|
|
445
|
+
return agent
|
|
446
|
+
|
|
301
447
|
|
|
302
448
|
class Executor:
|
|
303
449
|
"""
|
|
@@ -312,8 +458,9 @@ class Executor:
|
|
|
312
458
|
event_queue: asyncio.Queue[events.Event],
|
|
313
459
|
llm_clients: LLMClients,
|
|
314
460
|
model_profile_provider: ModelProfileProvider | None = None,
|
|
461
|
+
on_model_change: Callable[[str], None] | None = None,
|
|
315
462
|
):
|
|
316
|
-
self.context = ExecutorContext(event_queue, llm_clients, model_profile_provider)
|
|
463
|
+
self.context = ExecutorContext(event_queue, llm_clients, model_profile_provider, on_model_change)
|
|
317
464
|
self.submission_queue: asyncio.Queue[op.Submission] = asyncio.Queue()
|
|
318
465
|
# Track completion events for all submissions (not just those with ActiveTask)
|
|
319
466
|
self._completion_events: dict[str, asyncio.Event] = {}
|
|
@@ -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.
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
You are a powerful code search agent.
|
|
2
2
|
|
|
3
|
-
CRITICAL:
|
|
3
|
+
=== CRITICAL: READ-ONLY MODE - NO FILE MODIFICATIONS ===
|
|
4
|
+
This is a READ-ONLY exploration task. You are STRICTLY PROHIBITED from:
|
|
5
|
+
- Creating new files (no Write, touch, or file creation of any kind)
|
|
6
|
+
- Modifying existing files (no Edit operations)
|
|
7
|
+
- Deleting files (no rm or deletion)
|
|
8
|
+
- Moving or copying files (no mv or cp)
|
|
9
|
+
- Creating temporary files anywhere, including /tmp
|
|
10
|
+
- Using redirect operators (>, >>, |) or heredocs to write to files
|
|
11
|
+
- Running ANY commands that change system state
|
|
4
12
|
|
|
5
13
|
Your strengths:
|
|
6
14
|
- Rapidly finding files using glob patterns
|
|
@@ -14,12 +22,16 @@ Guidelines:
|
|
|
14
22
|
- Use Bash ONLY for read-only operations (ls, git status, git log, git diff, find, cat, head, tail). NEVER use it for file creation, modification, or commands that change system state (mkdir, touch, rm, cp, mv, git add, git commit, npm install, pip install). NEVER use redirect operators (>, >>, |) or heredocs to create files
|
|
15
23
|
- Adapt your search approach based on the thoroughness level specified by the caller
|
|
16
24
|
- quick = scan obvious targets; medium = cover all related modules; very thorough = exhaustive sweep with validation
|
|
17
|
-
- For maximum efficiency, whenever you need to perform multiple independent operations, invoke all relevant tools simultaneously rather than sequentially.
|
|
18
25
|
- Only your last message is surfaced back to the agent as the final answer.
|
|
19
26
|
- Return file paths as absolute paths in your final response
|
|
20
27
|
- For clear communication, avoid using emojis
|
|
21
28
|
- Do not create any files, or run bash commands that modify the user's system state in any way (This includes temporary files in the /tmp folder. Never create these files, instead communicate your final report directly as a regular message)
|
|
22
29
|
|
|
30
|
+
NOTE: You are meant to be a fast agent that returns output as quickly as possible. In order to achieve this you must:
|
|
31
|
+
- Make efficient use of the tools that you have at your disposal: be smart about how you search for files and implementations
|
|
32
|
+
- Wherever possible you should try to spawn multiple parallel tool calls for grepping and reading files
|
|
33
|
+
|
|
34
|
+
|
|
23
35
|
Complete the user's search request efficiently and report your findings clearly.
|
|
24
36
|
|
|
25
37
|
Notes:
|
|
@@ -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
|
klaude_code/core/reminders.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import json
|
|
2
1
|
import re
|
|
3
2
|
import shlex
|
|
4
3
|
from collections.abc import Awaitable, Callable
|
|
@@ -282,7 +281,6 @@ def get_memory_paths() -> list[tuple[Path, str]]:
|
|
|
282
281
|
"user's private global instructions for all projects",
|
|
283
282
|
),
|
|
284
283
|
(Path.cwd() / "AGENTS.md", "project instructions, checked into the codebase"),
|
|
285
|
-
(Path.cwd() / "AGENT.md", "project instructions, checked into the codebase"),
|
|
286
284
|
(Path.cwd() / "CLAUDE.md", "project instructions, checked into the codebase"),
|
|
287
285
|
]
|
|
288
286
|
|
|
@@ -351,46 +349,22 @@ IMPORTANT: this context may or may not be relevant to your tasks. You should not
|
|
|
351
349
|
return None
|
|
352
350
|
|
|
353
351
|
|
|
354
|
-
def get_last_turn_tool_call(session: Session) -> list[model.ToolCallItem]:
|
|
355
|
-
tool_calls: list[model.ToolCallItem] = []
|
|
356
|
-
for item in reversed(session.conversation_history):
|
|
357
|
-
if isinstance(item, model.ToolCallItem):
|
|
358
|
-
tool_calls.append(item)
|
|
359
|
-
if isinstance(
|
|
360
|
-
item,
|
|
361
|
-
(
|
|
362
|
-
model.ReasoningEncryptedItem,
|
|
363
|
-
model.ReasoningTextItem,
|
|
364
|
-
model.AssistantMessageItem,
|
|
365
|
-
),
|
|
366
|
-
):
|
|
367
|
-
break
|
|
368
|
-
return tool_calls
|
|
369
|
-
|
|
370
|
-
|
|
371
352
|
MEMORY_FILE_NAMES = ["CLAUDE.md", "AGENTS.md", "AGENT.md"]
|
|
372
353
|
|
|
373
354
|
|
|
374
355
|
async def last_path_memory_reminder(
|
|
375
356
|
session: Session,
|
|
376
357
|
) -> model.DeveloperMessageItem | None:
|
|
377
|
-
"""
|
|
378
|
-
|
|
379
|
-
|
|
358
|
+
"""Load CLAUDE.md/AGENTS.md from directories containing files in file_tracker.
|
|
359
|
+
|
|
360
|
+
Uses session.file_tracker to detect accessed paths (works for both tool calls
|
|
361
|
+
and @ file references). Uses session.loaded_memory to avoid duplicate loading.
|
|
362
|
+
"""
|
|
363
|
+
if not session.file_tracker:
|
|
380
364
|
return None
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
if tool_call.name in (tools.READ, tools.EDIT, tools.MULTI_EDIT, tools.WRITE):
|
|
384
|
-
try:
|
|
385
|
-
json_dict = json.loads(tool_call.arguments)
|
|
386
|
-
if path := json_dict.get("file_path", ""):
|
|
387
|
-
paths.append(path)
|
|
388
|
-
except json.JSONDecodeError:
|
|
389
|
-
continue
|
|
390
|
-
paths = list(set(paths))
|
|
365
|
+
|
|
366
|
+
paths = list(session.file_tracker.keys())
|
|
391
367
|
memories: list[Memory] = []
|
|
392
|
-
if len(paths) == 0:
|
|
393
|
-
return None
|
|
394
368
|
|
|
395
369
|
cwd = Path.cwd().resolve()
|
|
396
370
|
loaded_set: set[str] = set(session.loaded_memory)
|
|
@@ -484,8 +458,8 @@ def load_agent_reminders(
|
|
|
484
458
|
reminders.extend(
|
|
485
459
|
[
|
|
486
460
|
memory_reminder,
|
|
487
|
-
last_path_memory_reminder,
|
|
488
461
|
at_file_reader_reminder,
|
|
462
|
+
last_path_memory_reminder,
|
|
489
463
|
file_changed_externally_reminder,
|
|
490
464
|
image_reminder,
|
|
491
465
|
]
|
klaude_code/core/task.py
CHANGED
|
@@ -29,6 +29,8 @@ class MetadataAccumulator:
|
|
|
29
29
|
self._sub_agent_metadata: list[model.TaskMetadata] = []
|
|
30
30
|
self._throughput_weighted_sum: float = 0.0
|
|
31
31
|
self._throughput_tracked_tokens: int = 0
|
|
32
|
+
self._first_token_latency_sum: float = 0.0
|
|
33
|
+
self._first_token_latency_count: int = 0
|
|
32
34
|
self._turn_count: int = 0
|
|
33
35
|
|
|
34
36
|
def add(self, turn_metadata: model.ResponseMetadataItem) -> None:
|
|
@@ -51,13 +53,8 @@ class MetadataAccumulator:
|
|
|
51
53
|
acc_usage.context_limit = usage.context_limit
|
|
52
54
|
|
|
53
55
|
if usage.first_token_latency_ms is not None:
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
else:
|
|
57
|
-
acc_usage.first_token_latency_ms = min(
|
|
58
|
-
acc_usage.first_token_latency_ms,
|
|
59
|
-
usage.first_token_latency_ms,
|
|
60
|
-
)
|
|
56
|
+
self._first_token_latency_sum += usage.first_token_latency_ms
|
|
57
|
+
self._first_token_latency_count += 1
|
|
61
58
|
|
|
62
59
|
if usage.throughput_tps is not None:
|
|
63
60
|
current_output = usage.output_tokens
|
|
@@ -83,6 +80,11 @@ class MetadataAccumulator:
|
|
|
83
80
|
else:
|
|
84
81
|
main.usage.throughput_tps = None
|
|
85
82
|
|
|
83
|
+
if self._first_token_latency_count > 0:
|
|
84
|
+
main.usage.first_token_latency_ms = self._first_token_latency_sum / self._first_token_latency_count
|
|
85
|
+
else:
|
|
86
|
+
main.usage.first_token_latency_ms = None
|
|
87
|
+
|
|
86
88
|
main.task_duration_s = task_duration_s
|
|
87
89
|
main.turn_count = self._turn_count
|
|
88
90
|
return model.TaskMetadataItem(main=main, sub_agent_task_metadata=self._sub_agent_metadata)
|