klaude-code 1.2.19__py3-none-any.whl → 1.2.21__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 +23 -0
- klaude_code/cli/runtime.py +17 -0
- 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 +2 -1
- 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 +31 -6
- 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 +12 -1
- klaude_code/core/executor.py +177 -190
- klaude_code/core/manager/sub_agent_manager.py +3 -0
- klaude_code/core/prompt.py +4 -1
- klaude_code/core/prompts/prompt-sub-agent-web.md +3 -3
- klaude_code/core/reminders.py +70 -26
- klaude_code/core/task.py +4 -5
- 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.py +5 -2
- klaude_code/core/tool/file/write_tool.py +8 -6
- klaude_code/core/tool/shell/bash_tool.py +90 -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_runner.py +7 -7
- klaude_code/core/tool/web/mermaid_tool.md +26 -0
- klaude_code/core/tool/web/web_fetch_tool.py +77 -22
- klaude_code/core/tool/web/web_search_tool.py +5 -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 +182 -331
- klaude_code/session/store.py +215 -0
- klaude_code/session/templates/export_session.html +13 -14
- klaude_code/ui/modes/repl/completers.py +1 -2
- klaude_code/ui/modes/repl/event_handler.py +7 -23
- klaude_code/ui/modes/repl/input_prompt_toolkit.py +4 -6
- 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-1.2.19.dist-info → klaude_code-1.2.21.dist-info}/METADATA +2 -1
- {klaude_code-1.2.19.dist-info → klaude_code-1.2.21.dist-info}/RECORD +58 -55
- klaude_code/ui/utils/debouncer.py +0 -42
- {klaude_code-1.2.19.dist-info → klaude_code-1.2.21.dist-info}/WHEEL +0 -0
- {klaude_code-1.2.19.dist-info → klaude_code-1.2.21.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,174 +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
|
-
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
|
-
|
|
247
82
|
class ExecutorContext:
|
|
248
83
|
"""
|
|
249
84
|
Context object providing shared state and operations for the executor.
|
|
@@ -269,13 +104,7 @@ class ExecutorContext:
|
|
|
269
104
|
|
|
270
105
|
self.task_manager = TaskManager()
|
|
271
106
|
self.sub_agent_manager = SubAgentManager(event_queue, llm_clients, resolved_profile_provider)
|
|
272
|
-
self.
|
|
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
|
-
)
|
|
107
|
+
self._on_model_change = on_model_change
|
|
279
108
|
self._agent: Agent | None = None
|
|
280
109
|
|
|
281
110
|
async def emit_event(self, event: events.Event) -> None:
|
|
@@ -347,7 +176,7 @@ class ExecutorContext:
|
|
|
347
176
|
await self._ensure_agent(operation.session_id)
|
|
348
177
|
|
|
349
178
|
async def handle_user_input(self, operation: op.UserInputOperation) -> None:
|
|
350
|
-
"""Handle a user input operation by
|
|
179
|
+
"""Handle a user input operation by dispatching it into operations."""
|
|
351
180
|
|
|
352
181
|
if operation.session_id is None:
|
|
353
182
|
raise ValueError("session_id cannot be None")
|
|
@@ -356,29 +185,187 @@ class ExecutorContext:
|
|
|
356
185
|
agent = await self._ensure_agent(session_id)
|
|
357
186
|
user_input = operation.input
|
|
358
187
|
|
|
359
|
-
#
|
|
188
|
+
# Emit the original user input to UI (even if the persisted text differs).
|
|
360
189
|
await self.emit_event(
|
|
361
190
|
events.UserMessageEvent(content=user_input.text, session_id=session_id, images=user_input.images)
|
|
362
191
|
)
|
|
363
192
|
|
|
364
|
-
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 [])
|
|
365
195
|
|
|
366
|
-
|
|
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")
|
|
367
199
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
+
)
|
|
372
204
|
|
|
373
205
|
if result.events:
|
|
374
|
-
agent.session.append_history(
|
|
375
|
-
[evt.item for evt in result.events if isinstance(evt, events.DeveloperMessageEvent)]
|
|
376
|
-
)
|
|
377
206
|
for evt in result.events:
|
|
207
|
+
if isinstance(evt, events.DeveloperMessageEvent):
|
|
208
|
+
agent.session.append_history([evt.item])
|
|
378
209
|
await self.emit_event(evt)
|
|
379
210
|
|
|
380
|
-
for
|
|
381
|
-
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
|
|
382
369
|
|
|
383
370
|
async def handle_interrupt(self, operation: op.InterruptOperation) -> None:
|
|
384
371
|
"""Handle an interrupt by invoking agent.cancel() and cancelling tasks."""
|
|
@@ -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,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
|
klaude_code/core/reminders.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import re
|
|
2
2
|
import shlex
|
|
3
3
|
from collections.abc import Awaitable, Callable
|
|
4
|
+
from dataclasses import dataclass
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
|
|
6
7
|
from pydantic import BaseModel
|
|
@@ -31,6 +32,45 @@ def get_last_new_user_input(session: Session) -> str | None:
|
|
|
31
32
|
return "\n\n".join(result)
|
|
32
33
|
|
|
33
34
|
|
|
35
|
+
@dataclass
|
|
36
|
+
class AtPatternSource:
|
|
37
|
+
"""Represents an @ pattern with its source file (if from a memory file)."""
|
|
38
|
+
|
|
39
|
+
pattern: str
|
|
40
|
+
mentioned_in: str | None = None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def get_at_patterns_with_source(session: Session) -> list[AtPatternSource]:
|
|
44
|
+
"""Get @ patterns from last user input and developer messages, preserving source info."""
|
|
45
|
+
patterns: list[AtPatternSource] = []
|
|
46
|
+
|
|
47
|
+
for item in reversed(session.conversation_history):
|
|
48
|
+
if isinstance(item, model.ToolResultItem):
|
|
49
|
+
break
|
|
50
|
+
|
|
51
|
+
if isinstance(item, model.UserMessageItem):
|
|
52
|
+
content = item.content or ""
|
|
53
|
+
if "@" in content:
|
|
54
|
+
for match in AT_FILE_PATTERN.finditer(content):
|
|
55
|
+
path_str = match.group("quoted") or match.group("plain")
|
|
56
|
+
if path_str:
|
|
57
|
+
patterns.append(AtPatternSource(pattern=path_str, mentioned_in=None))
|
|
58
|
+
break
|
|
59
|
+
|
|
60
|
+
if isinstance(item, model.DeveloperMessageItem):
|
|
61
|
+
content = item.content or ""
|
|
62
|
+
if "@" not in content:
|
|
63
|
+
continue
|
|
64
|
+
# Use first memory_path as the source if available
|
|
65
|
+
source = item.memory_paths[0] if item.memory_paths else None
|
|
66
|
+
for match in AT_FILE_PATTERN.finditer(content):
|
|
67
|
+
path_str = match.group("quoted") or match.group("plain")
|
|
68
|
+
if path_str:
|
|
69
|
+
patterns.append(AtPatternSource(pattern=path_str, mentioned_in=source))
|
|
70
|
+
|
|
71
|
+
return patterns
|
|
72
|
+
|
|
73
|
+
|
|
34
74
|
async def _load_at_file_recursive(
|
|
35
75
|
session: Session,
|
|
36
76
|
pattern: str,
|
|
@@ -99,33 +139,22 @@ async def at_file_reader_reminder(
|
|
|
99
139
|
session: Session,
|
|
100
140
|
) -> model.DeveloperMessageItem | None:
|
|
101
141
|
"""Parse @foo/bar to read, with recursive loading of nested @ references"""
|
|
102
|
-
|
|
103
|
-
if not
|
|
104
|
-
return None
|
|
105
|
-
|
|
106
|
-
at_patterns: list[str] = []
|
|
107
|
-
|
|
108
|
-
for match in AT_FILE_PATTERN.finditer(last_user_input):
|
|
109
|
-
quoted = match.group("quoted")
|
|
110
|
-
plain = match.group("plain")
|
|
111
|
-
path_str = quoted if quoted is not None else plain
|
|
112
|
-
if path_str:
|
|
113
|
-
at_patterns.append(path_str)
|
|
114
|
-
|
|
115
|
-
if len(at_patterns) == 0:
|
|
142
|
+
at_pattern_sources = get_at_patterns_with_source(session)
|
|
143
|
+
if not at_pattern_sources:
|
|
116
144
|
return None
|
|
117
145
|
|
|
118
146
|
at_files: dict[str, model.AtPatternParseResult] = {} # path -> content
|
|
119
147
|
collected_images: list[model.ImageURLPart] = []
|
|
120
148
|
visited: set[str] = set()
|
|
121
149
|
|
|
122
|
-
for
|
|
150
|
+
for source in at_pattern_sources:
|
|
123
151
|
await _load_at_file_recursive(
|
|
124
152
|
session,
|
|
125
|
-
pattern,
|
|
153
|
+
source.pattern,
|
|
126
154
|
at_files,
|
|
127
155
|
collected_images,
|
|
128
156
|
visited,
|
|
157
|
+
mentioned_in=source.mentioned_in,
|
|
129
158
|
)
|
|
130
159
|
|
|
131
160
|
if len(at_files) == 0:
|
|
@@ -231,9 +260,9 @@ async def file_changed_externally_reminder(
|
|
|
231
260
|
changed_files: list[tuple[str, str, list[model.ImageURLPart] | None]] = []
|
|
232
261
|
collected_images: list[model.ImageURLPart] = []
|
|
233
262
|
if session.file_tracker and len(session.file_tracker) > 0:
|
|
234
|
-
for path,
|
|
263
|
+
for path, status in session.file_tracker.items():
|
|
235
264
|
try:
|
|
236
|
-
if Path(path).stat().st_mtime > mtime:
|
|
265
|
+
if Path(path).stat().st_mtime > status.mtime:
|
|
237
266
|
context_token = set_tool_context_from_session(session)
|
|
238
267
|
try:
|
|
239
268
|
tool_result = await ReadTool.call_with_args(
|
|
@@ -282,6 +311,7 @@ def get_memory_paths() -> list[tuple[Path, str]]:
|
|
|
282
311
|
),
|
|
283
312
|
(Path.cwd() / "AGENTS.md", "project instructions, checked into the codebase"),
|
|
284
313
|
(Path.cwd() / "CLAUDE.md", "project instructions, checked into the codebase"),
|
|
314
|
+
(Path.cwd() / ".claude" / "CLAUDE.md", "project instructions, checked into the codebase"),
|
|
285
315
|
]
|
|
286
316
|
|
|
287
317
|
|
|
@@ -313,16 +343,32 @@ async def image_reminder(session: Session) -> model.DeveloperMessageItem | None:
|
|
|
313
343
|
)
|
|
314
344
|
|
|
315
345
|
|
|
346
|
+
def _is_memory_loaded(session: Session, path: str) -> bool:
|
|
347
|
+
"""Check if a memory file has already been loaded (tracked with is_memory=True)."""
|
|
348
|
+
status = session.file_tracker.get(path)
|
|
349
|
+
return status is not None and status.is_memory
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _mark_memory_loaded(session: Session, path: str) -> None:
|
|
353
|
+
"""Mark a file as loaded memory in file_tracker."""
|
|
354
|
+
try:
|
|
355
|
+
mtime = Path(path).stat().st_mtime
|
|
356
|
+
except (OSError, FileNotFoundError):
|
|
357
|
+
mtime = 0.0
|
|
358
|
+
session.file_tracker[path] = model.FileStatus(mtime=mtime, is_memory=True)
|
|
359
|
+
|
|
360
|
+
|
|
316
361
|
async def memory_reminder(session: Session) -> model.DeveloperMessageItem | None:
|
|
317
362
|
"""CLAUDE.md AGENTS.md"""
|
|
318
363
|
memory_paths = get_memory_paths()
|
|
319
364
|
memories: list[Memory] = []
|
|
320
365
|
for memory_path, instruction in memory_paths:
|
|
321
|
-
|
|
366
|
+
path_str = str(memory_path)
|
|
367
|
+
if memory_path.exists() and memory_path.is_file() and not _is_memory_loaded(session, path_str):
|
|
322
368
|
try:
|
|
323
369
|
text = memory_path.read_text()
|
|
324
|
-
session
|
|
325
|
-
memories.append(Memory(path=
|
|
370
|
+
_mark_memory_loaded(session, path_str)
|
|
371
|
+
memories.append(Memory(path=path_str, instruction=instruction, content=text))
|
|
326
372
|
except (PermissionError, UnicodeDecodeError, OSError):
|
|
327
373
|
continue
|
|
328
374
|
if len(memories) > 0:
|
|
@@ -358,7 +404,7 @@ async def last_path_memory_reminder(
|
|
|
358
404
|
"""Load CLAUDE.md/AGENTS.md from directories containing files in file_tracker.
|
|
359
405
|
|
|
360
406
|
Uses session.file_tracker to detect accessed paths (works for both tool calls
|
|
361
|
-
and @ file references).
|
|
407
|
+
and @ file references). Checks is_memory flag to avoid duplicate loading.
|
|
362
408
|
"""
|
|
363
409
|
if not session.file_tracker:
|
|
364
410
|
return None
|
|
@@ -367,7 +413,6 @@ async def last_path_memory_reminder(
|
|
|
367
413
|
memories: list[Memory] = []
|
|
368
414
|
|
|
369
415
|
cwd = Path.cwd().resolve()
|
|
370
|
-
loaded_set: set[str] = set(session.loaded_memory)
|
|
371
416
|
seen_memory_files: set[str] = set()
|
|
372
417
|
|
|
373
418
|
for p_str in paths:
|
|
@@ -395,15 +440,14 @@ async def last_path_memory_reminder(
|
|
|
395
440
|
for fname in MEMORY_FILE_NAMES:
|
|
396
441
|
mem_path = current_dir / fname
|
|
397
442
|
mem_path_str = str(mem_path)
|
|
398
|
-
if mem_path_str in seen_memory_files or mem_path_str
|
|
443
|
+
if mem_path_str in seen_memory_files or _is_memory_loaded(session, mem_path_str):
|
|
399
444
|
continue
|
|
400
445
|
if mem_path.exists() and mem_path.is_file():
|
|
401
446
|
try:
|
|
402
447
|
text = mem_path.read_text()
|
|
403
448
|
except (PermissionError, UnicodeDecodeError, OSError):
|
|
404
449
|
continue
|
|
405
|
-
session
|
|
406
|
-
loaded_set.add(mem_path_str)
|
|
450
|
+
_mark_memory_loaded(session, mem_path_str)
|
|
407
451
|
seen_memory_files.add(mem_path_str)
|
|
408
452
|
memories.append(
|
|
409
453
|
Memory(
|