klaude-code 1.2.18__py3-none-any.whl → 1.2.20__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/main.py +42 -22
- klaude_code/cli/runtime.py +46 -2
- klaude_code/{version.py → cli/self_update.py} +110 -2
- klaude_code/command/__init__.py +1 -3
- klaude_code/command/clear_cmd.py +5 -4
- klaude_code/command/command_abc.py +5 -40
- klaude_code/command/debug_cmd.py +2 -2
- klaude_code/command/diff_cmd.py +2 -1
- klaude_code/command/export_cmd.py +14 -49
- klaude_code/command/export_online_cmd.py +10 -4
- klaude_code/command/help_cmd.py +2 -1
- klaude_code/command/model_cmd.py +7 -5
- klaude_code/command/prompt-jj-workspace.md +18 -0
- klaude_code/command/prompt_command.py +16 -9
- klaude_code/command/refresh_cmd.py +3 -2
- klaude_code/command/registry.py +98 -28
- klaude_code/command/release_notes_cmd.py +2 -1
- klaude_code/command/status_cmd.py +2 -1
- klaude_code/command/terminal_setup_cmd.py +2 -1
- klaude_code/command/thinking_cmd.py +6 -4
- klaude_code/core/executor.py +187 -180
- klaude_code/core/manager/sub_agent_manager.py +3 -0
- klaude_code/core/prompt.py +4 -1
- klaude_code/core/prompts/prompt-sub-agent-explore.md +14 -2
- klaude_code/core/prompts/prompt-sub-agent-web.md +3 -3
- klaude_code/core/reminders.py +70 -26
- klaude_code/core/task.py +13 -12
- klaude_code/core/tool/__init__.py +2 -0
- klaude_code/core/tool/file/apply_patch_tool.py +3 -1
- klaude_code/core/tool/file/edit_tool.py +7 -5
- klaude_code/core/tool/file/multi_edit_tool.py +7 -5
- klaude_code/core/tool/file/read_tool.md +1 -1
- klaude_code/core/tool/file/read_tool.py +8 -4
- klaude_code/core/tool/file/write_tool.py +8 -6
- klaude_code/core/tool/memory/skill_loader.py +12 -10
- klaude_code/core/tool/shell/bash_tool.py +89 -17
- klaude_code/core/tool/sub_agent_tool.py +5 -1
- klaude_code/core/tool/tool_abc.py +18 -0
- klaude_code/core/tool/tool_context.py +6 -6
- klaude_code/core/tool/tool_registry.py +1 -1
- klaude_code/core/tool/tool_runner.py +7 -7
- klaude_code/core/tool/web/web_fetch_tool.py +77 -22
- klaude_code/core/tool/web/web_search_tool.py +5 -1
- 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/model.py +8 -1
- klaude_code/protocol/op.py +47 -0
- klaude_code/protocol/op_handler.py +25 -1
- klaude_code/protocol/sub_agent/web.py +1 -1
- klaude_code/session/codec.py +71 -0
- klaude_code/session/export.py +21 -11
- klaude_code/session/session.py +186 -322
- klaude_code/session/store.py +215 -0
- klaude_code/session/templates/export_session.html +48 -47
- klaude_code/ui/modes/repl/completers.py +211 -71
- klaude_code/ui/modes/repl/event_handler.py +7 -23
- klaude_code/ui/modes/repl/input_prompt_toolkit.py +5 -7
- 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 +10 -1
- klaude_code/ui/renderers/tools.py +3 -4
- klaude_code/ui/rich/__init__.py +10 -1
- klaude_code/ui/rich/cjk_wrap.py +228 -0
- klaude_code/ui/rich/status.py +0 -1
- klaude_code/ui/utils/common.py +0 -18
- {klaude_code-1.2.18.dist-info → klaude_code-1.2.20.dist-info}/METADATA +18 -2
- {klaude_code-1.2.18.dist-info → klaude_code-1.2.20.dist-info}/RECORD +73 -70
- klaude_code/ui/utils/debouncer.py +0 -42
- {klaude_code-1.2.18.dist-info → klaude_code-1.2.20.dist-info}/WHEEL +0 -0
- {klaude_code-1.2.18.dist-info → klaude_code-1.2.20.dist-info}/entry_points.txt +0 -0
klaude_code/core/executor.py
CHANGED
|
@@ -8,10 +8,12 @@ handling operations submitted from the CLI and coordinating with agents.
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
import asyncio
|
|
11
|
-
|
|
11
|
+
import subprocess
|
|
12
|
+
from collections.abc import Callable
|
|
12
13
|
from dataclasses import dataclass
|
|
14
|
+
from pathlib import Path
|
|
13
15
|
|
|
14
|
-
from klaude_code.command import
|
|
16
|
+
from klaude_code.command import dispatch_command
|
|
15
17
|
from klaude_code.config import load_config
|
|
16
18
|
from klaude_code.core.agent import Agent, DefaultModelProfileProvider, ModelProfileProvider
|
|
17
19
|
from klaude_code.core.manager import LLMClients, SubAgentManager
|
|
@@ -20,6 +22,7 @@ from klaude_code.llm.registry import create_llm_client
|
|
|
20
22
|
from klaude_code.protocol import commands, events, model, op
|
|
21
23
|
from klaude_code.protocol.op_handler import OperationHandler
|
|
22
24
|
from klaude_code.protocol.sub_agent import SubAgentResult
|
|
25
|
+
from klaude_code.session.export import build_export_html, get_default_export_path
|
|
23
26
|
from klaude_code.session.session import Session
|
|
24
27
|
from klaude_code.trace import DebugType, log_debug
|
|
25
28
|
|
|
@@ -76,164 +79,6 @@ class TaskManager:
|
|
|
76
79
|
self._tasks.clear()
|
|
77
80
|
|
|
78
81
|
|
|
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
|
-
|
|
237
82
|
class ExecutorContext:
|
|
238
83
|
"""
|
|
239
84
|
Context object providing shared state and operations for the executor.
|
|
@@ -249,6 +94,7 @@ class ExecutorContext:
|
|
|
249
94
|
event_queue: asyncio.Queue[events.Event],
|
|
250
95
|
llm_clients: LLMClients,
|
|
251
96
|
model_profile_provider: ModelProfileProvider | None = None,
|
|
97
|
+
on_model_change: Callable[[str], None] | None = None,
|
|
252
98
|
):
|
|
253
99
|
self.event_queue: asyncio.Queue[events.Event] = event_queue
|
|
254
100
|
self.llm_clients: LLMClients = llm_clients
|
|
@@ -258,12 +104,7 @@ class ExecutorContext:
|
|
|
258
104
|
|
|
259
105
|
self.task_manager = TaskManager()
|
|
260
106
|
self.sub_agent_manager = SubAgentManager(event_queue, llm_clients, resolved_profile_provider)
|
|
261
|
-
self.
|
|
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
|
-
)
|
|
107
|
+
self._on_model_change = on_model_change
|
|
267
108
|
self._agent: Agent | None = None
|
|
268
109
|
|
|
269
110
|
async def emit_event(self, event: events.Event) -> None:
|
|
@@ -302,6 +143,13 @@ class ExecutorContext:
|
|
|
302
143
|
|
|
303
144
|
session = Session.create() if session_id is None else Session.load(session_id)
|
|
304
145
|
|
|
146
|
+
if (
|
|
147
|
+
session.model_thinking is not None
|
|
148
|
+
and session.model_name
|
|
149
|
+
and session.model_name == self.llm_clients.main.model_name
|
|
150
|
+
):
|
|
151
|
+
self.llm_clients.main.get_llm_config().thinking = session.model_thinking
|
|
152
|
+
|
|
305
153
|
profile = self.model_profile_provider.build_profile(self.llm_clients.main)
|
|
306
154
|
agent = Agent(session=session, profile=profile)
|
|
307
155
|
|
|
@@ -328,7 +176,7 @@ class ExecutorContext:
|
|
|
328
176
|
await self._ensure_agent(operation.session_id)
|
|
329
177
|
|
|
330
178
|
async def handle_user_input(self, operation: op.UserInputOperation) -> None:
|
|
331
|
-
"""Handle a user input operation by
|
|
179
|
+
"""Handle a user input operation by dispatching it into operations."""
|
|
332
180
|
|
|
333
181
|
if operation.session_id is None:
|
|
334
182
|
raise ValueError("session_id cannot be None")
|
|
@@ -337,29 +185,187 @@ class ExecutorContext:
|
|
|
337
185
|
agent = await self._ensure_agent(session_id)
|
|
338
186
|
user_input = operation.input
|
|
339
187
|
|
|
340
|
-
#
|
|
188
|
+
# Emit the original user input to UI (even if the persisted text differs).
|
|
341
189
|
await self.emit_event(
|
|
342
190
|
events.UserMessageEvent(content=user_input.text, session_id=session_id, images=user_input.images)
|
|
343
191
|
)
|
|
344
192
|
|
|
345
|
-
result = await dispatch_command(user_input
|
|
193
|
+
result = await dispatch_command(user_input, agent, submission_id=operation.id)
|
|
194
|
+
ops: list[op.Operation] = list(result.operations or [])
|
|
346
195
|
|
|
347
|
-
|
|
196
|
+
run_ops = [candidate for candidate in ops if isinstance(candidate, op.RunAgentOperation)]
|
|
197
|
+
if len(run_ops) > 1:
|
|
198
|
+
raise ValueError("Multiple RunAgentOperation results are not supported")
|
|
348
199
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
200
|
+
persisted_user_input = run_ops[0].input if run_ops else user_input
|
|
201
|
+
agent.session.append_history(
|
|
202
|
+
[model.UserMessageItem(content=persisted_user_input.text, images=persisted_user_input.images)]
|
|
203
|
+
)
|
|
353
204
|
|
|
354
205
|
if result.events:
|
|
355
|
-
agent.session.append_history(
|
|
356
|
-
[evt.item for evt in result.events if isinstance(evt, events.DeveloperMessageEvent)]
|
|
357
|
-
)
|
|
358
206
|
for evt in result.events:
|
|
207
|
+
if isinstance(evt, events.DeveloperMessageEvent):
|
|
208
|
+
agent.session.append_history([evt.item])
|
|
359
209
|
await self.emit_event(evt)
|
|
360
210
|
|
|
361
|
-
for
|
|
362
|
-
await
|
|
211
|
+
for operation_item in ops:
|
|
212
|
+
await operation_item.execute(handler=self)
|
|
213
|
+
|
|
214
|
+
async def handle_run_agent(self, operation: op.RunAgentOperation) -> None:
|
|
215
|
+
agent = await self._ensure_agent(operation.session_id)
|
|
216
|
+
existing_active = self.task_manager.get(operation.id)
|
|
217
|
+
if existing_active is not None and not existing_active.task.done():
|
|
218
|
+
raise RuntimeError(f"Active task already registered for operation {operation.id}")
|
|
219
|
+
task: asyncio.Task[None] = asyncio.create_task(
|
|
220
|
+
self._run_agent_task(agent, operation.input, operation.id, operation.session_id)
|
|
221
|
+
)
|
|
222
|
+
self.task_manager.register(operation.id, task, operation.session_id)
|
|
223
|
+
|
|
224
|
+
async def handle_change_model(self, operation: op.ChangeModelOperation) -> None:
|
|
225
|
+
agent = await self._ensure_agent(operation.session_id)
|
|
226
|
+
config = load_config()
|
|
227
|
+
if config is None:
|
|
228
|
+
raise ValueError("Configuration must be initialized before changing model")
|
|
229
|
+
|
|
230
|
+
llm_config = config.get_model_config(operation.model_name)
|
|
231
|
+
llm_client = create_llm_client(llm_config)
|
|
232
|
+
agent.set_model_profile(self.model_profile_provider.build_profile(llm_client))
|
|
233
|
+
|
|
234
|
+
agent.session.model_config_name = operation.model_name
|
|
235
|
+
agent.session.model_thinking = llm_config.thinking
|
|
236
|
+
|
|
237
|
+
developer_item = model.DeveloperMessageItem(
|
|
238
|
+
content=f"switched to model: {operation.model_name}",
|
|
239
|
+
command_output=model.CommandOutput(command_name=commands.CommandName.MODEL),
|
|
240
|
+
)
|
|
241
|
+
agent.session.append_history([developer_item])
|
|
242
|
+
|
|
243
|
+
await self.emit_event(events.DeveloperMessageEvent(session_id=agent.session.id, item=developer_item))
|
|
244
|
+
await self.emit_event(events.WelcomeEvent(llm_config=llm_config, work_dir=str(agent.session.work_dir)))
|
|
245
|
+
|
|
246
|
+
if self._on_model_change is not None:
|
|
247
|
+
self._on_model_change(llm_client.model_name)
|
|
248
|
+
|
|
249
|
+
async def handle_clear_session(self, operation: op.ClearSessionOperation) -> None:
|
|
250
|
+
agent = await self._ensure_agent(operation.session_id)
|
|
251
|
+
new_session = Session.create(work_dir=agent.session.work_dir)
|
|
252
|
+
new_session.model_name = agent.session.model_name
|
|
253
|
+
new_session.model_config_name = agent.session.model_config_name
|
|
254
|
+
new_session.model_thinking = agent.session.model_thinking
|
|
255
|
+
agent.session = new_session
|
|
256
|
+
|
|
257
|
+
developer_item = model.DeveloperMessageItem(
|
|
258
|
+
content="started new conversation",
|
|
259
|
+
command_output=model.CommandOutput(command_name=commands.CommandName.CLEAR),
|
|
260
|
+
)
|
|
261
|
+
await self.emit_event(events.DeveloperMessageEvent(session_id=agent.session.id, item=developer_item))
|
|
262
|
+
|
|
263
|
+
async def handle_export_session(self, operation: op.ExportSessionOperation) -> None:
|
|
264
|
+
agent = await self._ensure_agent(operation.session_id)
|
|
265
|
+
try:
|
|
266
|
+
output_path = self._resolve_export_output_path(operation.output_path, agent.session)
|
|
267
|
+
html_doc = self._build_export_html(agent)
|
|
268
|
+
await asyncio.to_thread(output_path.parent.mkdir, parents=True, exist_ok=True)
|
|
269
|
+
await asyncio.to_thread(output_path.write_text, html_doc, "utf-8")
|
|
270
|
+
await asyncio.to_thread(self._open_file, output_path)
|
|
271
|
+
developer_item = model.DeveloperMessageItem(
|
|
272
|
+
content=f"Session exported and opened: {output_path}",
|
|
273
|
+
command_output=model.CommandOutput(command_name=commands.CommandName.EXPORT),
|
|
274
|
+
)
|
|
275
|
+
agent.session.append_history([developer_item])
|
|
276
|
+
await self.emit_event(events.DeveloperMessageEvent(session_id=agent.session.id, item=developer_item))
|
|
277
|
+
except Exception as exc: # pragma: no cover
|
|
278
|
+
import traceback
|
|
279
|
+
|
|
280
|
+
developer_item = model.DeveloperMessageItem(
|
|
281
|
+
content=f"Failed to export session: {exc}\n{traceback.format_exc()}",
|
|
282
|
+
command_output=model.CommandOutput(command_name=commands.CommandName.EXPORT, is_error=True),
|
|
283
|
+
)
|
|
284
|
+
agent.session.append_history([developer_item])
|
|
285
|
+
await self.emit_event(events.DeveloperMessageEvent(session_id=agent.session.id, item=developer_item))
|
|
286
|
+
|
|
287
|
+
async def _run_agent_task(
|
|
288
|
+
self,
|
|
289
|
+
agent: Agent,
|
|
290
|
+
user_input: model.UserInputPayload,
|
|
291
|
+
task_id: str,
|
|
292
|
+
session_id: str,
|
|
293
|
+
) -> None:
|
|
294
|
+
try:
|
|
295
|
+
log_debug(
|
|
296
|
+
f"Starting agent task {task_id} for session {session_id}",
|
|
297
|
+
style="green",
|
|
298
|
+
debug_type=DebugType.EXECUTION,
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
async def _runner(state: model.SubAgentState) -> SubAgentResult:
|
|
302
|
+
return await self.sub_agent_manager.run_sub_agent(agent, state)
|
|
303
|
+
|
|
304
|
+
token = current_run_subtask_callback.set(_runner)
|
|
305
|
+
try:
|
|
306
|
+
async for event in agent.run_task(user_input):
|
|
307
|
+
await self.emit_event(event)
|
|
308
|
+
finally:
|
|
309
|
+
current_run_subtask_callback.reset(token)
|
|
310
|
+
|
|
311
|
+
except asyncio.CancelledError:
|
|
312
|
+
log_debug(
|
|
313
|
+
f"Agent task {task_id} was cancelled",
|
|
314
|
+
style="yellow",
|
|
315
|
+
debug_type=DebugType.EXECUTION,
|
|
316
|
+
)
|
|
317
|
+
await self.emit_event(events.TaskFinishEvent(session_id=session_id, task_result="task cancelled"))
|
|
318
|
+
|
|
319
|
+
except Exception as e:
|
|
320
|
+
import traceback
|
|
321
|
+
|
|
322
|
+
log_debug(
|
|
323
|
+
f"Agent task {task_id} failed: {e!s}",
|
|
324
|
+
style="red",
|
|
325
|
+
debug_type=DebugType.EXECUTION,
|
|
326
|
+
)
|
|
327
|
+
log_debug(traceback.format_exc(), style="red", debug_type=DebugType.EXECUTION)
|
|
328
|
+
await self.emit_event(
|
|
329
|
+
events.ErrorEvent(
|
|
330
|
+
error_message=f"Agent task failed: [{e.__class__.__name__}] {e!s}",
|
|
331
|
+
can_retry=False,
|
|
332
|
+
)
|
|
333
|
+
)
|
|
334
|
+
finally:
|
|
335
|
+
self.task_manager.remove(task_id)
|
|
336
|
+
log_debug(
|
|
337
|
+
f"Cleaned up agent task {task_id}",
|
|
338
|
+
style="cyan",
|
|
339
|
+
debug_type=DebugType.EXECUTION,
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
def _resolve_export_output_path(self, raw: str | None, session: Session) -> Path:
|
|
343
|
+
trimmed = (raw or "").strip()
|
|
344
|
+
if trimmed:
|
|
345
|
+
candidate = Path(trimmed).expanduser()
|
|
346
|
+
if not candidate.is_absolute():
|
|
347
|
+
candidate = Path(session.work_dir) / candidate
|
|
348
|
+
if candidate.suffix.lower() != ".html":
|
|
349
|
+
candidate = candidate.with_suffix(".html")
|
|
350
|
+
return candidate
|
|
351
|
+
return get_default_export_path(session)
|
|
352
|
+
|
|
353
|
+
def _build_export_html(self, agent: Agent) -> str:
|
|
354
|
+
profile = agent.profile
|
|
355
|
+
system_prompt = (profile.system_prompt if profile else "") or ""
|
|
356
|
+
tool_schemas = profile.tools if profile else []
|
|
357
|
+
model_name = profile.llm_client.model_name if profile else "unknown"
|
|
358
|
+
return build_export_html(agent.session, system_prompt, tool_schemas, model_name)
|
|
359
|
+
|
|
360
|
+
def _open_file(self, path: Path) -> None:
|
|
361
|
+
try:
|
|
362
|
+
subprocess.run(["open", str(path)], check=True)
|
|
363
|
+
except FileNotFoundError as exc: # pragma: no cover
|
|
364
|
+
msg = "`open` command not found; please open the HTML manually."
|
|
365
|
+
raise RuntimeError(msg) from exc
|
|
366
|
+
except subprocess.CalledProcessError as exc: # pragma: no cover
|
|
367
|
+
msg = f"Failed to open HTML with `open`: {exc}"
|
|
368
|
+
raise RuntimeError(msg) from exc
|
|
363
369
|
|
|
364
370
|
async def handle_interrupt(self, operation: op.InterruptOperation) -> None:
|
|
365
371
|
"""Handle an interrupt by invoking agent.cancel() and cancelling tasks."""
|
|
@@ -439,8 +445,9 @@ class Executor:
|
|
|
439
445
|
event_queue: asyncio.Queue[events.Event],
|
|
440
446
|
llm_clients: LLMClients,
|
|
441
447
|
model_profile_provider: ModelProfileProvider | None = None,
|
|
448
|
+
on_model_change: Callable[[str], None] | None = None,
|
|
442
449
|
):
|
|
443
|
-
self.context = ExecutorContext(event_queue, llm_clients, model_profile_provider)
|
|
450
|
+
self.context = ExecutorContext(event_queue, llm_clients, model_profile_provider, on_model_change)
|
|
444
451
|
self.submission_queue: asyncio.Queue[op.Submission] = asyncio.Queue()
|
|
445
452
|
# Track completion events for all submissions (not just those with ActiveTask)
|
|
446
453
|
self._completion_events: dict[str, asyncio.Event] = {}
|
|
@@ -80,6 +80,9 @@ Only the content passed to `report_back` will be returned to user.\
|
|
|
80
80
|
result: str = ""
|
|
81
81
|
task_metadata: model.TaskMetadata | None = None
|
|
82
82
|
sub_agent_input = model.UserInputPayload(text=state.sub_agent_prompt, images=None)
|
|
83
|
+
child_session.append_history(
|
|
84
|
+
[model.UserMessageItem(content=sub_agent_input.text, images=sub_agent_input.images)]
|
|
85
|
+
)
|
|
83
86
|
async for event in child_agent.run_task(sub_agent_input):
|
|
84
87
|
# Capture TaskFinishEvent content for return
|
|
85
88
|
if isinstance(event, events.TaskFinishEvent):
|
klaude_code/core/prompt.py
CHANGED
|
@@ -12,6 +12,7 @@ COMMAND_DESCRIPTIONS: dict[str, str] = {
|
|
|
12
12
|
"fd": "simple and fast alternative to find",
|
|
13
13
|
"tree": "directory listing as a tree",
|
|
14
14
|
"sg": "ast-grep - AST-aware code search",
|
|
15
|
+
"jj": "jujutsu - Git-compatible version control system",
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
# Mapping from logical prompt keys to resource file paths under the core/prompt directory.
|
|
@@ -57,18 +58,20 @@ def _build_env_info(model_name: str) -> str:
|
|
|
57
58
|
cwd = Path.cwd()
|
|
58
59
|
today = datetime.datetime.now().strftime("%Y-%m-%d")
|
|
59
60
|
is_git_repo = (cwd / ".git").exists()
|
|
61
|
+
is_empty_dir = not any(cwd.iterdir())
|
|
60
62
|
|
|
61
63
|
available_tools: list[str] = []
|
|
62
64
|
for command, desc in COMMAND_DESCRIPTIONS.items():
|
|
63
65
|
if shutil.which(command) is not None:
|
|
64
66
|
available_tools.append(f"{command}: {desc}")
|
|
65
67
|
|
|
68
|
+
cwd_display = f"{cwd} (empty)" if is_empty_dir else str(cwd)
|
|
66
69
|
env_lines: list[str] = [
|
|
67
70
|
"",
|
|
68
71
|
"",
|
|
69
72
|
"Here is useful information about the environment you are running in:",
|
|
70
73
|
"<env>",
|
|
71
|
-
f"Working directory: {
|
|
74
|
+
f"Working directory: {cwd_display}",
|
|
72
75
|
f"Today's Date: {today}",
|
|
73
76
|
f"Is directory a git repo: {is_git_repo}",
|
|
74
77
|
f"You are powered by the model: {model_name}",
|
|
@@ -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:
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
You are a web research
|
|
1
|
+
You are a web research subagent that searches and fetches web content to provide up-to-date information as part of team.
|
|
2
2
|
|
|
3
3
|
## Available Tools
|
|
4
4
|
|
|
@@ -28,7 +28,7 @@ Balance efficiency with thoroughness. For open-ended questions (e.g., "recommend
|
|
|
28
28
|
- Avoid repeating similar queries - they won't yield new results
|
|
29
29
|
- NEVER use '-', 'site:', or quotes unless explicitly asked
|
|
30
30
|
- Include year/date for time-sensitive queries (check "Today's date" in <env>), don't limit yourself to your knowledge cutoff date
|
|
31
|
-
-
|
|
31
|
+
- Always use WebFetch to get the complete contents of websites - search snippets are often insufficient
|
|
32
32
|
- Follow relevant links on pages with WebFetch
|
|
33
33
|
- If truncated results are saved to local files, use grep/read to explore
|
|
34
34
|
|
|
@@ -48,4 +48,4 @@ You MUST end every response with a "Sources:" section listing all URLs with thei
|
|
|
48
48
|
|
|
49
49
|
Sources:
|
|
50
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
|
|
51
|
+
- [Another Source](https://example.com/page) -> /tmp/klaude/web/example_com_page-123456.md
|