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
@@ -10,12 +10,18 @@ from pathlib import Path
10
10
 
11
11
  from pydantic import BaseModel, Field
12
12
 
13
- from klaude_code import const
13
+ from klaude_code.const import (
14
+ BINARY_CHECK_SIZE,
15
+ READ_CHAR_LIMIT_PER_LINE,
16
+ READ_GLOBAL_LINE_CAP,
17
+ READ_MAX_CHARS,
18
+ READ_MAX_IMAGE_BYTES,
19
+ )
14
20
  from klaude_code.core.tool.file._utils import file_exists, is_directory
15
21
  from klaude_code.core.tool.tool_abc import ToolABC, load_desc
16
22
  from klaude_code.core.tool.tool_context import get_current_file_tracker
17
23
  from klaude_code.core.tool.tool_registry import register
18
- from klaude_code.protocol import llm_param, model, tools
24
+ from klaude_code.protocol import llm_param, message, model, tools
19
25
 
20
26
  _IMAGE_MIME_TYPES: dict[str, str] = {
21
27
  ".png": "image/png",
@@ -25,14 +31,12 @@ _IMAGE_MIME_TYPES: dict[str, str] = {
25
31
  ".webp": "image/webp",
26
32
  }
27
33
 
28
- _BINARY_CHECK_SIZE = 8192
29
-
30
34
 
31
35
  def _is_binary_file(file_path: str) -> bool:
32
36
  """Check if a file is binary by looking for null bytes in the first chunk."""
33
37
  try:
34
38
  with open(file_path, "rb") as f:
35
- chunk = f.read(_BINARY_CHECK_SIZE)
39
+ chunk = f.read(BINARY_CHECK_SIZE)
36
40
  return b"\x00" in chunk
37
41
  except OSError:
38
42
  return False
@@ -48,9 +52,9 @@ class ReadOptions:
48
52
  file_path: str
49
53
  offset: int
50
54
  limit: int | None
51
- char_limit_per_line: int | None = const.READ_CHAR_LIMIT_PER_LINE
52
- global_line_cap: int | None = const.READ_GLOBAL_LINE_CAP
53
- max_total_chars: int | None = const.READ_MAX_CHARS
55
+ char_limit_per_line: int | None = READ_CHAR_LIMIT_PER_LINE
56
+ global_line_cap: int | None = READ_GLOBAL_LINE_CAP
57
+ max_total_chars: int | None = READ_MAX_CHARS
54
58
 
55
59
 
56
60
  @dataclass
@@ -92,7 +96,7 @@ def _read_segment(options: ReadOptions) -> ReadSegmentResult:
92
96
  truncated_chars = original_len - options.char_limit_per_line
93
97
  content = (
94
98
  content[: options.char_limit_per_line]
95
- + f" ... (more {truncated_chars} characters in this line are truncated)"
99
+ + f" (more {truncated_chars} characters in this line are truncated)"
96
100
  )
97
101
  line_chars = len(content) + 1
98
102
  selected_chars += line_chars
@@ -178,42 +182,42 @@ class ReadTool(ToolABC):
178
182
  )
179
183
 
180
184
  @classmethod
181
- async def call(cls, arguments: str) -> model.ToolResultItem:
185
+ async def call(cls, arguments: str) -> message.ToolResultMessage:
182
186
  try:
183
187
  args = ReadTool.ReadArguments.model_validate_json(arguments)
184
188
  except Exception as e: # pragma: no cover - defensive
185
- return model.ToolResultItem(status="error", output=f"Invalid arguments: {e}")
189
+ return message.ToolResultMessage(status="error", output_text=f"Invalid arguments: {e}")
186
190
  return await cls.call_with_args(args)
187
191
 
188
192
  @classmethod
189
193
  def _effective_limits(cls) -> tuple[int | None, int | None, int | None]:
190
194
  return (
191
- const.READ_CHAR_LIMIT_PER_LINE,
192
- const.READ_GLOBAL_LINE_CAP,
193
- const.READ_MAX_CHARS,
195
+ READ_CHAR_LIMIT_PER_LINE,
196
+ READ_GLOBAL_LINE_CAP,
197
+ READ_MAX_CHARS,
194
198
  )
195
199
 
196
200
  @classmethod
197
- async def call_with_args(cls, args: ReadTool.ReadArguments) -> model.ToolResultItem:
201
+ async def call_with_args(cls, args: ReadTool.ReadArguments) -> message.ToolResultMessage:
198
202
  file_path = os.path.abspath(args.file_path)
199
203
  char_per_line, line_cap, max_chars = cls._effective_limits()
200
204
 
201
205
  if is_directory(file_path):
202
- return model.ToolResultItem(
206
+ return message.ToolResultMessage(
203
207
  status="error",
204
- output="<tool_use_error>Illegal operation on a directory. read</tool_use_error>",
208
+ output_text="<tool_use_error>Illegal operation on a directory. read</tool_use_error>",
205
209
  )
206
210
  if not file_exists(file_path):
207
- return model.ToolResultItem(
211
+ return message.ToolResultMessage(
208
212
  status="error",
209
- output="<tool_use_error>File does not exist.</tool_use_error>",
213
+ output_text="<tool_use_error>File does not exist.</tool_use_error>",
210
214
  )
211
215
 
212
216
  # Check for PDF files
213
217
  if Path(file_path).suffix.lower() == ".pdf":
214
- return model.ToolResultItem(
218
+ return message.ToolResultMessage(
215
219
  status="error",
216
- output=(
220
+ output_text=(
217
221
  "<tool_use_error>PDF files are not supported by this tool.\n"
218
222
  "If there's an available skill for PDF, use it.\n"
219
223
  "Or use a Python script with `pdfplumber` to extract text/tables:\n\n"
@@ -233,9 +237,9 @@ class ReadTool(ToolABC):
233
237
  is_image_file = _is_supported_image_file(file_path)
234
238
  # Check for binary files (skip for images which are handled separately)
235
239
  if not is_image_file and _is_binary_file(file_path):
236
- return model.ToolResultItem(
240
+ return message.ToolResultMessage(
237
241
  status="error",
238
- output=(
242
+ output_text=(
239
243
  "<tool_use_error>This appears to be a binary file and cannot be read as text. "
240
244
  "Use appropriate tools or libraries to handle binary files.</tool_use_error>"
241
245
  ),
@@ -247,12 +251,13 @@ class ReadTool(ToolABC):
247
251
  size_bytes = 0
248
252
 
249
253
  if is_image_file:
250
- if size_bytes > const.READ_MAX_IMAGE_BYTES:
254
+ if size_bytes > READ_MAX_IMAGE_BYTES:
251
255
  size_mb = size_bytes / (1024 * 1024)
252
- return model.ToolResultItem(
256
+ limit_mb = READ_MAX_IMAGE_BYTES / (1024 * 1024)
257
+ return message.ToolResultMessage(
253
258
  status="error",
254
- output=(
255
- f"<tool_use_error>Image size ({size_mb:.2f}MB) exceeds maximum supported size (4.00MB) for inline transfer.</tool_use_error>"
259
+ output_text=(
260
+ f"<tool_use_error>Image size ({size_mb:.2f}MB) exceeds maximum supported size ({limit_mb:.2f}MB) for inline transfer.</tool_use_error>"
256
261
  ),
257
262
  )
258
263
  try:
@@ -261,16 +266,16 @@ class ReadTool(ToolABC):
261
266
  image_bytes = image_file.read()
262
267
  data_url = f"data:{mime_type};base64,{b64encode(image_bytes).decode('ascii')}"
263
268
  except Exception as exc:
264
- return model.ToolResultItem(
269
+ return message.ToolResultMessage(
265
270
  status="error",
266
- output=f"<tool_use_error>Failed to read image file: {exc}</tool_use_error>",
271
+ output_text=f"<tool_use_error>Failed to read image file: {exc}</tool_use_error>",
267
272
  )
268
273
 
269
274
  _track_file_access(file_path, content_sha256=hashlib.sha256(image_bytes).hexdigest())
270
275
  size_kb = size_bytes / 1024.0 if size_bytes else 0.0
271
276
  output_text = f"[image] {Path(file_path).name} ({size_kb:.1f}KB)"
272
- image_part = model.ImageURLPart(image_url=model.ImageURLPart.ImageURL(url=data_url, id=None))
273
- return model.ToolResultItem(status="success", output=output_text, images=[image_part])
277
+ image_part = message.ImageURLPart(url=data_url, id=None)
278
+ return message.ToolResultMessage(status="success", output_text=output_text, parts=[image_part])
274
279
 
275
280
  offset = 1 if args.offset is None or args.offset < 1 else int(args.offset)
276
281
  limit = None if args.limit is None else int(args.limit)
@@ -291,36 +296,36 @@ class ReadTool(ToolABC):
291
296
  )
292
297
 
293
298
  except FileNotFoundError:
294
- return model.ToolResultItem(
299
+ return message.ToolResultMessage(
295
300
  status="error",
296
- output="<tool_use_error>File does not exist.</tool_use_error>",
301
+ output_text="<tool_use_error>File does not exist.</tool_use_error>",
297
302
  )
298
303
  except IsADirectoryError:
299
- return model.ToolResultItem(
304
+ return message.ToolResultMessage(
300
305
  status="error",
301
- output="<tool_use_error>Illegal operation on a directory. read</tool_use_error>",
306
+ output_text="<tool_use_error>Illegal operation on a directory. read</tool_use_error>",
302
307
  )
303
308
 
304
309
  if offset > max(read_result.total_lines, 0):
305
310
  warn = f"<system-reminder>Warning: the file exists but is shorter than the provided offset ({offset}). The file has {read_result.total_lines} lines.</system-reminder>"
306
311
  _track_file_access(file_path, content_sha256=read_result.content_sha256)
307
- return model.ToolResultItem(status="success", output=warn)
312
+ return message.ToolResultMessage(status="success", output_text=warn)
308
313
 
309
314
  lines_out: list[str] = [_format_numbered_line(no, content) for no, content in read_result.selected_lines]
310
315
 
311
316
  # Show truncation info with reason
312
317
  if read_result.remaining_due_to_char_limit > 0:
313
318
  lines_out.append(
314
- f"... ({read_result.remaining_due_to_char_limit} more lines truncated due to {max_chars} char limit, "
319
+ f" ({read_result.remaining_due_to_char_limit} more lines truncated due to {max_chars} char limit, "
315
320
  f"file has {read_result.total_lines} lines total, use offset/limit to read other parts)"
316
321
  )
317
322
  elif read_result.remaining_selected_beyond_cap > 0:
318
323
  lines_out.append(
319
- f"... ({read_result.remaining_selected_beyond_cap} more lines truncated due to {line_cap} line limit, "
324
+ f" ({read_result.remaining_selected_beyond_cap} more lines truncated due to {line_cap} line limit, "
320
325
  f"file has {read_result.total_lines} lines total, use offset/limit to read other parts)"
321
326
  )
322
327
 
323
328
  read_result_str = "\n".join(lines_out)
324
329
  _track_file_access(file_path, content_sha256=read_result.content_sha256)
325
330
 
326
- return model.ToolResultItem(status="success", output=read_result_str)
331
+ return message.ToolResultMessage(status="success", output_text=read_result_str)
@@ -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 WriteArguments(BaseModel):
@@ -46,18 +46,18 @@ class WriteTool(ToolABC):
46
46
  )
47
47
 
48
48
  @classmethod
49
- async def call(cls, arguments: str) -> model.ToolResultItem:
49
+ async def call(cls, arguments: str) -> message.ToolResultMessage:
50
50
  try:
51
51
  args = WriteArguments.model_validate_json(arguments)
52
52
  except ValueError as e: # pragma: no cover - defensive
53
- return model.ToolResultItem(status="error", output=f"Invalid arguments: {e}")
53
+ return message.ToolResultMessage(status="error", output_text=f"Invalid arguments: {e}")
54
54
 
55
55
  file_path = os.path.abspath(args.file_path)
56
56
 
57
57
  if is_directory(file_path):
58
- return model.ToolResultItem(
58
+ return message.ToolResultMessage(
59
59
  status="error",
60
- output="<tool_use_error>Illegal operation on a directory. write</tool_use_error>",
60
+ output_text="<tool_use_error>Illegal operation on a directory. write</tool_use_error>",
61
61
  )
62
62
 
63
63
  file_tracker = get_current_file_tracker()
@@ -67,9 +67,9 @@ class WriteTool(ToolABC):
67
67
  if exists:
68
68
  tracked_status = file_tracker.get(file_path) if file_tracker is not None else None
69
69
  if tracked_status is None:
70
- return model.ToolResultItem(
70
+ return message.ToolResultMessage(
71
71
  status="error",
72
- output=("File has not been read yet. Read it first before writing to it."),
72
+ output_text=("File has not been read yet. Read it first before writing to it."),
73
73
  )
74
74
 
75
75
  # Capture previous content (if any) for diff generation and external-change detection.
@@ -87,9 +87,9 @@ class WriteTool(ToolABC):
87
87
  if before_read_ok and tracked_status is not None and tracked_status.content_sha256 is not None:
88
88
  current_sha256 = hash_text_sha256(before)
89
89
  if current_sha256 != tracked_status.content_sha256:
90
- return model.ToolResultItem(
90
+ return message.ToolResultMessage(
91
91
  status="error",
92
- output=(
92
+ output_text=(
93
93
  "File has been modified externally. Either by user or a linter. "
94
94
  "Read it first before writing to it."
95
95
  ),
@@ -101,9 +101,9 @@ class WriteTool(ToolABC):
101
101
  except OSError:
102
102
  current_mtime = tracked_status.mtime
103
103
  if current_mtime != tracked_status.mtime:
104
- return model.ToolResultItem(
104
+ return message.ToolResultMessage(
105
105
  status="error",
106
- output=(
106
+ output_text=(
107
107
  "File has been modified externally. Either by user or a linter. "
108
108
  "Read it first before writing to it."
109
109
  ),
@@ -112,7 +112,7 @@ class WriteTool(ToolABC):
112
112
  try:
113
113
  await asyncio.to_thread(write_text, file_path, args.content)
114
114
  except (OSError, UnicodeError) as e: # pragma: no cover
115
- return model.ToolResultItem(status="error", output=f"<tool_use_error>{e}</tool_use_error>")
115
+ return message.ToolResultMessage(status="error", output_text=f"<tool_use_error>{e}</tool_use_error>")
116
116
 
117
117
  if file_tracker is not None:
118
118
  with contextlib.suppress(Exception):
@@ -132,5 +132,5 @@ class WriteTool(ToolABC):
132
132
  else:
133
133
  ui_extra = build_structured_diff(before, args.content, file_path=file_path)
134
134
 
135
- message = f"File {'overwritten' if exists else 'created'} successfully at: {file_path}"
136
- return model.ToolResultItem(status="success", output=message, ui_extra=ui_extra)
135
+ output_msg = f"File {'overwritten' if exists else 'created'} successfully at: {file_path}"
136
+ return message.ToolResultMessage(status="success", output_text=output_msg, ui_extra=ui_extra)
@@ -2,7 +2,7 @@
2
2
 
3
3
  from typing import Any, ClassVar, cast
4
4
 
5
- from klaude_code.protocol import llm_param, model, tools
5
+ from klaude_code.protocol import llm_param, message, tools
6
6
 
7
7
 
8
8
  def _normalize_schema_types(schema: dict[str, Any]) -> dict[str, Any]:
@@ -72,13 +72,13 @@ class ReportBackTool:
72
72
  )
73
73
 
74
74
  @classmethod
75
- async def call(cls, arguments: str) -> model.ToolResultItem:
75
+ async def call(cls, arguments: str) -> message.ToolResultMessage:
76
76
  """Execute the report_back tool.
77
77
 
78
78
  The actual handling of report_back results is done by TurnExecutor.
79
79
  This method just returns a success status to maintain the tool call flow.
80
80
  """
81
- return model.ToolResultItem(
81
+ return message.ToolResultMessage(
82
82
  status="success",
83
- output="Result reported successfully. Task will end.",
83
+ output_text="Result reported successfully. Task will end.",
84
84
  )
@@ -10,12 +10,12 @@ from typing import Any
10
10
 
11
11
  from pydantic import BaseModel
12
12
 
13
- from klaude_code import const
13
+ from klaude_code.const import BASH_DEFAULT_TIMEOUT_MS, BASH_TERMINATE_TIMEOUT_SEC
14
14
  from klaude_code.core.tool.shell.command_safety import is_safe_command
15
15
  from klaude_code.core.tool.tool_abc import ToolABC, load_desc
16
16
  from klaude_code.core.tool.tool_context import get_current_file_tracker
17
17
  from klaude_code.core.tool.tool_registry import register
18
- from klaude_code.protocol import llm_param, model, tools
18
+ from klaude_code.protocol import llm_param, message, model, tools
19
19
 
20
20
  # Regex to strip ANSI and terminal control sequences from command output
21
21
  #
@@ -58,8 +58,8 @@ class BashTool(ToolABC):
58
58
  },
59
59
  "timeout_ms": {
60
60
  "type": "integer",
61
- "description": f"The timeout for the command in milliseconds, default is {const.BASH_DEFAULT_TIMEOUT_MS}",
62
- "default": const.BASH_DEFAULT_TIMEOUT_MS,
61
+ "description": f"The timeout for the command in milliseconds, default is {BASH_DEFAULT_TIMEOUT_MS}",
62
+ "default": BASH_DEFAULT_TIMEOUT_MS,
63
63
  },
64
64
  },
65
65
  "required": ["command"],
@@ -68,27 +68,27 @@ class BashTool(ToolABC):
68
68
 
69
69
  class BashArguments(BaseModel):
70
70
  command: str
71
- timeout_ms: int = const.BASH_DEFAULT_TIMEOUT_MS
71
+ timeout_ms: int = BASH_DEFAULT_TIMEOUT_MS
72
72
 
73
73
  @classmethod
74
- async def call(cls, arguments: str) -> model.ToolResultItem:
74
+ async def call(cls, arguments: str) -> message.ToolResultMessage:
75
75
  try:
76
76
  args = BashTool.BashArguments.model_validate_json(arguments)
77
77
  except ValueError as e:
78
- return model.ToolResultItem(
78
+ return message.ToolResultMessage(
79
79
  status="error",
80
- output=f"Invalid arguments: {e}",
80
+ output_text=f"Invalid arguments: {e}",
81
81
  )
82
82
  return await cls.call_with_args(args)
83
83
 
84
84
  @classmethod
85
- async def call_with_args(cls, args: BashArguments) -> model.ToolResultItem:
85
+ async def call_with_args(cls, args: BashArguments) -> message.ToolResultMessage:
86
86
  # Safety check: only execute commands proven as "known safe"
87
87
  result = is_safe_command(args.command)
88
88
  if not result.is_safe:
89
- return model.ToolResultItem(
89
+ return message.ToolResultMessage(
90
90
  status="error",
91
- output=f"Command rejected: {result.error_msg}",
91
+ output_text=f"Command rejected: {result.error_msg}",
92
92
  )
93
93
 
94
94
  # Run the command using bash -lc so shell semantics work (pipes, &&, etc.)
@@ -279,7 +279,7 @@ class BashTool(ToolABC):
279
279
  pass
280
280
 
281
281
  with contextlib.suppress(Exception):
282
- await asyncio.wait_for(proc.wait(), timeout=1.0)
282
+ await asyncio.wait_for(proc.wait(), timeout=BASH_TERMINATE_TIMEOUT_SEC)
283
283
  return
284
284
 
285
285
  # Escalate to hard kill if it didn't exit quickly.
@@ -289,7 +289,7 @@ class BashTool(ToolABC):
289
289
  else:
290
290
  proc.kill()
291
291
  with contextlib.suppress(Exception):
292
- await asyncio.wait_for(proc.wait(), timeout=1.0)
292
+ await asyncio.wait_for(proc.wait(), timeout=BASH_TERMINATE_TIMEOUT_SEC)
293
293
 
294
294
  try:
295
295
  # Create a dedicated process group so we can terminate the whole tree.
@@ -311,9 +311,9 @@ class BashTool(ToolABC):
311
311
  except TimeoutError:
312
312
  with contextlib.suppress(Exception):
313
313
  await _terminate_process(proc)
314
- return model.ToolResultItem(
314
+ return message.ToolResultMessage(
315
315
  status="error",
316
- output=f"Timeout after {args.timeout_ms} ms running: {args.command}",
316
+ output_text=f"Timeout after {args.timeout_ms} ms running: {args.command}",
317
317
  )
318
318
  except asyncio.CancelledError:
319
319
  # Ensure subprocess is stopped and propagate cancellation.
@@ -332,11 +332,11 @@ class BashTool(ToolABC):
332
332
  output = (output + ("\n" if output else "")) + f"[stderr]\n{stderr}"
333
333
 
334
334
  _best_effort_update_file_tracker(args.command)
335
- return model.ToolResultItem(
335
+ return message.ToolResultMessage(
336
336
  status="success",
337
337
  # Preserve leading whitespace for tools like `nl -ba`.
338
338
  # Only trim trailing newlines to avoid adding an extra blank line in the UI.
339
- output=output.rstrip("\n"),
339
+ output_text=output.rstrip("\n"),
340
340
  )
341
341
  else:
342
342
  combined = ""
@@ -346,21 +346,21 @@ class BashTool(ToolABC):
346
346
  combined += f"[stderr]\n{stderr}"
347
347
  if not combined:
348
348
  combined = f"Command exited with code {rc}"
349
- return model.ToolResultItem(
349
+ return message.ToolResultMessage(
350
350
  status="error",
351
351
  # Preserve leading whitespace; only trim trailing newlines.
352
- output=combined.rstrip("\n"),
352
+ output_text=combined.rstrip("\n"),
353
353
  )
354
354
  except FileNotFoundError:
355
- return model.ToolResultItem(
355
+ return message.ToolResultMessage(
356
356
  status="error",
357
- output="bash not found on system path",
357
+ output_text="bash not found on system path",
358
358
  )
359
359
  except asyncio.CancelledError:
360
360
  # Propagate cooperative cancellation so outer layers can handle interrupts correctly.
361
361
  raise
362
362
  except OSError as e: # safeguard: catch remaining OS-level errors (permissions, resources, etc.)
363
- return model.ToolResultItem(
363
+ return message.ToolResultMessage(
364
364
  status="error",
365
- output=f"Execution error: {e}",
365
+ output_text=f"Execution error: {e}",
366
366
  )
@@ -6,7 +6,7 @@ from pydantic import BaseModel
6
6
 
7
7
  from klaude_code.core.tool.tool_abc import ToolABC, load_desc
8
8
  from klaude_code.core.tool.tool_registry import register
9
- from klaude_code.protocol import llm_param, model, tools
9
+ from klaude_code.protocol import llm_param, message, tools
10
10
  from klaude_code.skill import get_available_skills, get_skill, list_skill_names
11
11
 
12
12
 
@@ -55,23 +55,23 @@ class SkillTool(ToolABC):
55
55
  command: str
56
56
 
57
57
  @classmethod
58
- async def call(cls, arguments: str) -> model.ToolResultItem:
58
+ async def call(cls, arguments: str) -> message.ToolResultMessage:
59
59
  """Load and return full skill content."""
60
60
  try:
61
61
  args = cls.SkillArguments.model_validate_json(arguments)
62
62
  except ValueError as e:
63
- return model.ToolResultItem(
63
+ return message.ToolResultMessage(
64
64
  status="error",
65
- output=f"Invalid arguments: {e}",
65
+ output_text=f"Invalid arguments: {e}",
66
66
  )
67
67
 
68
68
  skill = get_skill(args.command)
69
69
 
70
70
  if not skill:
71
71
  available = ", ".join(list_skill_names())
72
- return model.ToolResultItem(
72
+ return message.ToolResultMessage(
73
73
  status="error",
74
- output=f"Skill '{args.command}' does not exist. Available skills: {available}",
74
+ output_text=f"Skill '{args.command}' does not exist. Available skills: {available}",
75
75
  )
76
76
 
77
77
  # Get base directory from skill_path
@@ -84,4 +84,4 @@ class SkillTool(ToolABC):
84
84
  Base directory for this skill: {base_dir}
85
85
 
86
86
  {skill.to_prompt()}"""
87
- return model.ToolResultItem(status="success", output=result)
87
+ return message.ToolResultMessage(status="success", output_text=result)
@@ -8,11 +8,12 @@ from __future__ import annotations
8
8
 
9
9
  import asyncio
10
10
  import json
11
- from typing import TYPE_CHECKING, ClassVar
11
+ from typing import TYPE_CHECKING, Any, ClassVar, cast
12
12
 
13
13
  from klaude_code.core.tool.tool_abc import ToolABC, ToolConcurrencyPolicy, ToolMetadata
14
- from klaude_code.core.tool.tool_context import current_run_subtask_callback
15
- from klaude_code.protocol import llm_param, model
14
+ from klaude_code.core.tool.tool_context import current_run_subtask_callback, current_sub_agent_resume_claims
15
+ from klaude_code.protocol import llm_param, message, model
16
+ from klaude_code.session.session import Session
16
17
 
17
18
  if TYPE_CHECKING:
18
19
  from klaude_code.protocol.sub_agent import SubAgentProfile
@@ -51,22 +52,48 @@ class SubAgentTool(ToolABC):
51
52
  )
52
53
 
53
54
  @classmethod
54
- async def call(cls, arguments: str) -> model.ToolResultItem:
55
+ async def call(cls, arguments: str) -> message.ToolResultMessage:
55
56
  profile = cls._profile
56
57
 
57
58
  try:
58
59
  args = json.loads(arguments)
59
60
  except json.JSONDecodeError as e:
60
- return model.ToolResultItem(status="error", output=f"Invalid JSON arguments: {e}")
61
+ return message.ToolResultMessage(status="error", output_text=f"Invalid JSON arguments: {e}")
61
62
 
62
63
  runner = current_run_subtask_callback.get()
63
64
  if runner is None:
64
- return model.ToolResultItem(status="error", output="No subtask runner available in this context")
65
+ return message.ToolResultMessage(status="error", output_text="No subtask runner available in this context")
65
66
 
66
67
  # Build the prompt using the profile's prompt builder
67
68
  prompt = profile.prompt_builder(args)
68
69
  description = args.get("description", "")
69
70
 
71
+ resume_raw = args.get("resume")
72
+ resume_session_id: str | None = None
73
+ if isinstance(resume_raw, str) and resume_raw.strip():
74
+ try:
75
+ resume_session_id = Session.resolve_sub_agent_session_id(resume_raw)
76
+ except ValueError as exc:
77
+ return message.ToolResultMessage(status="error", output_text=str(exc))
78
+
79
+ claims = current_sub_agent_resume_claims.get()
80
+ if claims is not None:
81
+ if resume_session_id in claims:
82
+ return message.ToolResultMessage(
83
+ status="error",
84
+ output_text=(
85
+ "Duplicate sub-agent resume in the same response: "
86
+ f"resume='{resume_raw.strip()}' (resolved='{resume_session_id[:7]}…'). "
87
+ "Merge into a single call or resume in a later turn."
88
+ ),
89
+ )
90
+ claims.add(resume_session_id)
91
+
92
+ generation = args.get("generation")
93
+ generation_dict: dict[str, Any] | None = (
94
+ cast(dict[str, Any], generation) if isinstance(generation, dict) else None
95
+ )
96
+
70
97
  # Extract output_schema if configured
71
98
  output_schema = None
72
99
  if profile.output_schema_arg:
@@ -78,17 +105,19 @@ class SubAgentTool(ToolABC):
78
105
  sub_agent_type=profile.name,
79
106
  sub_agent_desc=description,
80
107
  sub_agent_prompt=prompt,
108
+ resume=resume_session_id,
81
109
  output_schema=output_schema,
110
+ generation=generation_dict,
82
111
  )
83
112
  )
84
113
  except asyncio.CancelledError:
85
114
  raise
86
115
  except Exception as e:
87
- return model.ToolResultItem(status="error", output=f"Failed to run subtask: {e}")
116
+ return message.ToolResultMessage(status="error", output_text=f"Failed to run subtask: {e}")
88
117
 
89
- return model.ToolResultItem(
118
+ return message.ToolResultMessage(
90
119
  status="success" if not result.error else "error",
91
- output=result.task_result or "",
120
+ output_text=result.task_result,
92
121
  ui_extra=model.SessionIdUIExtra(session_id=result.session_id),
93
122
  task_metadata=result.task_metadata,
94
123
  )
@@ -5,7 +5,7 @@ from pydantic import BaseModel
5
5
  from klaude_code.core.tool.tool_abc import ToolABC, load_desc
6
6
  from klaude_code.core.tool.tool_context import get_current_todo_context
7
7
  from klaude_code.core.tool.tool_registry import register
8
- from klaude_code.protocol import llm_param, model, tools
8
+ from klaude_code.protocol import llm_param, message, model, tools
9
9
 
10
10
 
11
11
  def get_new_completed_todos(old_todos: list[model.TodoItem], new_todos: list[model.TodoItem]) -> list[str]:
@@ -77,21 +77,21 @@ class TodoWriteTool(ToolABC):
77
77
  )
78
78
 
79
79
  @classmethod
80
- async def call(cls, arguments: str) -> model.ToolResultItem:
80
+ async def call(cls, arguments: str) -> message.ToolResultMessage:
81
81
  try:
82
82
  args = TodoWriteArguments.model_validate_json(arguments)
83
83
  except ValueError as e:
84
- return model.ToolResultItem(
84
+ return message.ToolResultMessage(
85
85
  status="error",
86
- output=f"Invalid arguments: {e}",
86
+ output_text=f"Invalid arguments: {e}",
87
87
  )
88
88
 
89
89
  # Get current todo context to store todos
90
90
  todo_context = get_current_todo_context()
91
91
  if todo_context is None:
92
- return model.ToolResultItem(
92
+ return message.ToolResultMessage(
93
93
  status="error",
94
- output="No active session found",
94
+ output_text="No active session found",
95
95
  )
96
96
 
97
97
  # Get current todos before updating (for comparison)
@@ -113,9 +113,9 @@ Your todo list has changed. DO NOT mention this explicitly to the user. Here are
113
113
  {model.todo_list_str(args.todos)}. Continue on with the tasks at hand if applicable.
114
114
  </system-reminder>"""
115
115
 
116
- return model.ToolResultItem(
116
+ return message.ToolResultMessage(
117
117
  status="success",
118
- output=response,
118
+ output_text=response,
119
119
  ui_extra=model.TodoListUIExtra(todo_list=ui_extra),
120
120
  side_effects=[model.ToolSideEffect.TODO_CHANGE],
121
121
  )