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.
Files changed (129) hide show
  1. klaude_code/auth/base.py +2 -6
  2. klaude_code/cli/auth_cmd.py +4 -4
  3. klaude_code/cli/list_model.py +1 -1
  4. klaude_code/cli/main.py +1 -1
  5. klaude_code/cli/runtime.py +7 -5
  6. klaude_code/cli/self_update.py +1 -1
  7. klaude_code/cli/session_cmd.py +1 -1
  8. klaude_code/command/clear_cmd.py +6 -2
  9. klaude_code/command/command_abc.py +2 -2
  10. klaude_code/command/debug_cmd.py +4 -4
  11. klaude_code/command/export_cmd.py +2 -2
  12. klaude_code/command/export_online_cmd.py +12 -12
  13. klaude_code/command/fork_session_cmd.py +29 -23
  14. klaude_code/command/help_cmd.py +4 -4
  15. klaude_code/command/model_cmd.py +4 -4
  16. klaude_code/command/model_select.py +1 -1
  17. klaude_code/command/prompt-commit.md +11 -2
  18. klaude_code/command/prompt_command.py +3 -3
  19. klaude_code/command/refresh_cmd.py +2 -2
  20. klaude_code/command/registry.py +7 -5
  21. klaude_code/command/release_notes_cmd.py +4 -4
  22. klaude_code/command/resume_cmd.py +15 -11
  23. klaude_code/command/status_cmd.py +4 -4
  24. klaude_code/command/terminal_setup_cmd.py +8 -8
  25. klaude_code/command/thinking_cmd.py +4 -4
  26. klaude_code/config/assets/builtin_config.yaml +16 -0
  27. klaude_code/config/builtin_config.py +16 -5
  28. klaude_code/config/config.py +7 -2
  29. klaude_code/const.py +146 -91
  30. klaude_code/core/agent.py +3 -12
  31. klaude_code/core/executor.py +21 -13
  32. klaude_code/core/manager/sub_agent_manager.py +71 -7
  33. klaude_code/core/prompts/prompt-sub-agent-image-gen.md +1 -0
  34. klaude_code/core/prompts/prompt-sub-agent-web.md +27 -1
  35. klaude_code/core/reminders.py +88 -69
  36. klaude_code/core/task.py +44 -45
  37. klaude_code/core/tool/file/apply_patch_tool.py +9 -9
  38. klaude_code/core/tool/file/diff_builder.py +3 -5
  39. klaude_code/core/tool/file/edit_tool.py +23 -23
  40. klaude_code/core/tool/file/move_tool.py +43 -43
  41. klaude_code/core/tool/file/read_tool.py +44 -39
  42. klaude_code/core/tool/file/write_tool.py +14 -14
  43. klaude_code/core/tool/report_back_tool.py +4 -4
  44. klaude_code/core/tool/shell/bash_tool.py +23 -23
  45. klaude_code/core/tool/skill/skill_tool.py +7 -7
  46. klaude_code/core/tool/sub_agent_tool.py +38 -9
  47. klaude_code/core/tool/todo/todo_write_tool.py +8 -8
  48. klaude_code/core/tool/todo/update_plan_tool.py +6 -6
  49. klaude_code/core/tool/tool_abc.py +2 -2
  50. klaude_code/core/tool/tool_context.py +27 -0
  51. klaude_code/core/tool/tool_runner.py +88 -42
  52. klaude_code/core/tool/truncation.py +38 -20
  53. klaude_code/core/tool/web/mermaid_tool.py +6 -7
  54. klaude_code/core/tool/web/web_fetch_tool.py +68 -30
  55. klaude_code/core/tool/web/web_search_tool.py +15 -17
  56. klaude_code/core/turn.py +120 -73
  57. klaude_code/llm/anthropic/client.py +79 -44
  58. klaude_code/llm/anthropic/input.py +116 -108
  59. klaude_code/llm/bedrock/client.py +8 -5
  60. klaude_code/llm/claude/client.py +18 -8
  61. klaude_code/llm/client.py +4 -3
  62. klaude_code/llm/codex/client.py +15 -9
  63. klaude_code/llm/google/client.py +122 -60
  64. klaude_code/llm/google/input.py +94 -108
  65. klaude_code/llm/image.py +123 -0
  66. klaude_code/llm/input_common.py +136 -189
  67. klaude_code/llm/openai_compatible/client.py +17 -7
  68. klaude_code/llm/openai_compatible/input.py +36 -66
  69. klaude_code/llm/openai_compatible/stream.py +119 -67
  70. klaude_code/llm/openai_compatible/tool_call_accumulator.py +23 -11
  71. klaude_code/llm/openrouter/client.py +34 -9
  72. klaude_code/llm/openrouter/input.py +63 -64
  73. klaude_code/llm/openrouter/reasoning.py +22 -24
  74. klaude_code/llm/registry.py +20 -17
  75. klaude_code/llm/responses/client.py +107 -45
  76. klaude_code/llm/responses/input.py +115 -98
  77. klaude_code/llm/usage.py +52 -25
  78. klaude_code/protocol/__init__.py +1 -0
  79. klaude_code/protocol/events.py +16 -12
  80. klaude_code/protocol/llm_param.py +20 -2
  81. klaude_code/protocol/message.py +250 -0
  82. klaude_code/protocol/model.py +94 -281
  83. klaude_code/protocol/op.py +2 -2
  84. klaude_code/protocol/sub_agent/__init__.py +1 -0
  85. klaude_code/protocol/sub_agent/explore.py +10 -0
  86. klaude_code/protocol/sub_agent/image_gen.py +119 -0
  87. klaude_code/protocol/sub_agent/task.py +10 -0
  88. klaude_code/protocol/sub_agent/web.py +10 -0
  89. klaude_code/session/codec.py +6 -6
  90. klaude_code/session/export.py +261 -62
  91. klaude_code/session/selector.py +7 -24
  92. klaude_code/session/session.py +126 -54
  93. klaude_code/session/store.py +5 -32
  94. klaude_code/session/templates/export_session.html +1 -1
  95. klaude_code/session/templates/mermaid_viewer.html +1 -1
  96. klaude_code/trace/log.py +11 -6
  97. klaude_code/ui/core/input.py +1 -1
  98. klaude_code/ui/core/stage_manager.py +1 -8
  99. klaude_code/ui/modes/debug/display.py +2 -2
  100. klaude_code/ui/modes/repl/clipboard.py +2 -2
  101. klaude_code/ui/modes/repl/completers.py +18 -10
  102. klaude_code/ui/modes/repl/event_handler.py +136 -127
  103. klaude_code/ui/modes/repl/input_prompt_toolkit.py +1 -1
  104. klaude_code/ui/modes/repl/key_bindings.py +1 -1
  105. klaude_code/ui/modes/repl/renderer.py +107 -15
  106. klaude_code/ui/renderers/assistant.py +2 -2
  107. klaude_code/ui/renderers/common.py +65 -7
  108. klaude_code/ui/renderers/developer.py +7 -6
  109. klaude_code/ui/renderers/diffs.py +11 -11
  110. klaude_code/ui/renderers/mermaid_viewer.py +49 -2
  111. klaude_code/ui/renderers/metadata.py +33 -5
  112. klaude_code/ui/renderers/sub_agent.py +57 -16
  113. klaude_code/ui/renderers/thinking.py +37 -2
  114. klaude_code/ui/renderers/tools.py +180 -165
  115. klaude_code/ui/rich/live.py +3 -1
  116. klaude_code/ui/rich/markdown.py +39 -7
  117. klaude_code/ui/rich/quote.py +76 -1
  118. klaude_code/ui/rich/status.py +14 -8
  119. klaude_code/ui/rich/theme.py +8 -2
  120. klaude_code/ui/terminal/image.py +34 -0
  121. klaude_code/ui/terminal/notifier.py +2 -1
  122. klaude_code/ui/terminal/progress_bar.py +4 -4
  123. klaude_code/ui/terminal/selector.py +22 -4
  124. klaude_code/ui/utils/common.py +11 -2
  125. {klaude_code-1.9.0.dist-info → klaude_code-2.0.0.dist-info}/METADATA +4 -2
  126. klaude_code-2.0.0.dist-info/RECORD +229 -0
  127. klaude_code-1.9.0.dist-info/RECORD +0 -224
  128. {klaude_code-1.9.0.dist-info → klaude_code-2.0.0.dist-info}/WHEEL +0 -0
  129. {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 const
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, turn_metadata: model.ResponseMetadataItem) -> None:
37
- """Merge a turn's metadata into the accumulated state."""
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 = turn_metadata.usage
39
+ usage = turn_usage
40
40
 
41
- if usage is not None:
42
- if self._main_agent.usage is None:
43
- self._main_agent.usage = model.Usage()
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
- model.TaskMetadata.merge_usage(acc_usage, usage)
47
- acc_usage.currency = usage.currency
45
+ model.TaskMetadata.merge_usage(acc_usage, usage)
46
+ acc_usage.currency = usage.currency
48
47
 
49
- if usage.context_size is not None:
50
- acc_usage.context_size = usage.context_size
51
- if usage.context_limit is not None:
52
- acc_usage.context_limit = usage.context_limit
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
- if usage.first_token_latency_ms is not None:
55
- self._first_token_latency_sum += usage.first_token_latency_ms
56
- self._first_token_latency_count += 1
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
- if usage.throughput_tps is not None:
59
- current_output = usage.output_tokens
60
- if current_output > 0:
61
- self._throughput_weighted_sum += usage.throughput_tps * current_output
62
- self._throughput_tracked_tokens += current_output
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 turn_metadata.provider is not None:
65
- self._main_agent.provider = turn_metadata.provider
66
- if turn_metadata.model_name:
67
- self._main_agent.model_name = turn_metadata.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[model.ConversationItem]]
102
- append_history: Callable[[Sequence[model.ConversationItem]], None]
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: model.UserInputPayload) -> AsyncGenerator[events.Event]:
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(const.MAX_FAILED_TURN_RETRIES + 1):
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
- if e.metadata.usage is not None:
200
- context_percent = e.metadata.usage.context_usage_percent
201
- if context_percent is not None:
202
- yield events.ContextUsageEvent(
203
- session_id=session_ctx.session_id,
204
- context_percent=context_percent,
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 < const.MAX_FAILED_TURN_RETRIES:
217
+ if attempt < MAX_FAILED_TURN_RETRIES:
219
218
  delay = _retry_delay_seconds(attempt + 1)
220
- error_msg = f"Retrying {attempt + 1}/{const.MAX_FAILED_TURN_RETRIES} in {delay:.1f}s"
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 {const.MAX_FAILED_TURN_RETRIES} retries."
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 = const.INITIAL_RETRY_DELAY_S * (2 ** (capped_attempt - 1))
275
- return min(delay, const.MAX_RETRY_DELAY_S)
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) -> model.ToolResultItem:
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 model.ToolResultItem(status="error", output=str(error))
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 model.ToolResultItem(status="error", output=f"Execution error: {error}")
28
- return model.ToolResultItem(
27
+ return message.ToolResultMessage(status="error", output_text=f"Execution error: {error}")
28
+ return message.ToolResultMessage(
29
29
  status="success",
30
- output=output,
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) -> model.ToolResultItem:
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 model.ToolResultItem(status="error", output=f"Invalid arguments: {exc}")
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) -> model.ToolResultItem:
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=_DEFAULT_CONTEXT_LINES)
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) <= _MAX_LINE_LENGTH_FOR_CHAR_DIFF and len(new_line) <= _MAX_LINE_LENGTH_FOR_CHAR_DIFF
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) -> model.ToolResultItem:
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 model.ToolResultItem(status="error", output=f"Invalid arguments: {e}")
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 model.ToolResultItem(
98
+ return message.ToolResultMessage(
99
99
  status="error",
100
- output="<tool_use_error>Illegal operation on a directory. edit</tool_use_error>",
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 model.ToolResultItem(
104
+ return message.ToolResultMessage(
105
105
  status="error",
106
- output=(
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 model.ToolResultItem(
117
+ return message.ToolResultMessage(
118
118
  status="error",
119
- output=("File has not been read yet. Read it first before writing to it."),
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 model.ToolResultItem(
124
+ return message.ToolResultMessage(
125
125
  status="error",
126
- output=("File has not been read yet. Read it first before writing to it."),
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 model.ToolResultItem(
133
+ return message.ToolResultMessage(
134
134
  status="error",
135
- output="File has not been read yet. Read it first before writing to it.",
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 model.ToolResultItem(
143
+ return message.ToolResultMessage(
144
144
  status="error",
145
- output=(
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 model.ToolResultItem(
156
+ return message.ToolResultMessage(
157
157
  status="error",
158
- output=(
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 model.ToolResultItem(status="error", output=err)
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 model.ToolResultItem(
181
+ return message.ToolResultMessage(
182
182
  status="error",
183
- output=(
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 model.ToolResultItem(status="error", output=f"<tool_use_error>{e}</tool_use_error>")
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 model.ToolResultItem(status="success", output=msg, ui_extra=ui_extra)
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 model.ToolResultItem(status="success", output=output, ui_extra=ui_extra)
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) -> model.ToolResultItem:
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 model.ToolResultItem(status="error", output=f"Invalid arguments: {e}")
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 model.ToolResultItem(
171
+ return message.ToolResultMessage(
172
172
  status="error",
173
- output="<tool_use_error>Source path is a directory, not a file.</tool_use_error>",
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 model.ToolResultItem(
176
+ return message.ToolResultMessage(
177
177
  status="error",
178
- output="<tool_use_error>Target path is a directory, not a file.</tool_use_error>",
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 model.ToolResultItem(
183
+ return message.ToolResultMessage(
184
184
  status="error",
185
- output="<tool_use_error>start_line must be <= end_line.</tool_use_error>",
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 model.ToolResultItem(
194
+ return message.ToolResultMessage(
195
195
  status="error",
196
- output="<tool_use_error>Source file does not exist.</tool_use_error>",
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 model.ToolResultItem(
205
+ return message.ToolResultMessage(
206
206
  status="error",
207
- output="Source file has not been read yet. Read it first.",
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 model.ToolResultItem(
212
+ return message.ToolResultMessage(
213
213
  status="error",
214
- output="Target file has not been read yet. Read it first before writing to it.",
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 model.ToolResultItem(
222
- status="error", output=f"<tool_use_error>Failed to read source: {e}</tool_use_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 model.ToolResultItem(
229
+ return message.ToolResultMessage(
230
230
  status="error",
231
- output="Source file has been modified externally. Read it first before editing.",
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 model.ToolResultItem(
238
+ return message.ToolResultMessage(
239
239
  status="error",
240
- output=f"<tool_use_error>start_line {args.start_line} exceeds file length {len(source_lines)}.</tool_use_error>",
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 model.ToolResultItem(
243
+ return message.ToolResultMessage(
244
244
  status="error",
245
- output=f"<tool_use_error>end_line {args.end_line} exceeds file length {len(source_lines)}.</tool_use_error>",
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 model.ToolResultItem(
258
- status="error", output=f"<tool_use_error>Failed to read target: {e}</tool_use_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 model.ToolResultItem(
265
+ return message.ToolResultMessage(
266
266
  status="error",
267
- output="Target file has been modified externally. Read it first before writing to it.",
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 model.ToolResultItem(
272
+ return message.ToolResultMessage(
273
273
  status="error",
274
- output="<tool_use_error>Target file does not exist. Use insert_line=1 to create new file.</tool_use_error>",
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 model.ToolResultItem(
291
+ return message.ToolResultMessage(
292
292
  status="error",
293
- output="<tool_use_error>insert_line cannot be within the cut range for same-file move.</tool_use_error>",
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 model.ToolResultItem(
298
+ return message.ToolResultMessage(
299
299
  status="error",
300
- output=f"<tool_use_error>insert_line {args.insert_line} is out of bounds after cut.</tool_use_error>",
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 model.ToolResultItem(
313
- status="error", output=f"<tool_use_error>Failed to write: {e}</tool_use_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 model.ToolResultItem(
343
+ return message.ToolResultMessage(
344
344
  status="success",
345
- output=output,
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 model.ToolResultItem(
359
+ return message.ToolResultMessage(
360
360
  status="error",
361
- output=f"<tool_use_error>insert_line {args.insert_line} exceeds target file length + 1.</tool_use_error>",
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 model.ToolResultItem(
377
- status="error", output=f"<tool_use_error>Failed to write: {e}</tool_use_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 model.ToolResultItem(
431
+ return message.ToolResultMessage(
432
432
  status="success",
433
- output=output,
433
+ output_text=output,
434
434
  ui_extra=ui_extra,
435
435
  )