klaude-code 1.2.1__py3-none-any.whl → 1.2.3__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 (140) hide show
  1. klaude_code/cli/main.py +9 -4
  2. klaude_code/cli/runtime.py +42 -43
  3. klaude_code/command/__init__.py +7 -5
  4. klaude_code/command/clear_cmd.py +6 -29
  5. klaude_code/command/command_abc.py +44 -8
  6. klaude_code/command/diff_cmd.py +33 -27
  7. klaude_code/command/export_cmd.py +18 -26
  8. klaude_code/command/help_cmd.py +10 -8
  9. klaude_code/command/model_cmd.py +11 -40
  10. klaude_code/command/{prompt-update-dev-doc.md → prompt-dev-docs-update.md} +3 -2
  11. klaude_code/command/{prompt-dev-doc.md → prompt-dev-docs.md} +3 -2
  12. klaude_code/command/prompt-init.md +2 -5
  13. klaude_code/command/prompt_command.py +6 -6
  14. klaude_code/command/refresh_cmd.py +4 -5
  15. klaude_code/command/registry.py +16 -19
  16. klaude_code/command/terminal_setup_cmd.py +12 -11
  17. klaude_code/config/__init__.py +4 -0
  18. klaude_code/config/config.py +25 -26
  19. klaude_code/config/list_model.py +8 -3
  20. klaude_code/config/select_model.py +1 -1
  21. klaude_code/const/__init__.py +1 -1
  22. klaude_code/core/__init__.py +0 -3
  23. klaude_code/core/agent.py +25 -50
  24. klaude_code/core/executor.py +268 -101
  25. klaude_code/core/prompt.py +12 -12
  26. klaude_code/core/{prompt → prompts}/prompt-gemini.md +1 -1
  27. klaude_code/core/reminders.py +76 -95
  28. klaude_code/core/task.py +21 -14
  29. klaude_code/core/tool/__init__.py +45 -11
  30. klaude_code/core/tool/file/apply_patch.py +5 -1
  31. klaude_code/core/tool/file/apply_patch_tool.py +11 -13
  32. klaude_code/core/tool/file/edit_tool.py +27 -23
  33. klaude_code/core/tool/file/multi_edit_tool.py +15 -17
  34. klaude_code/core/tool/file/read_tool.py +41 -36
  35. klaude_code/core/tool/file/write_tool.py +13 -15
  36. klaude_code/core/tool/memory/memory_tool.py +85 -68
  37. klaude_code/core/tool/memory/skill_tool.py +10 -12
  38. klaude_code/core/tool/shell/bash_tool.py +24 -22
  39. klaude_code/core/tool/shell/command_safety.py +12 -1
  40. klaude_code/core/tool/sub_agent_tool.py +11 -12
  41. klaude_code/core/tool/todo/todo_write_tool.py +21 -28
  42. klaude_code/core/tool/todo/update_plan_tool.py +14 -24
  43. klaude_code/core/tool/tool_abc.py +3 -4
  44. klaude_code/core/tool/tool_context.py +7 -7
  45. klaude_code/core/tool/tool_registry.py +30 -47
  46. klaude_code/core/tool/tool_runner.py +35 -43
  47. klaude_code/core/tool/truncation.py +14 -20
  48. klaude_code/core/tool/web/mermaid_tool.py +12 -14
  49. klaude_code/core/tool/web/web_fetch_tool.py +15 -17
  50. klaude_code/core/turn.py +19 -7
  51. klaude_code/llm/__init__.py +3 -4
  52. klaude_code/llm/anthropic/client.py +30 -46
  53. klaude_code/llm/anthropic/input.py +4 -11
  54. klaude_code/llm/client.py +29 -8
  55. klaude_code/llm/input_common.py +66 -36
  56. klaude_code/llm/openai_compatible/client.py +42 -84
  57. klaude_code/llm/openai_compatible/input.py +11 -16
  58. klaude_code/llm/openai_compatible/tool_call_accumulator.py +2 -2
  59. klaude_code/llm/openrouter/client.py +40 -289
  60. klaude_code/llm/openrouter/input.py +13 -35
  61. klaude_code/llm/openrouter/reasoning_handler.py +209 -0
  62. klaude_code/llm/registry.py +5 -75
  63. klaude_code/llm/responses/client.py +34 -55
  64. klaude_code/llm/responses/input.py +24 -26
  65. klaude_code/llm/usage.py +109 -0
  66. klaude_code/protocol/__init__.py +4 -0
  67. klaude_code/protocol/events.py +3 -2
  68. klaude_code/protocol/{llm_parameter.py → llm_param.py} +12 -32
  69. klaude_code/protocol/model.py +49 -4
  70. klaude_code/protocol/op.py +18 -16
  71. klaude_code/protocol/op_handler.py +28 -0
  72. klaude_code/{core → protocol}/sub_agent.py +7 -0
  73. klaude_code/session/export.py +150 -70
  74. klaude_code/session/session.py +28 -14
  75. klaude_code/session/templates/export_session.html +180 -42
  76. klaude_code/trace/__init__.py +2 -2
  77. klaude_code/trace/log.py +11 -5
  78. klaude_code/ui/__init__.py +91 -8
  79. klaude_code/ui/core/__init__.py +1 -0
  80. klaude_code/ui/core/display.py +103 -0
  81. klaude_code/ui/core/input.py +71 -0
  82. klaude_code/ui/modes/__init__.py +1 -0
  83. klaude_code/ui/modes/debug/__init__.py +1 -0
  84. klaude_code/ui/{base/debug_event_display.py → modes/debug/display.py} +9 -5
  85. klaude_code/ui/modes/exec/__init__.py +1 -0
  86. klaude_code/ui/{base/exec_display.py → modes/exec/display.py} +28 -2
  87. klaude_code/ui/{repl → modes/repl}/__init__.py +5 -6
  88. klaude_code/ui/modes/repl/clipboard.py +152 -0
  89. klaude_code/ui/modes/repl/completers.py +429 -0
  90. klaude_code/ui/modes/repl/display.py +60 -0
  91. klaude_code/ui/modes/repl/event_handler.py +375 -0
  92. klaude_code/ui/modes/repl/input_prompt_toolkit.py +198 -0
  93. klaude_code/ui/modes/repl/key_bindings.py +170 -0
  94. klaude_code/ui/{repl → modes/repl}/renderer.py +109 -132
  95. klaude_code/ui/renderers/assistant.py +21 -0
  96. klaude_code/ui/renderers/common.py +0 -16
  97. klaude_code/ui/renderers/developer.py +18 -18
  98. klaude_code/ui/renderers/diffs.py +36 -14
  99. klaude_code/ui/renderers/errors.py +1 -1
  100. klaude_code/ui/renderers/metadata.py +50 -27
  101. klaude_code/ui/renderers/sub_agent.py +43 -9
  102. klaude_code/ui/renderers/thinking.py +33 -1
  103. klaude_code/ui/renderers/tools.py +212 -20
  104. klaude_code/ui/renderers/user_input.py +19 -23
  105. klaude_code/ui/rich/__init__.py +1 -0
  106. klaude_code/ui/{rich_ext → rich}/searchable_text.py +3 -1
  107. klaude_code/ui/{renderers → rich}/status.py +29 -18
  108. klaude_code/ui/{base → rich}/theme.py +8 -2
  109. klaude_code/ui/terminal/__init__.py +1 -0
  110. klaude_code/ui/{base/terminal_color.py → terminal/color.py} +4 -1
  111. klaude_code/ui/{base/terminal_control.py → terminal/control.py} +1 -0
  112. klaude_code/ui/{base/terminal_notifier.py → terminal/notifier.py} +5 -2
  113. klaude_code/ui/utils/__init__.py +1 -0
  114. klaude_code/ui/{base/utils.py → utils/common.py} +35 -3
  115. {klaude_code-1.2.1.dist-info → klaude_code-1.2.3.dist-info}/METADATA +1 -1
  116. klaude_code-1.2.3.dist-info/RECORD +161 -0
  117. klaude_code/core/clipboard_manifest.py +0 -124
  118. klaude_code/llm/openrouter/tool_call_accumulator.py +0 -80
  119. klaude_code/ui/base/__init__.py +0 -1
  120. klaude_code/ui/base/display_abc.py +0 -36
  121. klaude_code/ui/base/input_abc.py +0 -20
  122. klaude_code/ui/repl/display.py +0 -36
  123. klaude_code/ui/repl/event_handler.py +0 -247
  124. klaude_code/ui/repl/input.py +0 -773
  125. klaude_code/ui/rich_ext/__init__.py +0 -1
  126. klaude_code-1.2.1.dist-info/RECORD +0 -151
  127. /klaude_code/core/{prompt → prompts}/prompt-claude-code.md +0 -0
  128. /klaude_code/core/{prompt → prompts}/prompt-codex.md +0 -0
  129. /klaude_code/core/{prompt → prompts}/prompt-subagent-explore.md +0 -0
  130. /klaude_code/core/{prompt → prompts}/prompt-subagent-oracle.md +0 -0
  131. /klaude_code/core/{prompt → prompts}/prompt-subagent-webfetch.md +0 -0
  132. /klaude_code/core/{prompt → prompts}/prompt-subagent.md +0 -0
  133. /klaude_code/ui/{base → core}/stage_manager.py +0 -0
  134. /klaude_code/ui/{rich_ext → rich}/live.py +0 -0
  135. /klaude_code/ui/{rich_ext → rich}/markdown.py +0 -0
  136. /klaude_code/ui/{rich_ext → rich}/quote.py +0 -0
  137. /klaude_code/ui/{base → terminal}/progress_bar.py +0 -0
  138. /klaude_code/ui/{base → utils}/debouncer.py +0 -0
  139. {klaude_code-1.2.1.dist-info → klaude_code-1.2.3.dist-info}/WHEEL +0 -0
  140. {klaude_code-1.2.1.dist-info → klaude_code-1.2.3.dist-info}/entry_points.txt +0 -0
@@ -1,15 +1,11 @@
1
1
  import json
2
- import re
3
2
  from pathlib import Path
4
3
  from typing import Awaitable, Callable
5
4
 
6
5
  from pydantic import BaseModel
7
6
 
8
- from klaude_code.const import TODO_REMINDER_TOOL_CALL_THRESHOLD
9
- from klaude_code.core.clipboard_manifest import load_latest_clipboard_manifest, next_session_token
10
- from klaude_code.core.tool.file.read_tool import ReadTool
11
- from klaude_code.core.tool.shell.bash_tool import BashTool
12
- from klaude_code.core.tool.tool_context import reset_tool_context, set_tool_context_from_session
7
+ from klaude_code import const
8
+ from klaude_code.core.tool import BashTool, ReadTool, reset_tool_context, set_tool_context_from_session
13
9
  from klaude_code.protocol import model, tools
14
10
  from klaude_code.session import Session
15
11
 
@@ -30,7 +26,9 @@ def get_last_new_user_input(session: Session) -> str | None:
30
26
  return "\n\n".join(result)
31
27
 
32
28
 
33
- async def at_file_reader_reminder(session: Session) -> model.DeveloperMessageItem | None:
29
+ async def at_file_reader_reminder(
30
+ session: Session,
31
+ ) -> model.DeveloperMessageItem | None:
34
32
  """Parse @foo/bar to read"""
35
33
  last_user_input = get_last_new_user_input(session)
36
34
  if not last_user_input or "@" not in last_user_input.strip():
@@ -124,7 +122,9 @@ async def empty_todo_reminder(session: Session) -> model.DeveloperMessageItem |
124
122
  return None
125
123
 
126
124
 
127
- async def todo_not_used_recently_reminder(session: Session) -> model.DeveloperMessageItem | None:
125
+ async def todo_not_used_recently_reminder(
126
+ session: Session,
127
+ ) -> model.DeveloperMessageItem | None:
128
128
  """Remind agent to use TodoWrite tool if it hasn't been used recently (>=10 other tool calls), with cooldown.
129
129
 
130
130
  Cooldown behavior:
@@ -147,10 +147,10 @@ async def todo_not_used_recently_reminder(session: Session) -> model.DeveloperMe
147
147
  if item.name in (tools.TODO_WRITE, tools.UPDATE_PLAN):
148
148
  break
149
149
  other_tool_call_count_befor_last_todo += 1
150
- if other_tool_call_count_befor_last_todo >= TODO_REMINDER_TOOL_CALL_THRESHOLD:
150
+ if other_tool_call_count_befor_last_todo >= const.TODO_REMINDER_TOOL_CALL_THRESHOLD:
151
151
  break
152
152
 
153
- not_used_recently = other_tool_call_count_befor_last_todo >= TODO_REMINDER_TOOL_CALL_THRESHOLD
153
+ not_used_recently = other_tool_call_count_befor_last_todo >= const.TODO_REMINDER_TOOL_CALL_THRESHOLD
154
154
 
155
155
  if not not_used_recently:
156
156
  return None
@@ -173,7 +173,9 @@ Here are the existing contents of your todo list:
173
173
  return None
174
174
 
175
175
 
176
- async def file_changed_externally_reminder(session: Session) -> model.DeveloperMessageItem | None:
176
+ async def file_changed_externally_reminder(
177
+ session: Session,
178
+ ) -> model.DeveloperMessageItem | None:
177
179
  """Remind agent about user/linter' changes to the files in FileTracker, provding the newest content of the file."""
178
180
  changed_files: list[tuple[str, str, list[model.ImageURLPart] | None]] = []
179
181
  collected_images: list[model.ImageURLPart] = []
@@ -192,7 +194,13 @@ async def file_changed_externally_reminder(session: Session) -> model.DeveloperM
192
194
  collected_images.extend(tool_result.images)
193
195
  finally:
194
196
  reset_tool_context(context_token)
195
- except (FileNotFoundError, IsADirectoryError, OSError, PermissionError, UnicodeDecodeError):
197
+ except (
198
+ FileNotFoundError,
199
+ IsADirectoryError,
200
+ OSError,
201
+ PermissionError,
202
+ UnicodeDecodeError,
203
+ ):
196
204
  continue
197
205
  if len(changed_files) > 0:
198
206
  changed_files_str = "\n\n".join(
@@ -213,8 +221,14 @@ async def file_changed_externally_reminder(session: Session) -> model.DeveloperM
213
221
 
214
222
  def get_memory_paths() -> list[tuple[Path, str]]:
215
223
  return [
216
- (Path.home() / ".claude" / "CLAUDE.md", "user's private global instructions for all projects"),
217
- (Path.home() / ".codex" / "AGENTS.md", "user's private global instructions for all projects"),
224
+ (
225
+ Path.home() / ".claude" / "CLAUDE.md",
226
+ "user's private global instructions for all projects",
227
+ ),
228
+ (
229
+ Path.home() / ".codex" / "AGENTS.md",
230
+ "user's private global instructions for all projects",
231
+ ),
218
232
  (Path.cwd() / "AGENTS.md", "project instructions, checked into the codebase"),
219
233
  (Path.cwd() / "AGENT.md", "project instructions, checked into the codebase"),
220
234
  (Path.cwd() / "CLAUDE.md", "project instructions, checked into the codebase"),
@@ -227,6 +241,28 @@ class Memory(BaseModel):
227
241
  content: str
228
242
 
229
243
 
244
+ def get_last_user_message_image_count(session: Session) -> int:
245
+ """Get image count from the last user message in conversation history."""
246
+ for item in reversed(session.conversation_history):
247
+ if isinstance(item, model.ToolResultItem):
248
+ return 0
249
+ if isinstance(item, model.UserMessageItem):
250
+ return len(item.images) if item.images else 0
251
+ return 0
252
+
253
+
254
+ async def image_reminder(session: Session) -> model.DeveloperMessageItem | None:
255
+ """Remind agent about images attached by user in the last message."""
256
+ image_count = get_last_user_message_image_count(session)
257
+ if image_count == 0:
258
+ return None
259
+
260
+ return model.DeveloperMessageItem(
261
+ content=f"<system-reminder>User attached {image_count} image{'s' if image_count > 1 else ''} in their message. Make sure to analyze and reference these images as needed.</system-reminder>",
262
+ user_image_count=image_count,
263
+ )
264
+
265
+
230
266
  async def memory_reminder(session: Session) -> model.DeveloperMessageItem | None:
231
267
  """CLAUDE.md AGENTS.md"""
232
268
  memory_paths = get_memory_paths()
@@ -268,7 +304,14 @@ def get_last_turn_tool_call(session: Session) -> list[model.ToolCallItem]:
268
304
  for item in reversed(session.conversation_history):
269
305
  if isinstance(item, model.ToolCallItem):
270
306
  tool_calls.append(item)
271
- if isinstance(item, (model.ReasoningEncryptedItem, model.ReasoningTextItem, model.AssistantMessageItem)):
307
+ if isinstance(
308
+ item,
309
+ (
310
+ model.ReasoningEncryptedItem,
311
+ model.ReasoningTextItem,
312
+ model.AssistantMessageItem,
313
+ ),
314
+ ):
272
315
  break
273
316
  return tool_calls
274
317
 
@@ -276,7 +319,9 @@ def get_last_turn_tool_call(session: Session) -> list[model.ToolCallItem]:
276
319
  MEMORY_FILE_NAMES = ["CLAUDE.md", "AGENTS.md", "AGENT.md"]
277
320
 
278
321
 
279
- async def last_path_memory_reminder(session: Session) -> model.DeveloperMessageItem | None:
322
+ async def last_path_memory_reminder(
323
+ session: Session,
324
+ ) -> model.DeveloperMessageItem | None:
280
325
  """When last turn tool call entered a directory (or parent directory) with CLAUDE.md AGENTS.md"""
281
326
  tool_calls = get_last_turn_tool_call(session)
282
327
  if len(tool_calls) == 0:
@@ -356,63 +401,6 @@ async def last_path_memory_reminder(session: Session) -> model.DeveloperMessageI
356
401
  )
357
402
 
358
403
 
359
- async def clipboard_image_reminder(session: Session) -> model.DeveloperMessageItem | None:
360
- """Parse [Image #N] and attach images from clipboard history."""
361
- last_user_input = get_last_new_user_input(session)
362
- if not last_user_input or "[Image #" not in last_user_input:
363
- return None
364
-
365
- manifest = load_latest_clipboard_manifest()
366
- if manifest is None:
367
- return None
368
- manifest_source = manifest.source_id
369
- current_source = next_session_token()
370
- if manifest_source and manifest_source != current_source:
371
- return None
372
- image_map = manifest.tag_map()
373
- if not image_map:
374
- return None
375
-
376
- collected_images: list[model.ImageURLPart] = []
377
-
378
- # Find all tokens
379
- # Regex for [Image #(\d+)]
380
- matches = re.findall(r"\[Image #(\d+)\]", last_user_input)
381
- requested_tags = [f"[Image #{num}]" for num in matches]
382
-
383
- processed_paths: set[str] = set()
384
-
385
- attached_tags: list[str] = []
386
-
387
- for tag in requested_tags:
388
- if tag in image_map:
389
- path = image_map[tag]
390
- if path in processed_paths:
391
- continue
392
-
393
- context_token = set_tool_context_from_session(session)
394
- try:
395
- # We use ReadTool to get the image object in the correct format
396
- # This assumes ReadTool handles image files correctly
397
- args = ReadTool.ReadArguments(file_path=path)
398
- tool_result = await ReadTool.call_with_args(args)
399
- if tool_result.images:
400
- collected_images.extend(tool_result.images)
401
- processed_paths.add(path)
402
- attached_tags.append(tag)
403
- finally:
404
- reset_tool_context(context_token)
405
-
406
- if not collected_images:
407
- return None
408
-
409
- return model.DeveloperMessageItem(
410
- content="",
411
- images=collected_images,
412
- clipboard_images=attached_tags,
413
- )
414
-
415
-
416
404
  ALL_REMINDERS = [
417
405
  empty_todo_reminder,
418
406
  todo_not_used_recently_reminder,
@@ -420,19 +408,27 @@ ALL_REMINDERS = [
420
408
  memory_reminder,
421
409
  last_path_memory_reminder,
422
410
  at_file_reader_reminder,
423
- clipboard_image_reminder,
411
+ image_reminder,
424
412
  ]
425
413
 
426
414
 
427
- def get_vanilla_reminders() -> list[Reminder]:
428
- return [at_file_reader_reminder, clipboard_image_reminder]
415
+ def load_agent_reminders(
416
+ model_name: str, sub_agent_type: str | None = None, *, vanilla: bool = False
417
+ ) -> list[Reminder]:
418
+ """Get reminders for an agent based on model and agent type.
429
419
 
420
+ Args:
421
+ model_name: The model name.
422
+ sub_agent_type: If None, returns main agent reminders. Otherwise returns sub-agent reminders.
423
+ vanilla: If True, returns minimal vanilla reminders (ignores sub_agent_type).
424
+ """
425
+ if vanilla:
426
+ return [at_file_reader_reminder]
430
427
 
431
- def get_main_agent_reminders(model_name: str) -> list[Reminder]:
432
428
  reminders: list[Reminder] = []
433
429
 
434
- # For GPT-5, we do not show empty todo and todo not used recently reminders
435
- if "gpt-5" not in model_name:
430
+ # Only main agent (not sub-agent) gets todo reminders, and not for GPT-5
431
+ if sub_agent_type is None and "gpt-5" not in model_name:
436
432
  reminders.append(empty_todo_reminder)
437
433
  reminders.append(todo_not_used_recently_reminder)
438
434
 
@@ -441,23 +437,8 @@ def get_main_agent_reminders(model_name: str) -> list[Reminder]:
441
437
  memory_reminder,
442
438
  last_path_memory_reminder,
443
439
  at_file_reader_reminder,
444
- clipboard_image_reminder,
445
- file_changed_externally_reminder,
446
- ]
447
- )
448
-
449
- return reminders
450
-
451
-
452
- def get_sub_agent_reminders(model_name: str) -> list[Reminder]:
453
- reminders: list[Reminder] = []
454
- reminders.extend(
455
- [
456
- memory_reminder,
457
- last_path_memory_reminder,
458
- at_file_reader_reminder,
459
- clipboard_image_reminder,
460
440
  file_changed_externally_reminder,
441
+ image_reminder,
461
442
  ]
462
443
  )
463
444
 
klaude_code/core/task.py CHANGED
@@ -6,10 +6,9 @@ from collections.abc import AsyncGenerator, Callable, MutableMapping, Sequence
6
6
  from dataclasses import dataclass
7
7
  from typing import TYPE_CHECKING
8
8
 
9
- from klaude_code.const import INITIAL_RETRY_DELAY_S, MAX_FAILED_TURN_RETRIES, MAX_RETRY_DELAY_S
9
+ from klaude_code import const
10
10
  from klaude_code.core.reminders import Reminder
11
- from klaude_code.core.tool.tool_abc import ToolABC
12
- from klaude_code.core.tool.tool_context import TodoContext
11
+ from klaude_code.core.tool import TodoContext, ToolABC
13
12
  from klaude_code.core.turn import TurnError, TurnExecutionContext, TurnExecutor
14
13
  from klaude_code.protocol import events, model
15
14
  from klaude_code.trace import DebugType, log_debug
@@ -63,6 +62,16 @@ class MetadataAccumulator:
63
62
  self._throughput_weighted_sum += usage.throughput_tps * current_output
64
63
  self._throughput_tracked_tokens += current_output
65
64
 
65
+ # Accumulate costs
66
+ if usage.input_cost is not None:
67
+ acc_usage.input_cost = (acc_usage.input_cost or 0.0) + usage.input_cost
68
+ if usage.output_cost is not None:
69
+ acc_usage.output_cost = (acc_usage.output_cost or 0.0) + usage.output_cost
70
+ if usage.cache_read_cost is not None:
71
+ acc_usage.cache_read_cost = (acc_usage.cache_read_cost or 0.0) + usage.cache_read_cost
72
+ if usage.total_cost is not None:
73
+ acc_usage.total_cost = (acc_usage.total_cost or 0.0) + usage.total_cost
74
+
66
75
  if turn_metadata.provider is not None:
67
76
  accumulated.provider = turn_metadata.provider
68
77
  if turn_metadata.model_name:
@@ -79,9 +88,7 @@ class MetadataAccumulator:
79
88
  accumulated = self._accumulated
80
89
  if accumulated.usage is not None:
81
90
  if self._throughput_tracked_tokens > 0:
82
- accumulated.usage.throughput_tps = (
83
- self._throughput_weighted_sum / self._throughput_tracked_tokens
84
- )
91
+ accumulated.usage.throughput_tps = self._throughput_weighted_sum / self._throughput_tracked_tokens
85
92
  else:
86
93
  accumulated.usage.throughput_tps = None
87
94
 
@@ -128,7 +135,7 @@ class TaskExecutor:
128
135
  self._current_turn = None
129
136
  return ui_events
130
137
 
131
- async def run(self, user_input: str) -> AsyncGenerator[events.Event, None]:
138
+ async def run(self, user_input: model.UserInputPayload) -> AsyncGenerator[events.Event, None]:
132
139
  """Execute the task, yielding events as they occur."""
133
140
  ctx = self._context
134
141
  self._started_at = time.perf_counter()
@@ -138,7 +145,7 @@ class TaskExecutor:
138
145
  sub_agent_state=ctx.sub_agent_state,
139
146
  )
140
147
 
141
- ctx.append_history([model.UserMessageItem(content=user_input)])
148
+ ctx.append_history([model.UserMessageItem(content=user_input.text, images=user_input.images)])
142
149
 
143
150
  profile = ctx.profile
144
151
  metadata_accumulator = MetadataAccumulator(model_name=profile.llm_client.model_name)
@@ -166,7 +173,7 @@ class TaskExecutor:
166
173
  turn_succeeded = False
167
174
  last_error_message: str | None = None
168
175
 
169
- for attempt in range(MAX_FAILED_TURN_RETRIES + 1):
176
+ for attempt in range(const.MAX_FAILED_TURN_RETRIES + 1):
170
177
  turn = TurnExecutor(turn_context)
171
178
  self._current_turn = turn
172
179
 
@@ -186,9 +193,9 @@ class TaskExecutor:
186
193
  break
187
194
  except TurnError as e:
188
195
  last_error_message = str(e)
189
- if attempt < MAX_FAILED_TURN_RETRIES:
196
+ if attempt < const.MAX_FAILED_TURN_RETRIES:
190
197
  delay = _retry_delay_seconds(attempt + 1)
191
- error_msg = f"Retrying {attempt + 1}/{MAX_FAILED_TURN_RETRIES} in {delay:.1f}s"
198
+ error_msg = f"Retrying {attempt + 1}/{const.MAX_FAILED_TURN_RETRIES} in {delay:.1f}s"
192
199
  if last_error_message:
193
200
  error_msg = f"{error_msg} - {last_error_message}"
194
201
  yield events.ErrorEvent(error_message=error_msg, can_retry=True)
@@ -202,7 +209,7 @@ class TaskExecutor:
202
209
  style="red",
203
210
  debug_type=DebugType.EXECUTION,
204
211
  )
205
- final_error = f"Turn failed after {MAX_FAILED_TURN_RETRIES} retries."
212
+ final_error = f"Turn failed after {const.MAX_FAILED_TURN_RETRIES} retries."
206
213
  if last_error_message:
207
214
  final_error = f"{last_error_message}\n{final_error}"
208
215
  yield events.ErrorEvent(error_message=final_error, can_retry=False)
@@ -226,5 +233,5 @@ class TaskExecutor:
226
233
  def _retry_delay_seconds(attempt: int) -> float:
227
234
  """Compute exponential backoff delay for the given attempt count."""
228
235
  capped_attempt = max(1, attempt)
229
- delay = INITIAL_RETRY_DELAY_S * (2 ** (capped_attempt - 1))
230
- return min(delay, MAX_RETRY_DELAY_S)
236
+ delay = const.INITIAL_RETRY_DELAY_S * (2 ** (capped_attempt - 1))
237
+ return min(delay, const.MAX_RETRY_DELAY_S)
@@ -1,41 +1,75 @@
1
+ from .file.apply_patch import DiffError, process_patch
1
2
  from .file.apply_patch_tool import ApplyPatchTool
2
3
  from .file.edit_tool import EditTool
3
4
  from .file.multi_edit_tool import MultiEditTool
4
5
  from .file.read_tool import ReadTool
5
6
  from .file.write_tool import WriteTool
6
- from .memory.memory_tool import MemoryTool
7
+ from .memory.memory_tool import MEMORY_DIR_NAME, MemoryTool
8
+ from .memory.skill_loader import Skill, SkillLoader
7
9
  from .memory.skill_tool import SkillTool
8
10
  from .shell.bash_tool import BashTool
11
+ from .shell.command_safety import SafetyCheckResult, is_safe_command
9
12
  from .sub_agent_tool import SubAgentTool
10
13
  from .todo.todo_write_tool import TodoWriteTool
11
14
  from .todo.update_plan_tool import UpdatePlanTool
12
- from .tool_registry import get_main_agent_tools, get_registry, get_sub_agent_tools, get_tool_schemas
15
+ from .tool_abc import ToolABC
16
+ from .tool_context import (
17
+ TodoContext,
18
+ ToolContextToken,
19
+ current_run_subtask_callback,
20
+ reset_tool_context,
21
+ set_tool_context_from_session,
22
+ tool_context,
23
+ )
24
+ from .tool_registry import get_registry, get_tool_schemas, load_agent_tools
13
25
  from .tool_runner import run_tool
14
26
  from .truncation import SimpleTruncationStrategy, TruncationStrategy, get_truncation_strategy, set_truncation_strategy
15
27
  from .web.mermaid_tool import MermaidTool
16
28
  from .web.web_fetch_tool import WebFetchTool
17
29
 
18
30
  __all__ = [
31
+ # Tools
32
+ "ApplyPatchTool",
19
33
  "BashTool",
20
- "ReadTool",
21
34
  "EditTool",
22
35
  "MemoryTool",
36
+ "MermaidTool",
23
37
  "MultiEditTool",
38
+ "ReadTool",
39
+ "SkillTool",
24
40
  "SubAgentTool",
25
41
  "TodoWriteTool",
26
- "WriteTool",
27
- "SkillTool",
28
42
  "UpdatePlanTool",
29
- "ApplyPatchTool",
30
- "MermaidTool",
31
43
  "WebFetchTool",
32
- "get_tool_schemas",
44
+ "WriteTool",
45
+ # Tool ABC
46
+ "ToolABC",
47
+ # Tool context
48
+ "TodoContext",
49
+ "ToolContextToken",
50
+ "current_run_subtask_callback",
51
+ "reset_tool_context",
52
+ "set_tool_context_from_session",
53
+ "tool_context",
54
+ # Tool registry
55
+ "load_agent_tools",
33
56
  "get_registry",
57
+ "get_tool_schemas",
34
58
  "run_tool",
35
- "get_sub_agent_tools",
36
- "get_main_agent_tools",
37
- "TruncationStrategy",
59
+ # Truncation
38
60
  "SimpleTruncationStrategy",
61
+ "TruncationStrategy",
39
62
  "get_truncation_strategy",
40
63
  "set_truncation_strategy",
64
+ # Command safety
65
+ "SafetyCheckResult",
66
+ "is_safe_command",
67
+ # Skill
68
+ "Skill",
69
+ "SkillLoader",
70
+ # Memory
71
+ "MEMORY_DIR_NAME",
72
+ # Apply patch
73
+ "DiffError",
74
+ "process_patch",
41
75
  ]
@@ -417,7 +417,11 @@ def load_files(paths: list[str], open_fn: Callable[[str], str]) -> dict[str, str
417
417
  return orig
418
418
 
419
419
 
420
- def apply_commit(commit: Commit, write_fn: Callable[[str, str], None], remove_fn: Callable[[str], None]) -> None:
420
+ def apply_commit(
421
+ commit: Commit,
422
+ write_fn: Callable[[str, str], None],
423
+ remove_fn: Callable[[str], None],
424
+ ) -> None:
421
425
  for path, change in commit.changes.items():
422
426
  if change.type == ActionType.DELETE:
423
427
  remove_fn(path)
@@ -11,24 +11,22 @@ from klaude_code.core.tool.file import apply_patch as apply_patch_module
11
11
  from klaude_code.core.tool.tool_abc import ToolABC, load_desc
12
12
  from klaude_code.core.tool.tool_context import get_current_file_tracker
13
13
  from klaude_code.core.tool.tool_registry import register
14
- from klaude_code.protocol import tools
15
- from klaude_code.protocol.llm_parameter import ToolSchema
16
- from klaude_code.protocol.model import ToolResultItem, ToolResultUIExtra, ToolResultUIExtraType
14
+ from klaude_code.protocol import llm_param, model, tools
17
15
 
18
16
 
19
17
  class ApplyPatchHandler:
20
18
  @classmethod
21
- async def handle_apply_patch(cls, patch_text: str) -> ToolResultItem:
19
+ async def handle_apply_patch(cls, patch_text: str) -> model.ToolResultItem:
22
20
  try:
23
21
  output, diff_text = await asyncio.to_thread(cls._apply_patch_in_thread, patch_text)
24
22
  except apply_patch_module.DiffError as error:
25
- return ToolResultItem(status="error", output=str(error))
23
+ return model.ToolResultItem(status="error", output=str(error))
26
24
  except Exception as error: # pragma: no cover # unexpected errors bubbled to tool result
27
- return ToolResultItem(status="error", output=f"Execution error: {error}")
28
- return ToolResultItem(
25
+ return model.ToolResultItem(status="error", output=f"Execution error: {error}")
26
+ return model.ToolResultItem(
29
27
  status="success",
30
28
  output=output,
31
- ui_extra=ToolResultUIExtra(type=ToolResultUIExtraType.DIFF_TEXT, diff_text=diff_text),
29
+ ui_extra=model.ToolResultUIExtra(type=model.ToolResultUIExtraType.DIFF_TEXT, diff_text=diff_text),
32
30
  )
33
31
 
34
32
  @staticmethod
@@ -176,8 +174,8 @@ class ApplyPatchTool(ToolABC):
176
174
  patch: str
177
175
 
178
176
  @classmethod
179
- def schema(cls) -> ToolSchema:
180
- return ToolSchema(
177
+ def schema(cls) -> llm_param.ToolSchema:
178
+ return llm_param.ToolSchema(
181
179
  name=tools.APPLY_PATCH,
182
180
  type="function",
183
181
  description=load_desc(Path(__file__).parent / "apply_patch_tool.md"),
@@ -194,13 +192,13 @@ class ApplyPatchTool(ToolABC):
194
192
  )
195
193
 
196
194
  @classmethod
197
- async def call(cls, arguments: str) -> ToolResultItem:
195
+ async def call(cls, arguments: str) -> model.ToolResultItem:
198
196
  try:
199
197
  args = cls.ApplyPatchArguments.model_validate_json(arguments)
200
198
  except ValueError as exc:
201
- return ToolResultItem(status="error", output=f"Invalid arguments: {exc}")
199
+ return model.ToolResultItem(status="error", output=f"Invalid arguments: {exc}")
202
200
  return await cls.call_with_args(args)
203
201
 
204
202
  @classmethod
205
- async def call_with_args(cls, args: ApplyPatchArguments) -> ToolResultItem:
203
+ async def call_with_args(cls, args: ApplyPatchArguments) -> model.ToolResultItem:
206
204
  return await ApplyPatchHandler.handle_apply_patch(args.patch)
@@ -10,9 +10,7 @@ from pydantic import BaseModel, Field
10
10
  from klaude_code.core.tool.tool_abc import ToolABC, load_desc
11
11
  from klaude_code.core.tool.tool_context import get_current_file_tracker
12
12
  from klaude_code.core.tool.tool_registry import register
13
- from klaude_code.protocol.llm_parameter import ToolSchema
14
- from klaude_code.protocol.model import ToolResultItem, ToolResultUIExtra, ToolResultUIExtraType
15
- from klaude_code.protocol.tools import EDIT
13
+ from klaude_code.protocol import llm_param, model, tools
16
14
 
17
15
 
18
16
  def _is_directory(path: str) -> bool:
@@ -41,7 +39,7 @@ def _write_text(path: str, content: str) -> None:
41
39
  f.write(content)
42
40
 
43
41
 
44
- @register(EDIT)
42
+ @register(tools.EDIT)
45
43
  class EditTool(ToolABC):
46
44
  class EditArguments(BaseModel):
47
45
  file_path: str
@@ -50,9 +48,9 @@ class EditTool(ToolABC):
50
48
  replace_all: bool = Field(default=False)
51
49
 
52
50
  @classmethod
53
- def schema(cls) -> ToolSchema:
54
- return ToolSchema(
55
- name=EDIT,
51
+ def schema(cls) -> llm_param.ToolSchema:
52
+ return llm_param.ToolSchema(
53
+ name=tools.EDIT,
56
54
  type="function",
57
55
  description=load_desc(Path(__file__).parent / "edit_tool.md"),
58
56
  parameters={
@@ -112,23 +110,23 @@ class EditTool(ToolABC):
112
110
  return content.replace(old_string, new_string, 1)
113
111
 
114
112
  @classmethod
115
- async def call(cls, arguments: str) -> ToolResultItem:
113
+ async def call(cls, arguments: str) -> model.ToolResultItem:
116
114
  try:
117
115
  args = EditTool.EditArguments.model_validate_json(arguments)
118
116
  except Exception as e: # pragma: no cover - defensive
119
- return ToolResultItem(status="error", output=f"Invalid arguments: {e}")
117
+ return model.ToolResultItem(status="error", output=f"Invalid arguments: {e}")
120
118
 
121
119
  file_path = os.path.abspath(args.file_path)
122
120
 
123
121
  # Common file errors
124
122
  if _is_directory(file_path):
125
- return ToolResultItem(
123
+ return model.ToolResultItem(
126
124
  status="error",
127
125
  output="<tool_use_error>Illegal operation on a directory. edit</tool_use_error>",
128
126
  )
129
127
 
130
128
  if args.old_string == "":
131
- return ToolResultItem(
129
+ return model.ToolResultItem(
132
130
  status="error",
133
131
  output=(
134
132
  "<tool_use_error>old_string must not be empty for Edit. "
@@ -140,14 +138,14 @@ class EditTool(ToolABC):
140
138
  file_tracker = get_current_file_tracker()
141
139
  if not _file_exists(file_path):
142
140
  # We require reading before editing
143
- return ToolResultItem(
141
+ return model.ToolResultItem(
144
142
  status="error",
145
143
  output=("File has not been read yet. Read it first before writing to it."),
146
144
  )
147
145
  if file_tracker is not None:
148
146
  tracked = file_tracker.get(file_path)
149
147
  if tracked is None:
150
- return ToolResultItem(
148
+ return model.ToolResultItem(
151
149
  status="error",
152
150
  output=("File has not been read yet. Read it first before writing to it."),
153
151
  )
@@ -156,7 +154,7 @@ class EditTool(ToolABC):
156
154
  except Exception:
157
155
  current_mtime = tracked
158
156
  if current_mtime != tracked:
159
- return ToolResultItem(
157
+ return model.ToolResultItem(
160
158
  status="error",
161
159
  output=(
162
160
  "File has been modified externally. Either by user or a linter. Read it first before writing to it."
@@ -167,24 +165,30 @@ class EditTool(ToolABC):
167
165
  try:
168
166
  before = await asyncio.to_thread(_read_text, file_path)
169
167
  except FileNotFoundError:
170
- return ToolResultItem(
168
+ return model.ToolResultItem(
171
169
  status="error",
172
170
  output="File has not been read yet. Read it first before writing to it.",
173
171
  )
174
172
 
175
173
  err = cls.valid(
176
- content=before, old_string=args.old_string, new_string=args.new_string, replace_all=args.replace_all
174
+ content=before,
175
+ old_string=args.old_string,
176
+ new_string=args.new_string,
177
+ replace_all=args.replace_all,
177
178
  )
178
179
  if err is not None:
179
- return ToolResultItem(status="error", output=err)
180
+ return model.ToolResultItem(status="error", output=err)
180
181
 
181
182
  after = cls.execute(
182
- content=before, old_string=args.old_string, new_string=args.new_string, replace_all=args.replace_all
183
+ content=before,
184
+ old_string=args.old_string,
185
+ new_string=args.new_string,
186
+ replace_all=args.replace_all,
183
187
  )
184
188
 
185
189
  # If nothing changed due to replacement semantics (should not happen after valid), guard anyway
186
190
  if before == after:
187
- return ToolResultItem(
191
+ return model.ToolResultItem(
188
192
  status="error",
189
193
  output=(
190
194
  "<tool_use_error>No changes to make: old_string and new_string are exactly the same.</tool_use_error>"
@@ -195,7 +199,7 @@ class EditTool(ToolABC):
195
199
  try:
196
200
  await asyncio.to_thread(_write_text, file_path, after)
197
201
  except Exception as e: # pragma: no cover
198
- return ToolResultItem(status="error", output=f"<tool_use_error>{e}</tool_use_error>")
202
+ return model.ToolResultItem(status="error", output=f"<tool_use_error>{e}</tool_use_error>")
199
203
 
200
204
  # Prepare UI extra: unified diff with 3 context lines
201
205
  diff_lines = list(
@@ -208,7 +212,7 @@ class EditTool(ToolABC):
208
212
  )
209
213
  )
210
214
  diff_text = "\n".join(diff_lines)
211
- ui_extra = ToolResultUIExtra(type=ToolResultUIExtraType.DIFF_TEXT, diff_text=diff_text)
215
+ ui_extra = model.ToolResultUIExtra(type=model.ToolResultUIExtraType.DIFF_TEXT, diff_text=diff_text)
212
216
 
213
217
  # Update tracker with new mtime
214
218
  if file_tracker is not None:
@@ -220,7 +224,7 @@ class EditTool(ToolABC):
220
224
  # Build output message
221
225
  if args.replace_all:
222
226
  msg = f"The file {file_path} has been updated. All occurrences of '{args.old_string}' were successfully replaced with '{args.new_string}'."
223
- return ToolResultItem(status="success", output=msg, ui_extra=ui_extra)
227
+ return model.ToolResultItem(status="success", output=msg, ui_extra=ui_extra)
224
228
 
225
229
  # For single replacement, show a snippet consisting of context + added lines only
226
230
  # Parse the diff to collect target line numbers in the 'after' file
@@ -267,4 +271,4 @@ class EditTool(ToolABC):
267
271
  f"The file {file_path} has been updated. Here's the result of running `cat -n` on a snippet of the edited file:\n"
268
272
  f"{snippet}"
269
273
  )
270
- return ToolResultItem(status="success", output=output, ui_extra=ui_extra)
274
+ return model.ToolResultItem(status="success", output=output, ui_extra=ui_extra)