klaude-code 1.9.0__py3-none-any.whl → 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- klaude_code/auth/base.py +2 -6
- klaude_code/cli/auth_cmd.py +4 -4
- klaude_code/cli/list_model.py +1 -1
- klaude_code/cli/main.py +1 -1
- klaude_code/cli/runtime.py +7 -5
- klaude_code/cli/self_update.py +1 -1
- klaude_code/cli/session_cmd.py +1 -1
- klaude_code/command/clear_cmd.py +6 -2
- klaude_code/command/command_abc.py +2 -2
- klaude_code/command/debug_cmd.py +4 -4
- klaude_code/command/export_cmd.py +2 -2
- klaude_code/command/export_online_cmd.py +12 -12
- klaude_code/command/fork_session_cmd.py +29 -23
- klaude_code/command/help_cmd.py +4 -4
- klaude_code/command/model_cmd.py +4 -4
- klaude_code/command/model_select.py +1 -1
- klaude_code/command/prompt-commit.md +11 -2
- klaude_code/command/prompt_command.py +3 -3
- klaude_code/command/refresh_cmd.py +2 -2
- klaude_code/command/registry.py +7 -5
- klaude_code/command/release_notes_cmd.py +4 -4
- klaude_code/command/resume_cmd.py +15 -11
- klaude_code/command/status_cmd.py +4 -4
- klaude_code/command/terminal_setup_cmd.py +8 -8
- klaude_code/command/thinking_cmd.py +4 -4
- klaude_code/config/assets/builtin_config.yaml +16 -0
- klaude_code/config/builtin_config.py +16 -5
- klaude_code/config/config.py +7 -2
- klaude_code/const.py +146 -91
- klaude_code/core/agent.py +3 -12
- klaude_code/core/executor.py +21 -13
- klaude_code/core/manager/sub_agent_manager.py +71 -7
- klaude_code/core/prompts/prompt-sub-agent-image-gen.md +1 -0
- klaude_code/core/prompts/prompt-sub-agent-web.md +27 -1
- klaude_code/core/reminders.py +88 -69
- klaude_code/core/task.py +44 -45
- klaude_code/core/tool/file/apply_patch_tool.py +9 -9
- klaude_code/core/tool/file/diff_builder.py +3 -5
- klaude_code/core/tool/file/edit_tool.py +23 -23
- klaude_code/core/tool/file/move_tool.py +43 -43
- klaude_code/core/tool/file/read_tool.py +44 -39
- klaude_code/core/tool/file/write_tool.py +14 -14
- klaude_code/core/tool/report_back_tool.py +4 -4
- klaude_code/core/tool/shell/bash_tool.py +23 -23
- klaude_code/core/tool/skill/skill_tool.py +7 -7
- klaude_code/core/tool/sub_agent_tool.py +38 -9
- klaude_code/core/tool/todo/todo_write_tool.py +8 -8
- klaude_code/core/tool/todo/update_plan_tool.py +6 -6
- klaude_code/core/tool/tool_abc.py +2 -2
- klaude_code/core/tool/tool_context.py +27 -0
- klaude_code/core/tool/tool_runner.py +88 -42
- klaude_code/core/tool/truncation.py +38 -20
- klaude_code/core/tool/web/mermaid_tool.py +6 -7
- klaude_code/core/tool/web/web_fetch_tool.py +68 -30
- klaude_code/core/tool/web/web_search_tool.py +15 -17
- klaude_code/core/turn.py +120 -73
- klaude_code/llm/anthropic/client.py +79 -44
- klaude_code/llm/anthropic/input.py +116 -108
- klaude_code/llm/bedrock/client.py +8 -5
- klaude_code/llm/claude/client.py +18 -8
- klaude_code/llm/client.py +4 -3
- klaude_code/llm/codex/client.py +15 -9
- klaude_code/llm/google/client.py +122 -60
- klaude_code/llm/google/input.py +94 -108
- klaude_code/llm/image.py +123 -0
- klaude_code/llm/input_common.py +136 -189
- klaude_code/llm/openai_compatible/client.py +17 -7
- klaude_code/llm/openai_compatible/input.py +36 -66
- klaude_code/llm/openai_compatible/stream.py +119 -67
- klaude_code/llm/openai_compatible/tool_call_accumulator.py +23 -11
- klaude_code/llm/openrouter/client.py +34 -9
- klaude_code/llm/openrouter/input.py +63 -64
- klaude_code/llm/openrouter/reasoning.py +22 -24
- klaude_code/llm/registry.py +20 -17
- klaude_code/llm/responses/client.py +107 -45
- klaude_code/llm/responses/input.py +115 -98
- klaude_code/llm/usage.py +52 -25
- klaude_code/protocol/__init__.py +1 -0
- klaude_code/protocol/events.py +16 -12
- klaude_code/protocol/llm_param.py +20 -2
- klaude_code/protocol/message.py +250 -0
- klaude_code/protocol/model.py +94 -281
- klaude_code/protocol/op.py +2 -2
- klaude_code/protocol/sub_agent/__init__.py +1 -0
- klaude_code/protocol/sub_agent/explore.py +10 -0
- klaude_code/protocol/sub_agent/image_gen.py +119 -0
- klaude_code/protocol/sub_agent/task.py +10 -0
- klaude_code/protocol/sub_agent/web.py +10 -0
- klaude_code/session/codec.py +6 -6
- klaude_code/session/export.py +261 -62
- klaude_code/session/selector.py +7 -24
- klaude_code/session/session.py +126 -54
- klaude_code/session/store.py +5 -32
- klaude_code/session/templates/export_session.html +1 -1
- klaude_code/session/templates/mermaid_viewer.html +1 -1
- klaude_code/trace/log.py +11 -6
- klaude_code/ui/core/input.py +1 -1
- klaude_code/ui/core/stage_manager.py +1 -8
- klaude_code/ui/modes/debug/display.py +2 -2
- klaude_code/ui/modes/repl/clipboard.py +2 -2
- klaude_code/ui/modes/repl/completers.py +18 -10
- klaude_code/ui/modes/repl/event_handler.py +136 -127
- klaude_code/ui/modes/repl/input_prompt_toolkit.py +1 -1
- klaude_code/ui/modes/repl/key_bindings.py +1 -1
- klaude_code/ui/modes/repl/renderer.py +107 -15
- klaude_code/ui/renderers/assistant.py +2 -2
- klaude_code/ui/renderers/common.py +65 -7
- klaude_code/ui/renderers/developer.py +7 -6
- klaude_code/ui/renderers/diffs.py +11 -11
- klaude_code/ui/renderers/mermaid_viewer.py +49 -2
- klaude_code/ui/renderers/metadata.py +33 -5
- klaude_code/ui/renderers/sub_agent.py +57 -16
- klaude_code/ui/renderers/thinking.py +37 -2
- klaude_code/ui/renderers/tools.py +180 -165
- klaude_code/ui/rich/live.py +3 -1
- klaude_code/ui/rich/markdown.py +39 -7
- klaude_code/ui/rich/quote.py +76 -1
- klaude_code/ui/rich/status.py +14 -8
- klaude_code/ui/rich/theme.py +8 -2
- klaude_code/ui/terminal/image.py +34 -0
- klaude_code/ui/terminal/notifier.py +2 -1
- klaude_code/ui/terminal/progress_bar.py +4 -4
- klaude_code/ui/terminal/selector.py +22 -4
- klaude_code/ui/utils/common.py +11 -2
- {klaude_code-1.9.0.dist-info → klaude_code-2.0.0.dist-info}/METADATA +4 -2
- klaude_code-2.0.0.dist-info/RECORD +229 -0
- klaude_code-1.9.0.dist-info/RECORD +0 -224
- {klaude_code-1.9.0.dist-info → klaude_code-2.0.0.dist-info}/WHEEL +0 -0
- {klaude_code-1.9.0.dist-info → klaude_code-2.0.0.dist-info}/entry_points.txt +0 -0
klaude_code/core/task.py
CHANGED
|
@@ -6,11 +6,11 @@ from collections.abc import AsyncGenerator, Callable, Sequence
|
|
|
6
6
|
from dataclasses import dataclass
|
|
7
7
|
from typing import TYPE_CHECKING
|
|
8
8
|
|
|
9
|
-
from klaude_code import
|
|
9
|
+
from klaude_code.const import INITIAL_RETRY_DELAY_S, MAX_FAILED_TURN_RETRIES, MAX_RETRY_DELAY_S
|
|
10
10
|
from klaude_code.core.reminders import Reminder
|
|
11
11
|
from klaude_code.core.tool import FileTracker, TodoContext, ToolABC
|
|
12
12
|
from klaude_code.core.turn import TurnError, TurnExecutionContext, TurnExecutor
|
|
13
|
-
from klaude_code.protocol import events, model
|
|
13
|
+
from klaude_code.protocol import events, message, model
|
|
14
14
|
from klaude_code.trace import DebugType, log_debug
|
|
15
15
|
|
|
16
16
|
if TYPE_CHECKING:
|
|
@@ -33,38 +33,37 @@ class MetadataAccumulator:
|
|
|
33
33
|
self._first_token_latency_count: int = 0
|
|
34
34
|
self._turn_count: int = 0
|
|
35
35
|
|
|
36
|
-
def add(self,
|
|
37
|
-
"""Merge a turn's
|
|
36
|
+
def add(self, turn_usage: model.Usage) -> None:
|
|
37
|
+
"""Merge a turn's usage into the accumulated state."""
|
|
38
38
|
self._turn_count += 1
|
|
39
|
-
usage =
|
|
39
|
+
usage = turn_usage
|
|
40
40
|
|
|
41
|
-
if usage is
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
acc_usage = self._main_agent.usage
|
|
41
|
+
if self._main_agent.usage is None:
|
|
42
|
+
self._main_agent.usage = model.Usage()
|
|
43
|
+
acc_usage = self._main_agent.usage
|
|
45
44
|
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
model.TaskMetadata.merge_usage(acc_usage, usage)
|
|
46
|
+
acc_usage.currency = usage.currency
|
|
48
47
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
if usage.context_size is not None:
|
|
49
|
+
acc_usage.context_size = usage.context_size
|
|
50
|
+
if usage.context_limit is not None:
|
|
51
|
+
acc_usage.context_limit = usage.context_limit
|
|
53
52
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
if usage.first_token_latency_ms is not None:
|
|
54
|
+
self._first_token_latency_sum += usage.first_token_latency_ms
|
|
55
|
+
self._first_token_latency_count += 1
|
|
57
56
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
57
|
+
if usage.throughput_tps is not None:
|
|
58
|
+
current_output = usage.output_tokens
|
|
59
|
+
if current_output > 0:
|
|
60
|
+
self._throughput_weighted_sum += usage.throughput_tps * current_output
|
|
61
|
+
self._throughput_tracked_tokens += current_output
|
|
63
62
|
|
|
64
|
-
if
|
|
65
|
-
self._main_agent.provider =
|
|
66
|
-
if
|
|
67
|
-
self._main_agent.model_name =
|
|
63
|
+
if usage.provider is not None:
|
|
64
|
+
self._main_agent.provider = usage.provider
|
|
65
|
+
if usage.model_name:
|
|
66
|
+
self._main_agent.model_name = usage.model_name
|
|
68
67
|
|
|
69
68
|
def add_sub_agent_metadata(self, sub_agent_metadata: model.TaskMetadata) -> None:
|
|
70
69
|
"""Add sub-agent task metadata to the accumulated state."""
|
|
@@ -98,8 +97,8 @@ class SessionContext:
|
|
|
98
97
|
"""
|
|
99
98
|
|
|
100
99
|
session_id: str
|
|
101
|
-
get_conversation_history: Callable[[], list[
|
|
102
|
-
append_history: Callable[[Sequence[
|
|
100
|
+
get_conversation_history: Callable[[], list[message.HistoryEvent]]
|
|
101
|
+
append_history: Callable[[Sequence[message.HistoryEvent]], None]
|
|
103
102
|
file_tracker: FileTracker
|
|
104
103
|
todo_context: TodoContext
|
|
105
104
|
|
|
@@ -150,7 +149,7 @@ class TaskExecutor:
|
|
|
150
149
|
|
|
151
150
|
return ui_events
|
|
152
151
|
|
|
153
|
-
async def run(self, user_input:
|
|
152
|
+
async def run(self, user_input: message.UserInputPayload) -> AsyncGenerator[events.Event]:
|
|
154
153
|
"""Execute the task, yielding events as they occur."""
|
|
155
154
|
ctx = self._context
|
|
156
155
|
session_ctx = ctx.session_ctx
|
|
@@ -178,13 +177,14 @@ class TaskExecutor:
|
|
|
178
177
|
system_prompt=profile.system_prompt,
|
|
179
178
|
tools=profile.tools,
|
|
180
179
|
tool_registry=ctx.tool_registry,
|
|
180
|
+
sub_agent_state=ctx.sub_agent_state,
|
|
181
181
|
)
|
|
182
182
|
|
|
183
183
|
turn: TurnExecutor | None = None
|
|
184
184
|
turn_succeeded = False
|
|
185
185
|
last_error_message: str | None = None
|
|
186
186
|
|
|
187
|
-
for attempt in range(
|
|
187
|
+
for attempt in range(MAX_FAILED_TURN_RETRIES + 1):
|
|
188
188
|
turn = TurnExecutor(turn_context)
|
|
189
189
|
self._current_turn = turn
|
|
190
190
|
|
|
@@ -196,13 +196,12 @@ class TaskExecutor:
|
|
|
196
196
|
case events.ResponseMetadataEvent() as e:
|
|
197
197
|
metadata_accumulator.add(e.metadata)
|
|
198
198
|
# Emit context usage event if available
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
)
|
|
199
|
+
context_percent = e.metadata.context_usage_percent
|
|
200
|
+
if context_percent is not None:
|
|
201
|
+
yield events.ContextUsageEvent(
|
|
202
|
+
session_id=session_ctx.session_id,
|
|
203
|
+
context_percent=context_percent,
|
|
204
|
+
)
|
|
206
205
|
case events.ToolResultEvent() as e:
|
|
207
206
|
# Collect sub-agent task metadata from tool results
|
|
208
207
|
if e.task_metadata is not None:
|
|
@@ -215,9 +214,9 @@ class TaskExecutor:
|
|
|
215
214
|
break
|
|
216
215
|
except TurnError as e:
|
|
217
216
|
last_error_message = str(e)
|
|
218
|
-
if attempt <
|
|
217
|
+
if attempt < MAX_FAILED_TURN_RETRIES:
|
|
219
218
|
delay = _retry_delay_seconds(attempt + 1)
|
|
220
|
-
error_msg = f"Retrying {attempt + 1}/{
|
|
219
|
+
error_msg = f"Retrying {attempt + 1}/{MAX_FAILED_TURN_RETRIES} in {delay:.1f}s"
|
|
221
220
|
if last_error_message:
|
|
222
221
|
error_msg = f"{error_msg} - {last_error_message}"
|
|
223
222
|
yield events.ErrorEvent(
|
|
@@ -233,7 +232,7 @@ class TaskExecutor:
|
|
|
233
232
|
style="red",
|
|
234
233
|
debug_type=DebugType.EXECUTION,
|
|
235
234
|
)
|
|
236
|
-
final_error = f"Turn failed after {
|
|
235
|
+
final_error = f"Turn failed after {MAX_FAILED_TURN_RETRIES} retries."
|
|
237
236
|
if last_error_message:
|
|
238
237
|
final_error = f"{last_error_message}\n{final_error}"
|
|
239
238
|
yield events.ErrorEvent(error_message=final_error, can_retry=False, session_id=session_ctx.session_id)
|
|
@@ -243,9 +242,9 @@ class TaskExecutor:
|
|
|
243
242
|
# Empty result should retry instead of finishing
|
|
244
243
|
if turn is not None and not turn.task_result.strip():
|
|
245
244
|
if ctx.sub_agent_state is not None:
|
|
246
|
-
error_msg = "Sub-agent returned empty result, retrying
|
|
245
|
+
error_msg = "Sub-agent returned empty result, retrying…"
|
|
247
246
|
else:
|
|
248
|
-
error_msg = "Agent returned empty result, retrying
|
|
247
|
+
error_msg = "Agent returned empty result, retrying…"
|
|
249
248
|
yield events.ErrorEvent(error_message=error_msg, can_retry=True, session_id=session_ctx.session_id)
|
|
250
249
|
continue
|
|
251
250
|
break
|
|
@@ -271,5 +270,5 @@ class TaskExecutor:
|
|
|
271
270
|
def _retry_delay_seconds(attempt: int) -> float:
|
|
272
271
|
"""Compute exponential backoff delay for the given attempt count."""
|
|
273
272
|
capped_attempt = max(1, attempt)
|
|
274
|
-
delay =
|
|
275
|
-
return min(delay,
|
|
273
|
+
delay = INITIAL_RETRY_DELAY_S * (2 ** (capped_attempt - 1))
|
|
274
|
+
return min(delay, MAX_RETRY_DELAY_S)
|
|
@@ -13,21 +13,21 @@ from klaude_code.core.tool.file.diff_builder import build_structured_file_diff
|
|
|
13
13
|
from klaude_code.core.tool.tool_abc import ToolABC, load_desc
|
|
14
14
|
from klaude_code.core.tool.tool_context import get_current_file_tracker
|
|
15
15
|
from klaude_code.core.tool.tool_registry import register
|
|
16
|
-
from klaude_code.protocol import llm_param, model, tools
|
|
16
|
+
from klaude_code.protocol import llm_param, message, model, tools
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
class ApplyPatchHandler:
|
|
20
20
|
@classmethod
|
|
21
|
-
async def handle_apply_patch(cls, patch_text: str) ->
|
|
21
|
+
async def handle_apply_patch(cls, patch_text: str) -> message.ToolResultMessage:
|
|
22
22
|
try:
|
|
23
23
|
output, ui_extra = await asyncio.to_thread(cls._apply_patch_in_thread, patch_text)
|
|
24
24
|
except apply_patch_module.DiffError as error:
|
|
25
|
-
return
|
|
25
|
+
return message.ToolResultMessage(status="error", output_text=str(error))
|
|
26
26
|
except Exception as error: # pragma: no cover # unexpected errors bubbled to tool result
|
|
27
|
-
return
|
|
28
|
-
return
|
|
27
|
+
return message.ToolResultMessage(status="error", output_text=f"Execution error: {error}")
|
|
28
|
+
return message.ToolResultMessage(
|
|
29
29
|
status="success",
|
|
30
|
-
|
|
30
|
+
output_text=output,
|
|
31
31
|
ui_extra=ui_extra,
|
|
32
32
|
)
|
|
33
33
|
|
|
@@ -172,13 +172,13 @@ class ApplyPatchTool(ToolABC):
|
|
|
172
172
|
)
|
|
173
173
|
|
|
174
174
|
@classmethod
|
|
175
|
-
async def call(cls, arguments: str) ->
|
|
175
|
+
async def call(cls, arguments: str) -> message.ToolResultMessage:
|
|
176
176
|
try:
|
|
177
177
|
args = cls.ApplyPatchArguments.model_validate_json(arguments)
|
|
178
178
|
except ValueError as exc:
|
|
179
|
-
return
|
|
179
|
+
return message.ToolResultMessage(status="error", output_text=f"Invalid arguments: {exc}")
|
|
180
180
|
return await cls.call_with_args(args)
|
|
181
181
|
|
|
182
182
|
@classmethod
|
|
183
|
-
async def call_with_args(cls, args: ApplyPatchArguments) ->
|
|
183
|
+
async def call_with_args(cls, args: ApplyPatchArguments) -> message.ToolResultMessage:
|
|
184
184
|
return await ApplyPatchHandler.handle_apply_patch(args.patch)
|
|
@@ -5,11 +5,9 @@ from typing import cast
|
|
|
5
5
|
|
|
6
6
|
from diff_match_patch import diff_match_patch # type: ignore[import-untyped]
|
|
7
7
|
|
|
8
|
+
from klaude_code.const import DIFF_DEFAULT_CONTEXT_LINES, DIFF_MAX_LINE_LENGTH_FOR_CHAR_DIFF
|
|
8
9
|
from klaude_code.protocol import model
|
|
9
10
|
|
|
10
|
-
_MAX_LINE_LENGTH_FOR_CHAR_DIFF = 2000
|
|
11
|
-
_DEFAULT_CONTEXT_LINES = 3
|
|
12
|
-
|
|
13
11
|
|
|
14
12
|
def build_structured_diff(before: str, after: str, *, file_path: str) -> model.DiffUIExtra:
|
|
15
13
|
"""Build a structured diff with char-level spans for a single file."""
|
|
@@ -31,7 +29,7 @@ def _build_file_diff(before: str, after: str, *, file_path: str) -> model.DiffFi
|
|
|
31
29
|
stats_add = 0
|
|
32
30
|
stats_remove = 0
|
|
33
31
|
|
|
34
|
-
grouped_opcodes = matcher.get_grouped_opcodes(n=
|
|
32
|
+
grouped_opcodes = matcher.get_grouped_opcodes(n=DIFF_DEFAULT_CONTEXT_LINES)
|
|
35
33
|
for group_idx, group in enumerate(grouped_opcodes):
|
|
36
34
|
if group_idx > 0:
|
|
37
35
|
lines.append(_gap_line())
|
|
@@ -148,4 +146,4 @@ def _diff_line_spans(old_line: str, new_line: str) -> tuple[list[model.DiffSpan]
|
|
|
148
146
|
|
|
149
147
|
|
|
150
148
|
def _should_char_diff(old_line: str, new_line: str) -> bool:
|
|
151
|
-
return len(old_line) <=
|
|
149
|
+
return len(old_line) <= DIFF_MAX_LINE_LENGTH_FOR_CHAR_DIFF and len(new_line) <= DIFF_MAX_LINE_LENGTH_FOR_CHAR_DIFF
|
|
@@ -13,7 +13,7 @@ from klaude_code.core.tool.file.diff_builder import build_structured_diff
|
|
|
13
13
|
from klaude_code.core.tool.tool_abc import ToolABC, load_desc
|
|
14
14
|
from klaude_code.core.tool.tool_context import get_current_file_tracker
|
|
15
15
|
from klaude_code.core.tool.tool_registry import register
|
|
16
|
-
from klaude_code.protocol import llm_param, model, tools
|
|
16
|
+
from klaude_code.protocol import llm_param, message, model, tools
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
@register(tools.EDIT)
|
|
@@ -85,25 +85,25 @@ class EditTool(ToolABC):
|
|
|
85
85
|
return content.replace(old_string, new_string, 1)
|
|
86
86
|
|
|
87
87
|
@classmethod
|
|
88
|
-
async def call(cls, arguments: str) ->
|
|
88
|
+
async def call(cls, arguments: str) -> message.ToolResultMessage:
|
|
89
89
|
try:
|
|
90
90
|
args = EditTool.EditArguments.model_validate_json(arguments)
|
|
91
91
|
except ValueError as e: # pragma: no cover - defensive
|
|
92
|
-
return
|
|
92
|
+
return message.ToolResultMessage(status="error", output_text=f"Invalid arguments: {e}")
|
|
93
93
|
|
|
94
94
|
file_path = os.path.abspath(args.file_path)
|
|
95
95
|
|
|
96
96
|
# Common file errors
|
|
97
97
|
if is_directory(file_path):
|
|
98
|
-
return
|
|
98
|
+
return message.ToolResultMessage(
|
|
99
99
|
status="error",
|
|
100
|
-
|
|
100
|
+
output_text="<tool_use_error>Illegal operation on a directory. edit</tool_use_error>",
|
|
101
101
|
)
|
|
102
102
|
|
|
103
103
|
if args.old_string == "":
|
|
104
|
-
return
|
|
104
|
+
return message.ToolResultMessage(
|
|
105
105
|
status="error",
|
|
106
|
-
|
|
106
|
+
output_text=(
|
|
107
107
|
"<tool_use_error>old_string must not be empty for Edit. "
|
|
108
108
|
"To create or overwrite a file, use the Write tool instead.</tool_use_error>"
|
|
109
109
|
),
|
|
@@ -114,25 +114,25 @@ class EditTool(ToolABC):
|
|
|
114
114
|
tracked_status: model.FileStatus | None = None
|
|
115
115
|
if not file_exists(file_path):
|
|
116
116
|
# We require reading before editing
|
|
117
|
-
return
|
|
117
|
+
return message.ToolResultMessage(
|
|
118
118
|
status="error",
|
|
119
|
-
|
|
119
|
+
output_text=("File has not been read yet. Read it first before writing to it."),
|
|
120
120
|
)
|
|
121
121
|
if file_tracker is not None:
|
|
122
122
|
tracked_status = file_tracker.get(file_path)
|
|
123
123
|
if tracked_status is None:
|
|
124
|
-
return
|
|
124
|
+
return message.ToolResultMessage(
|
|
125
125
|
status="error",
|
|
126
|
-
|
|
126
|
+
output_text=("File has not been read yet. Read it first before writing to it."),
|
|
127
127
|
)
|
|
128
128
|
|
|
129
129
|
# Edit existing file: validate and apply
|
|
130
130
|
try:
|
|
131
131
|
before = await asyncio.to_thread(read_text, file_path)
|
|
132
132
|
except FileNotFoundError:
|
|
133
|
-
return
|
|
133
|
+
return message.ToolResultMessage(
|
|
134
134
|
status="error",
|
|
135
|
-
|
|
135
|
+
output_text="File has not been read yet. Read it first before writing to it.",
|
|
136
136
|
)
|
|
137
137
|
|
|
138
138
|
# Re-check external modifications using content hash when available.
|
|
@@ -140,9 +140,9 @@ class EditTool(ToolABC):
|
|
|
140
140
|
if tracked_status.content_sha256 is not None:
|
|
141
141
|
current_sha256 = hash_text_sha256(before)
|
|
142
142
|
if current_sha256 != tracked_status.content_sha256:
|
|
143
|
-
return
|
|
143
|
+
return message.ToolResultMessage(
|
|
144
144
|
status="error",
|
|
145
|
-
|
|
145
|
+
output_text=(
|
|
146
146
|
"File has been modified externally. Either by user or a linter. Read it first before writing to it."
|
|
147
147
|
),
|
|
148
148
|
)
|
|
@@ -153,9 +153,9 @@ class EditTool(ToolABC):
|
|
|
153
153
|
except OSError:
|
|
154
154
|
current_mtime = tracked_status.mtime
|
|
155
155
|
if current_mtime != tracked_status.mtime:
|
|
156
|
-
return
|
|
156
|
+
return message.ToolResultMessage(
|
|
157
157
|
status="error",
|
|
158
|
-
|
|
158
|
+
output_text=(
|
|
159
159
|
"File has been modified externally. Either by user or a linter. Read it first before writing to it."
|
|
160
160
|
),
|
|
161
161
|
)
|
|
@@ -167,7 +167,7 @@ class EditTool(ToolABC):
|
|
|
167
167
|
replace_all=args.replace_all,
|
|
168
168
|
)
|
|
169
169
|
if err is not None:
|
|
170
|
-
return
|
|
170
|
+
return message.ToolResultMessage(status="error", output_text=err)
|
|
171
171
|
|
|
172
172
|
after = cls.execute(
|
|
173
173
|
content=before,
|
|
@@ -178,9 +178,9 @@ class EditTool(ToolABC):
|
|
|
178
178
|
|
|
179
179
|
# If nothing changed due to replacement semantics (should not happen after valid), guard anyway
|
|
180
180
|
if before == after:
|
|
181
|
-
return
|
|
181
|
+
return message.ToolResultMessage(
|
|
182
182
|
status="error",
|
|
183
|
-
|
|
183
|
+
output_text=(
|
|
184
184
|
"<tool_use_error>No changes to make: old_string and new_string are exactly the same.</tool_use_error>"
|
|
185
185
|
),
|
|
186
186
|
)
|
|
@@ -189,7 +189,7 @@ class EditTool(ToolABC):
|
|
|
189
189
|
try:
|
|
190
190
|
await asyncio.to_thread(write_text, file_path, after)
|
|
191
191
|
except (OSError, UnicodeError) as e: # pragma: no cover
|
|
192
|
-
return
|
|
192
|
+
return message.ToolResultMessage(status="error", output_text=f"<tool_use_error>{e}</tool_use_error>")
|
|
193
193
|
|
|
194
194
|
# Prepare UI extra: unified diff with 3 context lines
|
|
195
195
|
diff_lines = list(
|
|
@@ -217,7 +217,7 @@ class EditTool(ToolABC):
|
|
|
217
217
|
# Build output message
|
|
218
218
|
if args.replace_all:
|
|
219
219
|
msg = f"The file {file_path} has been updated. All occurrences of '{args.old_string}' were successfully replaced with '{args.new_string}'."
|
|
220
|
-
return
|
|
220
|
+
return message.ToolResultMessage(status="success", output_text=msg, ui_extra=ui_extra)
|
|
221
221
|
|
|
222
222
|
# For single replacement, show a snippet consisting of context + added lines only
|
|
223
223
|
# Parse the diff to collect target line numbers in the 'after' file
|
|
@@ -258,4 +258,4 @@ class EditTool(ToolABC):
|
|
|
258
258
|
f"The file {file_path} has been updated. Here's the result of running `cat -n` on a snippet of the edited file:\n"
|
|
259
259
|
f"{snippet}"
|
|
260
260
|
)
|
|
261
|
-
return
|
|
261
|
+
return message.ToolResultMessage(status="success", output_text=output, ui_extra=ui_extra)
|
|
@@ -12,7 +12,7 @@ from klaude_code.core.tool.file.diff_builder import build_structured_diff
|
|
|
12
12
|
from klaude_code.core.tool.tool_abc import ToolABC, load_desc
|
|
13
13
|
from klaude_code.core.tool.tool_context import get_current_file_tracker
|
|
14
14
|
from klaude_code.core.tool.tool_registry import register
|
|
15
|
-
from klaude_code.protocol import llm_param, model, tools
|
|
15
|
+
from klaude_code.protocol import llm_param, message, model, tools
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
class MoveArguments(BaseModel):
|
|
@@ -156,11 +156,11 @@ class MoveTool(ToolABC):
|
|
|
156
156
|
)
|
|
157
157
|
|
|
158
158
|
@classmethod
|
|
159
|
-
async def call(cls, arguments: str) ->
|
|
159
|
+
async def call(cls, arguments: str) -> message.ToolResultMessage:
|
|
160
160
|
try:
|
|
161
161
|
args = MoveArguments.model_validate_json(arguments)
|
|
162
162
|
except ValueError as e:
|
|
163
|
-
return
|
|
163
|
+
return message.ToolResultMessage(status="error", output_text=f"Invalid arguments: {e}")
|
|
164
164
|
|
|
165
165
|
source_path = os.path.abspath(args.source_file_path)
|
|
166
166
|
target_path = os.path.abspath(args.target_file_path)
|
|
@@ -168,21 +168,21 @@ class MoveTool(ToolABC):
|
|
|
168
168
|
|
|
169
169
|
# Validate paths
|
|
170
170
|
if is_directory(source_path):
|
|
171
|
-
return
|
|
171
|
+
return message.ToolResultMessage(
|
|
172
172
|
status="error",
|
|
173
|
-
|
|
173
|
+
output_text="<tool_use_error>Source path is a directory, not a file.</tool_use_error>",
|
|
174
174
|
)
|
|
175
175
|
if is_directory(target_path):
|
|
176
|
-
return
|
|
176
|
+
return message.ToolResultMessage(
|
|
177
177
|
status="error",
|
|
178
|
-
|
|
178
|
+
output_text="<tool_use_error>Target path is a directory, not a file.</tool_use_error>",
|
|
179
179
|
)
|
|
180
180
|
|
|
181
181
|
# Validate line range
|
|
182
182
|
if args.start_line > args.end_line:
|
|
183
|
-
return
|
|
183
|
+
return message.ToolResultMessage(
|
|
184
184
|
status="error",
|
|
185
|
-
|
|
185
|
+
output_text="<tool_use_error>start_line must be <= end_line.</tool_use_error>",
|
|
186
186
|
)
|
|
187
187
|
|
|
188
188
|
# Check file tracker
|
|
@@ -191,9 +191,9 @@ class MoveTool(ToolABC):
|
|
|
191
191
|
target_exists = file_exists(target_path)
|
|
192
192
|
|
|
193
193
|
if not source_exists:
|
|
194
|
-
return
|
|
194
|
+
return message.ToolResultMessage(
|
|
195
195
|
status="error",
|
|
196
|
-
|
|
196
|
+
output_text="<tool_use_error>Source file does not exist.</tool_use_error>",
|
|
197
197
|
)
|
|
198
198
|
|
|
199
199
|
source_status: model.FileStatus | None = None
|
|
@@ -202,47 +202,47 @@ class MoveTool(ToolABC):
|
|
|
202
202
|
if file_tracker is not None:
|
|
203
203
|
source_status = file_tracker.get(source_path)
|
|
204
204
|
if source_status is None:
|
|
205
|
-
return
|
|
205
|
+
return message.ToolResultMessage(
|
|
206
206
|
status="error",
|
|
207
|
-
|
|
207
|
+
output_text="Source file has not been read yet. Read it first.",
|
|
208
208
|
)
|
|
209
209
|
if target_exists:
|
|
210
210
|
target_status = file_tracker.get(target_path)
|
|
211
211
|
if target_status is None:
|
|
212
|
-
return
|
|
212
|
+
return message.ToolResultMessage(
|
|
213
213
|
status="error",
|
|
214
|
-
|
|
214
|
+
output_text="Target file has not been read yet. Read it first before writing to it.",
|
|
215
215
|
)
|
|
216
216
|
|
|
217
217
|
# Read source file
|
|
218
218
|
try:
|
|
219
219
|
source_content = await asyncio.to_thread(read_text, source_path)
|
|
220
220
|
except OSError as e:
|
|
221
|
-
return
|
|
222
|
-
status="error",
|
|
221
|
+
return message.ToolResultMessage(
|
|
222
|
+
status="error", output_text=f"<tool_use_error>Failed to read source: {e}</tool_use_error>"
|
|
223
223
|
)
|
|
224
224
|
|
|
225
225
|
# Verify source hasn't been modified externally
|
|
226
226
|
if source_status is not None and source_status.content_sha256 is not None:
|
|
227
227
|
current_sha256 = hash_text_sha256(source_content)
|
|
228
228
|
if current_sha256 != source_status.content_sha256:
|
|
229
|
-
return
|
|
229
|
+
return message.ToolResultMessage(
|
|
230
230
|
status="error",
|
|
231
|
-
|
|
231
|
+
output_text="Source file has been modified externally. Read it first before editing.",
|
|
232
232
|
)
|
|
233
233
|
|
|
234
234
|
source_lines = source_content.splitlines(keepends=True)
|
|
235
235
|
|
|
236
236
|
# Validate line numbers against actual file
|
|
237
237
|
if args.start_line > len(source_lines):
|
|
238
|
-
return
|
|
238
|
+
return message.ToolResultMessage(
|
|
239
239
|
status="error",
|
|
240
|
-
|
|
240
|
+
output_text=f"<tool_use_error>start_line {args.start_line} exceeds file length {len(source_lines)}.</tool_use_error>",
|
|
241
241
|
)
|
|
242
242
|
if args.end_line > len(source_lines):
|
|
243
|
-
return
|
|
243
|
+
return message.ToolResultMessage(
|
|
244
244
|
status="error",
|
|
245
|
-
|
|
245
|
+
output_text=f"<tool_use_error>end_line {args.end_line} exceeds file length {len(source_lines)}.</tool_use_error>",
|
|
246
246
|
)
|
|
247
247
|
|
|
248
248
|
# Extract the lines to move (convert to 0-indexed)
|
|
@@ -254,24 +254,24 @@ class MoveTool(ToolABC):
|
|
|
254
254
|
try:
|
|
255
255
|
target_before = await asyncio.to_thread(read_text, target_path)
|
|
256
256
|
except OSError as e:
|
|
257
|
-
return
|
|
258
|
-
status="error",
|
|
257
|
+
return message.ToolResultMessage(
|
|
258
|
+
status="error", output_text=f"<tool_use_error>Failed to read target: {e}</tool_use_error>"
|
|
259
259
|
)
|
|
260
260
|
|
|
261
261
|
# Verify target hasn't been modified externally
|
|
262
262
|
if target_status is not None and target_status.content_sha256 is not None:
|
|
263
263
|
current_sha256 = hash_text_sha256(target_before)
|
|
264
264
|
if current_sha256 != target_status.content_sha256:
|
|
265
|
-
return
|
|
265
|
+
return message.ToolResultMessage(
|
|
266
266
|
status="error",
|
|
267
|
-
|
|
267
|
+
output_text="Target file has been modified externally. Read it first before writing to it.",
|
|
268
268
|
)
|
|
269
269
|
|
|
270
270
|
# For new target file, only allow insert_line = 1
|
|
271
271
|
if not target_exists and args.insert_line != 1:
|
|
272
|
-
return
|
|
272
|
+
return message.ToolResultMessage(
|
|
273
273
|
status="error",
|
|
274
|
-
|
|
274
|
+
output_text="<tool_use_error>Target file does not exist. Use insert_line=1 to create new file.</tool_use_error>",
|
|
275
275
|
)
|
|
276
276
|
|
|
277
277
|
# Build new content for both files
|
|
@@ -288,16 +288,16 @@ class MoveTool(ToolABC):
|
|
|
288
288
|
adjusted_insert -= args.end_line - args.start_line + 1
|
|
289
289
|
elif args.insert_line > args.start_line:
|
|
290
290
|
# Insert position is within the cut region - error
|
|
291
|
-
return
|
|
291
|
+
return message.ToolResultMessage(
|
|
292
292
|
status="error",
|
|
293
|
-
|
|
293
|
+
output_text="<tool_use_error>insert_line cannot be within the cut range for same-file move.</tool_use_error>",
|
|
294
294
|
)
|
|
295
295
|
|
|
296
296
|
# Validate adjusted insert line
|
|
297
297
|
if adjusted_insert > len(new_lines) + 1:
|
|
298
|
-
return
|
|
298
|
+
return message.ToolResultMessage(
|
|
299
299
|
status="error",
|
|
300
|
-
|
|
300
|
+
output_text=f"<tool_use_error>insert_line {args.insert_line} is out of bounds after cut.</tool_use_error>",
|
|
301
301
|
)
|
|
302
302
|
|
|
303
303
|
# Insert at adjusted position
|
|
@@ -309,8 +309,8 @@ class MoveTool(ToolABC):
|
|
|
309
309
|
try:
|
|
310
310
|
await asyncio.to_thread(write_text, source_path, source_after)
|
|
311
311
|
except OSError as e:
|
|
312
|
-
return
|
|
313
|
-
status="error",
|
|
312
|
+
return message.ToolResultMessage(
|
|
313
|
+
status="error", output_text=f"<tool_use_error>Failed to write: {e}</tool_use_error>"
|
|
314
314
|
)
|
|
315
315
|
|
|
316
316
|
# Update tracker
|
|
@@ -340,9 +340,9 @@ class MoveTool(ToolABC):
|
|
|
340
340
|
f"Source context (after cut):\n{cut_context}\n\n"
|
|
341
341
|
f"Insert context:\n{insert_context}"
|
|
342
342
|
)
|
|
343
|
-
return
|
|
343
|
+
return message.ToolResultMessage(
|
|
344
344
|
status="success",
|
|
345
|
-
|
|
345
|
+
output_text=output,
|
|
346
346
|
ui_extra=ui_extra,
|
|
347
347
|
)
|
|
348
348
|
else:
|
|
@@ -356,9 +356,9 @@ class MoveTool(ToolABC):
|
|
|
356
356
|
|
|
357
357
|
# Validate insert_line for existing target
|
|
358
358
|
if target_exists and args.insert_line > len(target_lines) + 1:
|
|
359
|
-
return
|
|
359
|
+
return message.ToolResultMessage(
|
|
360
360
|
status="error",
|
|
361
|
-
|
|
361
|
+
output_text=f"<tool_use_error>insert_line {args.insert_line} exceeds target file length + 1.</tool_use_error>",
|
|
362
362
|
)
|
|
363
363
|
|
|
364
364
|
new_target_lines = target_lines[: args.insert_line - 1] + cut_lines + target_lines[args.insert_line - 1 :]
|
|
@@ -373,8 +373,8 @@ class MoveTool(ToolABC):
|
|
|
373
373
|
await asyncio.to_thread(write_text, source_path, source_after)
|
|
374
374
|
await asyncio.to_thread(write_text, target_path, target_after)
|
|
375
375
|
except OSError as e:
|
|
376
|
-
return
|
|
377
|
-
status="error",
|
|
376
|
+
return message.ToolResultMessage(
|
|
377
|
+
status="error", output_text=f"<tool_use_error>Failed to write: {e}</tool_use_error>"
|
|
378
378
|
)
|
|
379
379
|
|
|
380
380
|
# Update tracker for both files
|
|
@@ -428,8 +428,8 @@ class MoveTool(ToolABC):
|
|
|
428
428
|
f"Source file context (after move):\n{source_context}\n\n"
|
|
429
429
|
f"Target file context (after insert):\n{target_context}"
|
|
430
430
|
)
|
|
431
|
-
return
|
|
431
|
+
return message.ToolResultMessage(
|
|
432
432
|
status="success",
|
|
433
|
-
|
|
433
|
+
output_text=output,
|
|
434
434
|
ui_extra=ui_extra,
|
|
435
435
|
)
|