klaude-code 2.0.2__py3-none-any.whl → 2.1.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 (151) hide show
  1. klaude_code/app/__init__.py +12 -0
  2. klaude_code/app/runtime.py +215 -0
  3. klaude_code/cli/auth_cmd.py +2 -2
  4. klaude_code/cli/config_cmd.py +2 -2
  5. klaude_code/cli/cost_cmd.py +1 -1
  6. klaude_code/cli/debug.py +12 -36
  7. klaude_code/cli/list_model.py +3 -3
  8. klaude_code/cli/main.py +17 -60
  9. klaude_code/cli/self_update.py +2 -187
  10. klaude_code/cli/session_cmd.py +2 -2
  11. klaude_code/config/config.py +1 -1
  12. klaude_code/config/select_model.py +1 -1
  13. klaude_code/const.py +9 -1
  14. klaude_code/core/agent.py +9 -62
  15. klaude_code/core/agent_profile.py +284 -0
  16. klaude_code/core/executor.py +335 -230
  17. klaude_code/core/manager/llm_clients_builder.py +1 -1
  18. klaude_code/core/manager/sub_agent_manager.py +16 -29
  19. klaude_code/core/reminders.py +64 -99
  20. klaude_code/core/task.py +12 -20
  21. klaude_code/core/tool/__init__.py +5 -17
  22. klaude_code/core/tool/context.py +84 -0
  23. klaude_code/core/tool/file/apply_patch_tool.py +18 -21
  24. klaude_code/core/tool/file/edit_tool.py +39 -42
  25. klaude_code/core/tool/file/read_tool.py +14 -9
  26. klaude_code/core/tool/file/write_tool.py +12 -13
  27. klaude_code/core/tool/report_back_tool.py +4 -1
  28. klaude_code/core/tool/shell/bash_tool.py +6 -11
  29. klaude_code/core/tool/skill/skill_tool.py +3 -1
  30. klaude_code/core/tool/sub_agent_tool.py +8 -7
  31. klaude_code/core/tool/todo/todo_write_tool.py +3 -9
  32. klaude_code/core/tool/todo/update_plan_tool.py +3 -5
  33. klaude_code/core/tool/tool_abc.py +2 -1
  34. klaude_code/core/tool/tool_registry.py +2 -33
  35. klaude_code/core/tool/tool_runner.py +13 -10
  36. klaude_code/core/tool/web/mermaid_tool.py +3 -1
  37. klaude_code/core/tool/web/web_fetch_tool.py +5 -3
  38. klaude_code/core/tool/web/web_search_tool.py +5 -3
  39. klaude_code/core/turn.py +86 -26
  40. klaude_code/llm/anthropic/client.py +1 -1
  41. klaude_code/llm/bedrock/client.py +1 -1
  42. klaude_code/llm/claude/client.py +1 -1
  43. klaude_code/llm/codex/client.py +1 -1
  44. klaude_code/llm/google/client.py +1 -1
  45. klaude_code/llm/openai_compatible/client.py +1 -1
  46. klaude_code/llm/openai_compatible/tool_call_accumulator.py +1 -1
  47. klaude_code/llm/openrouter/client.py +1 -1
  48. klaude_code/llm/openrouter/reasoning.py +1 -1
  49. klaude_code/llm/responses/client.py +1 -1
  50. klaude_code/protocol/events/__init__.py +57 -0
  51. klaude_code/protocol/events/base.py +18 -0
  52. klaude_code/protocol/events/chat.py +20 -0
  53. klaude_code/protocol/events/lifecycle.py +22 -0
  54. klaude_code/protocol/events/metadata.py +15 -0
  55. klaude_code/protocol/events/streaming.py +43 -0
  56. klaude_code/protocol/events/system.py +53 -0
  57. klaude_code/protocol/events/tools.py +23 -0
  58. klaude_code/protocol/op.py +5 -0
  59. klaude_code/session/session.py +6 -5
  60. klaude_code/skill/assets/create-plan/SKILL.md +76 -0
  61. klaude_code/skill/loader.py +1 -1
  62. klaude_code/skill/system_skills.py +1 -1
  63. klaude_code/tui/__init__.py +8 -0
  64. klaude_code/{command → tui/command}/clear_cmd.py +2 -1
  65. klaude_code/{command → tui/command}/debug_cmd.py +3 -2
  66. klaude_code/{command → tui/command}/export_cmd.py +2 -1
  67. klaude_code/{command → tui/command}/export_online_cmd.py +2 -1
  68. klaude_code/{command → tui/command}/fork_session_cmd.py +4 -3
  69. klaude_code/{command → tui/command}/help_cmd.py +2 -1
  70. klaude_code/{command → tui/command}/model_cmd.py +4 -3
  71. klaude_code/{command → tui/command}/model_select.py +2 -2
  72. klaude_code/{command → tui/command}/prompt_command.py +4 -3
  73. klaude_code/{command → tui/command}/refresh_cmd.py +3 -1
  74. klaude_code/{command → tui/command}/registry.py +6 -5
  75. klaude_code/{command → tui/command}/release_notes_cmd.py +2 -1
  76. klaude_code/{command → tui/command}/resume_cmd.py +4 -3
  77. klaude_code/{command → tui/command}/status_cmd.py +2 -1
  78. klaude_code/{command → tui/command}/terminal_setup_cmd.py +2 -1
  79. klaude_code/{command → tui/command}/thinking_cmd.py +3 -2
  80. klaude_code/tui/commands.py +164 -0
  81. klaude_code/{ui/renderers → tui/components}/assistant.py +3 -3
  82. klaude_code/{ui/renderers → tui/components}/bash_syntax.py +2 -2
  83. klaude_code/{ui/renderers → tui/components}/common.py +1 -1
  84. klaude_code/{ui/renderers → tui/components}/developer.py +4 -4
  85. klaude_code/{ui/renderers → tui/components}/diffs.py +2 -2
  86. klaude_code/{ui/renderers → tui/components}/errors.py +2 -2
  87. klaude_code/{ui/renderers → tui/components}/metadata.py +7 -7
  88. klaude_code/{ui → tui/components}/rich/markdown.py +9 -23
  89. klaude_code/{ui → tui/components}/rich/status.py +2 -2
  90. klaude_code/{ui → tui/components}/rich/theme.py +3 -1
  91. klaude_code/{ui/renderers → tui/components}/sub_agent.py +23 -43
  92. klaude_code/{ui/renderers → tui/components}/thinking.py +3 -3
  93. klaude_code/{ui/renderers → tui/components}/tools.py +9 -9
  94. klaude_code/{ui/renderers → tui/components}/user_input.py +3 -20
  95. klaude_code/tui/display.py +85 -0
  96. klaude_code/{ui/modes/repl → tui/input}/__init__.py +1 -1
  97. klaude_code/{ui/modes/repl → tui/input}/completers.py +1 -1
  98. klaude_code/{ui/modes/repl/input_prompt_toolkit.py → tui/input/prompt_toolkit.py} +6 -6
  99. klaude_code/tui/machine.py +606 -0
  100. klaude_code/tui/renderer.py +707 -0
  101. klaude_code/tui/runner.py +321 -0
  102. klaude_code/tui/terminal/__init__.py +56 -0
  103. klaude_code/{ui → tui}/terminal/color.py +1 -1
  104. klaude_code/{ui → tui}/terminal/control.py +1 -1
  105. klaude_code/{ui → tui}/terminal/notifier.py +1 -1
  106. klaude_code/ui/__init__.py +6 -50
  107. klaude_code/ui/core/display.py +3 -3
  108. klaude_code/ui/core/input.py +2 -1
  109. klaude_code/ui/{modes/debug/display.py → debug_mode.py} +1 -1
  110. klaude_code/ui/{modes/exec/display.py → exec_mode.py} +0 -2
  111. klaude_code/ui/terminal/__init__.py +6 -54
  112. klaude_code/ui/terminal/title.py +31 -0
  113. klaude_code/update.py +163 -0
  114. {klaude_code-2.0.2.dist-info → klaude_code-2.1.0.dist-info}/METADATA +1 -1
  115. klaude_code-2.1.0.dist-info/RECORD +235 -0
  116. klaude_code/cli/runtime.py +0 -518
  117. klaude_code/core/prompt.py +0 -108
  118. klaude_code/core/tool/tool_context.py +0 -148
  119. klaude_code/protocol/events.py +0 -195
  120. klaude_code/skill/assets/dev-docs/SKILL.md +0 -108
  121. klaude_code/trace/__init__.py +0 -21
  122. klaude_code/ui/core/stage_manager.py +0 -48
  123. klaude_code/ui/modes/__init__.py +0 -1
  124. klaude_code/ui/modes/debug/__init__.py +0 -1
  125. klaude_code/ui/modes/exec/__init__.py +0 -1
  126. klaude_code/ui/modes/repl/display.py +0 -61
  127. klaude_code/ui/modes/repl/event_handler.py +0 -629
  128. klaude_code/ui/modes/repl/renderer.py +0 -464
  129. klaude_code/ui/utils/__init__.py +0 -1
  130. klaude_code-2.0.2.dist-info/RECORD +0 -227
  131. /klaude_code/{trace/log.py → log.py} +0 -0
  132. /klaude_code/{command → tui/command}/__init__.py +0 -0
  133. /klaude_code/{command → tui/command}/command_abc.py +0 -0
  134. /klaude_code/{command → tui/command}/prompt-commit.md +0 -0
  135. /klaude_code/{command → tui/command}/prompt-init.md +0 -0
  136. /klaude_code/{ui/renderers → tui/components}/__init__.py +0 -0
  137. /klaude_code/{ui/renderers → tui/components}/mermaid_viewer.py +0 -0
  138. /klaude_code/{ui → tui/components}/rich/__init__.py +0 -0
  139. /klaude_code/{ui → tui/components}/rich/cjk_wrap.py +0 -0
  140. /klaude_code/{ui → tui/components}/rich/code_panel.py +0 -0
  141. /klaude_code/{ui → tui/components}/rich/live.py +0 -0
  142. /klaude_code/{ui → tui/components}/rich/quote.py +0 -0
  143. /klaude_code/{ui → tui/components}/rich/searchable_text.py +0 -0
  144. /klaude_code/{ui/modes/repl → tui/input}/clipboard.py +0 -0
  145. /klaude_code/{ui/modes/repl → tui/input}/key_bindings.py +0 -0
  146. /klaude_code/{ui → tui}/terminal/image.py +0 -0
  147. /klaude_code/{ui → tui}/terminal/progress_bar.py +0 -0
  148. /klaude_code/{ui → tui}/terminal/selector.py +0 -0
  149. /klaude_code/ui/{utils/common.py → common.py} +0 -0
  150. {klaude_code-2.0.2.dist-info → klaude_code-2.1.0.dist-info}/WHEEL +0 -0
  151. {klaude_code-2.0.2.dist-info → klaude_code-2.1.0.dist-info}/entry_points.txt +0 -0
@@ -9,10 +9,10 @@ from pathlib import Path
9
9
  from pydantic import BaseModel, Field
10
10
 
11
11
  from klaude_code.const import DIFF_DEFAULT_CONTEXT_LINES
12
+ from klaude_code.core.tool.context import ToolContext
12
13
  from klaude_code.core.tool.file._utils import file_exists, hash_text_sha256, is_directory, read_text, write_text
13
14
  from klaude_code.core.tool.file.diff_builder import build_structured_diff
14
15
  from klaude_code.core.tool.tool_abc import ToolABC, load_desc
15
- from klaude_code.core.tool.tool_context import get_current_file_tracker
16
16
  from klaude_code.core.tool.tool_registry import register
17
17
  from klaude_code.protocol import llm_param, message, model, tools
18
18
 
@@ -86,7 +86,7 @@ class EditTool(ToolABC):
86
86
  return content.replace(old_string, new_string, 1)
87
87
 
88
88
  @classmethod
89
- async def call(cls, arguments: str) -> message.ToolResultMessage:
89
+ async def call(cls, arguments: str, context: ToolContext) -> message.ToolResultMessage:
90
90
  try:
91
91
  args = EditTool.EditArguments.model_validate_json(arguments)
92
92
  except ValueError as e: # pragma: no cover - defensive
@@ -111,7 +111,7 @@ class EditTool(ToolABC):
111
111
  )
112
112
 
113
113
  # FileTracker checks (only for editing existing files)
114
- file_tracker = get_current_file_tracker()
114
+ file_tracker = context.file_tracker
115
115
  tracked_status: model.FileStatus | None = None
116
116
  if not file_exists(file_path):
117
117
  # We require reading before editing
@@ -119,13 +119,12 @@ class EditTool(ToolABC):
119
119
  status="error",
120
120
  output_text=("File has not been read yet. Read it first before writing to it."),
121
121
  )
122
- if file_tracker is not None:
123
- tracked_status = file_tracker.get(file_path)
124
- if tracked_status is None:
125
- return message.ToolResultMessage(
126
- status="error",
127
- output_text=("File has not been read yet. Read it first before writing to it."),
128
- )
122
+ tracked_status = file_tracker.get(file_path)
123
+ if tracked_status is None:
124
+ return message.ToolResultMessage(
125
+ status="error",
126
+ output_text=("File has not been read yet. Read it first before writing to it."),
127
+ )
129
128
 
130
129
  # Edit existing file: validate and apply
131
130
  try:
@@ -137,29 +136,28 @@ class EditTool(ToolABC):
137
136
  )
138
137
 
139
138
  # Re-check external modifications using content hash when available.
140
- if tracked_status is not None:
141
- if tracked_status.content_sha256 is not None:
142
- current_sha256 = hash_text_sha256(before)
143
- if current_sha256 != tracked_status.content_sha256:
144
- return message.ToolResultMessage(
145
- status="error",
146
- output_text=(
147
- "File has been modified externally. Either by user or a linter. Read it first before writing to it."
148
- ),
149
- )
150
- else:
151
- # Backward-compat: old sessions only stored mtime.
152
- try:
153
- current_mtime = Path(file_path).stat().st_mtime
154
- except OSError:
155
- current_mtime = tracked_status.mtime
156
- if current_mtime != tracked_status.mtime:
157
- return message.ToolResultMessage(
158
- status="error",
159
- output_text=(
160
- "File has been modified externally. Either by user or a linter. Read it first before writing to it."
161
- ),
162
- )
139
+ if tracked_status.content_sha256 is not None:
140
+ current_sha256 = hash_text_sha256(before)
141
+ if current_sha256 != tracked_status.content_sha256:
142
+ return message.ToolResultMessage(
143
+ status="error",
144
+ output_text=(
145
+ "File has been modified externally. Either by user or a linter. Read it first before writing to it."
146
+ ),
147
+ )
148
+ else:
149
+ # Backward-compat: old sessions only stored mtime.
150
+ try:
151
+ current_mtime = Path(file_path).stat().st_mtime
152
+ except OSError:
153
+ current_mtime = tracked_status.mtime
154
+ if current_mtime != tracked_status.mtime:
155
+ return message.ToolResultMessage(
156
+ status="error",
157
+ output_text=(
158
+ "File has been modified externally. Either by user or a linter. Read it first before writing to it."
159
+ ),
160
+ )
163
161
 
164
162
  err = cls.valid(
165
163
  content=before,
@@ -205,15 +203,14 @@ class EditTool(ToolABC):
205
203
  ui_extra = build_structured_diff(before, after, file_path=file_path)
206
204
 
207
205
  # Update tracker with new mtime and content hash
208
- if file_tracker is not None:
209
- with contextlib.suppress(Exception):
210
- existing = file_tracker.get(file_path)
211
- is_mem = existing.is_memory if existing else False
212
- file_tracker[file_path] = model.FileStatus(
213
- mtime=Path(file_path).stat().st_mtime,
214
- content_sha256=hash_text_sha256(after),
215
- is_memory=is_mem,
216
- )
206
+ with contextlib.suppress(Exception):
207
+ existing = file_tracker.get(file_path)
208
+ is_mem = existing.is_memory if existing else False
209
+ file_tracker[file_path] = model.FileStatus(
210
+ mtime=Path(file_path).stat().st_mtime,
211
+ content_sha256=hash_text_sha256(after),
212
+ is_memory=is_mem,
213
+ )
217
214
 
218
215
  # Build output message
219
216
  if args.replace_all:
@@ -17,9 +17,9 @@ from klaude_code.const import (
17
17
  READ_MAX_CHARS,
18
18
  READ_MAX_IMAGE_BYTES,
19
19
  )
20
+ from klaude_code.core.tool.context import FileTracker, ToolContext
20
21
  from klaude_code.core.tool.file._utils import file_exists, is_directory
21
22
  from klaude_code.core.tool.tool_abc import ToolABC, load_desc
22
- from klaude_code.core.tool.tool_context import get_current_file_tracker
23
23
  from klaude_code.core.tool.tool_registry import register
24
24
  from klaude_code.protocol import llm_param, message, model, tools
25
25
 
@@ -121,8 +121,13 @@ def _read_segment(options: ReadOptions) -> ReadSegmentResult:
121
121
  )
122
122
 
123
123
 
124
- def _track_file_access(file_path: str, *, content_sha256: str | None = None, is_memory: bool = False) -> None:
125
- file_tracker = get_current_file_tracker()
124
+ def _track_file_access(
125
+ file_tracker: FileTracker | None,
126
+ file_path: str,
127
+ *,
128
+ content_sha256: str | None = None,
129
+ is_memory: bool = False,
130
+ ) -> None:
126
131
  if file_tracker is None or not file_exists(file_path) or is_directory(file_path):
127
132
  return
128
133
  with contextlib.suppress(Exception):
@@ -182,12 +187,12 @@ class ReadTool(ToolABC):
182
187
  )
183
188
 
184
189
  @classmethod
185
- async def call(cls, arguments: str) -> message.ToolResultMessage:
190
+ async def call(cls, arguments: str, context: ToolContext) -> message.ToolResultMessage:
186
191
  try:
187
192
  args = ReadTool.ReadArguments.model_validate_json(arguments)
188
193
  except Exception as e: # pragma: no cover - defensive
189
194
  return message.ToolResultMessage(status="error", output_text=f"Invalid arguments: {e}")
190
- return await cls.call_with_args(args)
195
+ return await cls.call_with_args(args, context)
191
196
 
192
197
  @classmethod
193
198
  def _effective_limits(cls) -> tuple[int | None, int | None, int | None]:
@@ -198,7 +203,7 @@ class ReadTool(ToolABC):
198
203
  )
199
204
 
200
205
  @classmethod
201
- async def call_with_args(cls, args: ReadTool.ReadArguments) -> message.ToolResultMessage:
206
+ async def call_with_args(cls, args: ReadTool.ReadArguments, context: ToolContext) -> message.ToolResultMessage:
202
207
  file_path = os.path.abspath(args.file_path)
203
208
  char_per_line, line_cap, max_chars = cls._effective_limits()
204
209
 
@@ -271,7 +276,7 @@ class ReadTool(ToolABC):
271
276
  output_text=f"<tool_use_error>Failed to read image file: {exc}</tool_use_error>",
272
277
  )
273
278
 
274
- _track_file_access(file_path, content_sha256=hashlib.sha256(image_bytes).hexdigest())
279
+ _track_file_access(context.file_tracker, file_path, content_sha256=hashlib.sha256(image_bytes).hexdigest())
275
280
  size_kb = size_bytes / 1024.0 if size_bytes else 0.0
276
281
  output_text = f"[image] {Path(file_path).name} ({size_kb:.1f}KB)"
277
282
  image_part = message.ImageURLPart(url=data_url, id=None)
@@ -308,7 +313,7 @@ class ReadTool(ToolABC):
308
313
 
309
314
  if offset > max(read_result.total_lines, 0):
310
315
  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>"
311
- _track_file_access(file_path, content_sha256=read_result.content_sha256)
316
+ _track_file_access(context.file_tracker, file_path, content_sha256=read_result.content_sha256)
312
317
  return message.ToolResultMessage(status="success", output_text=warn)
313
318
 
314
319
  lines_out: list[str] = [_format_numbered_line(no, content) for no, content in read_result.selected_lines]
@@ -326,6 +331,6 @@ class ReadTool(ToolABC):
326
331
  )
327
332
 
328
333
  read_result_str = "\n".join(lines_out)
329
- _track_file_access(file_path, content_sha256=read_result.content_sha256)
334
+ _track_file_access(context.file_tracker, file_path, content_sha256=read_result.content_sha256)
330
335
 
331
336
  return message.ToolResultMessage(status="success", output_text=read_result_str)
@@ -7,10 +7,10 @@ from pathlib import Path
7
7
 
8
8
  from pydantic import BaseModel
9
9
 
10
+ from klaude_code.core.tool.context import ToolContext
10
11
  from klaude_code.core.tool.file._utils import file_exists, hash_text_sha256, is_directory, read_text, write_text
11
12
  from klaude_code.core.tool.file.diff_builder import build_structured_diff
12
13
  from klaude_code.core.tool.tool_abc import ToolABC, load_desc
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
15
  from klaude_code.protocol import llm_param, message, model, tools
16
16
 
@@ -46,7 +46,7 @@ class WriteTool(ToolABC):
46
46
  )
47
47
 
48
48
  @classmethod
49
- async def call(cls, arguments: str) -> message.ToolResultMessage:
49
+ async def call(cls, arguments: str, context: ToolContext) -> message.ToolResultMessage:
50
50
  try:
51
51
  args = WriteArguments.model_validate_json(arguments)
52
52
  except ValueError as e: # pragma: no cover - defensive
@@ -60,12 +60,12 @@ class WriteTool(ToolABC):
60
60
  output_text="<tool_use_error>Illegal operation on a directory. write</tool_use_error>",
61
61
  )
62
62
 
63
- file_tracker = get_current_file_tracker()
63
+ file_tracker = context.file_tracker
64
64
  exists = file_exists(file_path)
65
65
  tracked_status: model.FileStatus | None = None
66
66
 
67
67
  if exists:
68
- tracked_status = file_tracker.get(file_path) if file_tracker is not None else None
68
+ tracked_status = file_tracker.get(file_path)
69
69
  if tracked_status is None:
70
70
  return message.ToolResultMessage(
71
71
  status="error",
@@ -114,15 +114,14 @@ class WriteTool(ToolABC):
114
114
  except (OSError, UnicodeError) as e: # pragma: no cover
115
115
  return message.ToolResultMessage(status="error", output_text=f"<tool_use_error>{e}</tool_use_error>")
116
116
 
117
- if file_tracker is not None:
118
- with contextlib.suppress(Exception):
119
- existing = file_tracker.get(file_path)
120
- is_mem = existing.is_memory if existing else False
121
- file_tracker[file_path] = model.FileStatus(
122
- mtime=Path(file_path).stat().st_mtime,
123
- content_sha256=hash_text_sha256(args.content),
124
- is_memory=is_mem,
125
- )
117
+ with contextlib.suppress(Exception):
118
+ existing = file_tracker.get(file_path)
119
+ is_mem = existing.is_memory if existing else False
120
+ file_tracker[file_path] = model.FileStatus(
121
+ mtime=Path(file_path).stat().st_mtime,
122
+ content_sha256=hash_text_sha256(args.content),
123
+ is_memory=is_mem,
124
+ )
126
125
 
127
126
  # For markdown files, use MarkdownDocUIExtra to render content as markdown
128
127
  # Otherwise, build diff between previous and new content
@@ -2,6 +2,7 @@
2
2
 
3
3
  from typing import Any, ClassVar, cast
4
4
 
5
+ from klaude_code.core.tool.context import ToolContext
5
6
  from klaude_code.protocol import llm_param, message, tools
6
7
 
7
8
 
@@ -72,7 +73,9 @@ class ReportBackTool:
72
73
  )
73
74
 
74
75
  @classmethod
75
- async def call(cls, arguments: str) -> message.ToolResultMessage:
76
+ async def call(cls, arguments: str, context: ToolContext) -> message.ToolResultMessage:
77
+ del arguments
78
+ del context
76
79
  """Execute the report_back tool.
77
80
 
78
81
  The actual handling of report_back results is done by TurnExecutor.
@@ -11,9 +11,9 @@ from typing import Any
11
11
  from pydantic import BaseModel
12
12
 
13
13
  from klaude_code.const import BASH_DEFAULT_TIMEOUT_MS, BASH_TERMINATE_TIMEOUT_SEC
14
+ from klaude_code.core.tool.context import ToolContext
14
15
  from klaude_code.core.tool.shell.command_safety import is_safe_command
15
16
  from klaude_code.core.tool.tool_abc import ToolABC, load_desc
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
18
  from klaude_code.protocol import llm_param, message, model, tools
19
19
 
@@ -71,7 +71,7 @@ class BashTool(ToolABC):
71
71
  timeout_ms: int = BASH_DEFAULT_TIMEOUT_MS
72
72
 
73
73
  @classmethod
74
- async def call(cls, arguments: str) -> message.ToolResultMessage:
74
+ async def call(cls, arguments: str, context: ToolContext) -> message.ToolResultMessage:
75
75
  try:
76
76
  args = BashTool.BashArguments.model_validate_json(arguments)
77
77
  except ValueError as e:
@@ -79,10 +79,10 @@ class BashTool(ToolABC):
79
79
  status="error",
80
80
  output_text=f"Invalid arguments: {e}",
81
81
  )
82
- return await cls.call_with_args(args)
82
+ return await cls.call_with_args(args, context)
83
83
 
84
84
  @classmethod
85
- async def call_with_args(cls, args: BashArguments) -> message.ToolResultMessage:
85
+ async def call_with_args(cls, args: BashArguments, context: ToolContext) -> 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:
@@ -119,6 +119,8 @@ class BashTool(ToolABC):
119
119
  }
120
120
  )
121
121
 
122
+ file_tracker = context.file_tracker
123
+
122
124
  def _hash_file_content_sha256(file_path: str) -> str | None:
123
125
  try:
124
126
  suffix = Path(file_path).suffix.lower()
@@ -144,9 +146,6 @@ class BashTool(ToolABC):
144
146
  return os.path.abspath(os.path.join(base_dir, path))
145
147
 
146
148
  def _track_files_read(file_paths: list[str], *, base_dir: str) -> None:
147
- file_tracker = get_current_file_tracker()
148
- if file_tracker is None:
149
- return
150
149
  for p in file_paths:
151
150
  abs_path = _resolve_in_dir(base_dir, p)
152
151
  if not os.path.exists(abs_path) or os.path.isdir(abs_path):
@@ -168,10 +167,6 @@ class BashTool(ToolABC):
168
167
  _track_files_read(file_paths, base_dir=base_dir)
169
168
 
170
169
  def _track_mv(src_paths: list[str], dest_path: str, *, base_dir: str) -> None:
171
- file_tracker = get_current_file_tracker()
172
- if file_tracker is None:
173
- return
174
-
175
170
  abs_dest = _resolve_in_dir(base_dir, dest_path)
176
171
  dest_is_dir = os.path.isdir(abs_dest)
177
172
 
@@ -4,6 +4,7 @@ from pathlib import Path
4
4
 
5
5
  from pydantic import BaseModel
6
6
 
7
+ from klaude_code.core.tool.context import ToolContext
7
8
  from klaude_code.core.tool.tool_abc import ToolABC, load_desc
8
9
  from klaude_code.core.tool.tool_registry import register
9
10
  from klaude_code.protocol import llm_param, message, tools
@@ -55,7 +56,8 @@ class SkillTool(ToolABC):
55
56
  command: str
56
57
 
57
58
  @classmethod
58
- async def call(cls, arguments: str) -> message.ToolResultMessage:
59
+ async def call(cls, arguments: str, context: ToolContext) -> message.ToolResultMessage:
60
+ del context
59
61
  """Load and return full skill content."""
60
62
  try:
61
63
  args = cls.SkillArguments.model_validate_json(arguments)
@@ -10,8 +10,8 @@ import asyncio
10
10
  import json
11
11
  from typing import TYPE_CHECKING, Any, ClassVar, cast
12
12
 
13
+ from klaude_code.core.tool.context import ToolContext
13
14
  from klaude_code.core.tool.tool_abc import ToolABC, ToolConcurrencyPolicy, ToolMetadata
14
- from klaude_code.core.tool.tool_context import current_run_subtask_callback, current_sub_agent_resume_claims
15
15
  from klaude_code.protocol import llm_param, message, model
16
16
  from klaude_code.session.session import Session
17
17
 
@@ -52,7 +52,7 @@ class SubAgentTool(ToolABC):
52
52
  )
53
53
 
54
54
  @classmethod
55
- async def call(cls, arguments: str) -> message.ToolResultMessage:
55
+ async def call(cls, arguments: str, context: ToolContext) -> message.ToolResultMessage:
56
56
  profile = cls._profile
57
57
 
58
58
  try:
@@ -60,7 +60,7 @@ class SubAgentTool(ToolABC):
60
60
  except json.JSONDecodeError as e:
61
61
  return message.ToolResultMessage(status="error", output_text=f"Invalid JSON arguments: {e}")
62
62
 
63
- runner = current_run_subtask_callback.get()
63
+ runner = context.run_subtask
64
64
  if runner is None:
65
65
  return message.ToolResultMessage(status="error", output_text="No subtask runner available in this context")
66
66
 
@@ -76,9 +76,10 @@ class SubAgentTool(ToolABC):
76
76
  except ValueError as exc:
77
77
  return message.ToolResultMessage(status="error", output_text=str(exc))
78
78
 
79
- claims = current_sub_agent_resume_claims.get()
79
+ claims = context.sub_agent_resume_claims
80
80
  if claims is not None:
81
- if resume_session_id in claims:
81
+ ok = await claims.claim(resume_session_id)
82
+ if not ok:
82
83
  return message.ToolResultMessage(
83
84
  status="error",
84
85
  output_text=(
@@ -87,7 +88,6 @@ class SubAgentTool(ToolABC):
87
88
  "Merge into a single call or resume in a later turn."
88
89
  ),
89
90
  )
90
- claims.add(resume_session_id)
91
91
 
92
92
  generation = args.get("generation")
93
93
  generation_dict: dict[str, Any] | None = (
@@ -108,7 +108,8 @@ class SubAgentTool(ToolABC):
108
108
  resume=resume_session_id,
109
109
  output_schema=output_schema,
110
110
  generation=generation_dict,
111
- )
111
+ ),
112
+ context.record_sub_agent_session_id,
112
113
  )
113
114
  except asyncio.CancelledError:
114
115
  raise
@@ -2,8 +2,8 @@ from pathlib import Path
2
2
 
3
3
  from pydantic import BaseModel
4
4
 
5
+ from klaude_code.core.tool.context import ToolContext
5
6
  from klaude_code.core.tool.tool_abc import ToolABC, load_desc
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
8
  from klaude_code.protocol import llm_param, message, model, tools
9
9
 
@@ -76,7 +76,7 @@ class TodoWriteTool(ToolABC):
76
76
  )
77
77
 
78
78
  @classmethod
79
- async def call(cls, arguments: str) -> message.ToolResultMessage:
79
+ async def call(cls, arguments: str, context: ToolContext) -> message.ToolResultMessage:
80
80
  try:
81
81
  args = TodoWriteArguments.model_validate_json(arguments)
82
82
  except ValueError as e:
@@ -85,13 +85,7 @@ class TodoWriteTool(ToolABC):
85
85
  output_text=f"Invalid arguments: {e}",
86
86
  )
87
87
 
88
- # Get current todo context to store todos
89
- todo_context = get_current_todo_context()
90
- if todo_context is None:
91
- return message.ToolResultMessage(
92
- status="error",
93
- output_text="No active session found",
94
- )
88
+ todo_context = context.todo_context
95
89
 
96
90
  # Get current todos before updating (for comparison)
97
91
  old_todos = todo_context.get_todos()
@@ -6,8 +6,8 @@ from pathlib import Path
6
6
 
7
7
  from pydantic import BaseModel, field_validator
8
8
 
9
+ from klaude_code.core.tool.context import ToolContext
9
10
  from klaude_code.core.tool.tool_abc import ToolABC, load_desc
10
- from klaude_code.core.tool.tool_context import get_current_todo_context
11
11
  from klaude_code.core.tool.tool_registry import register
12
12
  from klaude_code.protocol import llm_param, message, model, tools
13
13
 
@@ -79,15 +79,13 @@ class UpdatePlanTool(ToolABC):
79
79
  )
80
80
 
81
81
  @classmethod
82
- async def call(cls, arguments: str) -> message.ToolResultMessage:
82
+ async def call(cls, arguments: str, context: ToolContext) -> message.ToolResultMessage:
83
83
  try:
84
84
  args = UpdatePlanArguments.model_validate_json(arguments)
85
85
  except ValueError as exc:
86
86
  return message.ToolResultMessage(status="error", output_text=f"Invalid arguments: {exc}")
87
87
 
88
- todo_context = get_current_todo_context()
89
- if todo_context is None:
90
- return message.ToolResultMessage(status="error", output_text="No active session found")
88
+ todo_context = context.todo_context
91
89
 
92
90
  new_todos = [model.TodoItem(content=item.step, status=item.status) for item in args.plan]
93
91
  old_todos = todo_context.get_todos()
@@ -4,6 +4,7 @@ from dataclasses import dataclass
4
4
  from enum import Enum
5
5
  from pathlib import Path
6
6
 
7
+ from klaude_code.core.tool.context import ToolContext
7
8
  from klaude_code.protocol import llm_param, message
8
9
 
9
10
 
@@ -27,7 +28,7 @@ class ToolABC(ABC):
27
28
 
28
29
  @classmethod
29
30
  @abstractmethod
30
- async def call(cls, arguments: str) -> message.ToolResultMessage:
31
+ async def call(cls, arguments: str, context: ToolContext) -> message.ToolResultMessage:
31
32
  raise NotImplementedError
32
33
 
33
34
 
@@ -3,8 +3,8 @@ from typing import TypeVar
3
3
 
4
4
  from klaude_code.core.tool.sub_agent_tool import SubAgentTool
5
5
  from klaude_code.core.tool.tool_abc import ToolABC
6
- from klaude_code.protocol import llm_param, tools
7
- from klaude_code.protocol.sub_agent import get_sub_agent_profile, iter_sub_agent_profiles, sub_agent_tool_names
6
+ from klaude_code.protocol import llm_param
7
+ from klaude_code.protocol.sub_agent import iter_sub_agent_profiles
8
8
 
9
9
  _REGISTRY: dict[str, type[ToolABC]] = {}
10
10
 
@@ -45,34 +45,3 @@ def get_tool_schemas(tool_names: list[str]) -> list[llm_param.ToolSchema]:
45
45
  def get_registry() -> dict[str, type[ToolABC]]:
46
46
  """Get the global tool registry."""
47
47
  return _REGISTRY
48
-
49
-
50
- def load_agent_tools(
51
- model_name: str, sub_agent_type: tools.SubAgentType | None = None, *, vanilla: bool = False
52
- ) -> list[llm_param.ToolSchema]:
53
- """Get tools for an agent based on model and agent type.
54
-
55
- Args:
56
- model_name: The model name.
57
- sub_agent_type: If None, returns main agent tools. Otherwise returns sub-agent tools.
58
- vanilla: If True, returns minimal vanilla tools (ignores sub_agent_type).
59
- """
60
- if vanilla:
61
- return get_tool_schemas([tools.BASH, tools.EDIT, tools.WRITE, tools.READ])
62
-
63
- if sub_agent_type is not None:
64
- profile = get_sub_agent_profile(sub_agent_type)
65
- return get_tool_schemas(list(profile.tool_set))
66
-
67
- # Main agent tools
68
- if "gpt-5" in model_name:
69
- tool_names = [tools.BASH, tools.READ, tools.APPLY_PATCH, tools.UPDATE_PLAN]
70
- elif "gemini-3" in model_name:
71
- tool_names = [tools.BASH, tools.READ, tools.EDIT, tools.WRITE]
72
- else:
73
- tool_names = [tools.BASH, tools.READ, tools.EDIT, tools.WRITE, tools.TODO_WRITE]
74
-
75
- tool_names.extend(sub_agent_tool_names(enabled_only=True, model_name=model_name))
76
- tool_names.extend([tools.SKILL, tools.MERMAID])
77
- # tool_names.extend([tools.MEMORY])
78
- return get_tool_schemas(tool_names)
@@ -3,9 +3,9 @@ from collections.abc import AsyncGenerator, Callable, Iterable, Sequence
3
3
  from dataclasses import dataclass
4
4
 
5
5
  from klaude_code.const import CANCEL_OUTPUT
6
+ from klaude_code.core.tool.context import ToolContext
6
7
  from klaude_code.core.tool.report_back_tool import ReportBackTool
7
8
  from klaude_code.core.tool.tool_abc import ToolABC, ToolConcurrencyPolicy
8
- from klaude_code.core.tool.tool_context import current_sub_agent_session_id_recorder
9
9
  from klaude_code.core.tool.truncation import truncate_tool_output
10
10
  from klaude_code.protocol import message, model, tools
11
11
 
@@ -18,19 +18,24 @@ class ToolCallRequest:
18
18
  arguments_json: str
19
19
 
20
20
 
21
- async def run_tool(tool_call: ToolCallRequest, registry: dict[str, type[ToolABC]]) -> message.ToolResultMessage:
21
+ async def run_tool(
22
+ tool_call: ToolCallRequest,
23
+ registry: dict[str, type[ToolABC]],
24
+ context: ToolContext,
25
+ ) -> message.ToolResultMessage:
22
26
  """Execute a tool call and return the result.
23
27
 
24
28
  Args:
25
29
  tool_call: The tool call to execute.
26
30
  registry: The tool registry mapping tool names to tool classes.
31
+ context: The explicit tool execution context.
27
32
 
28
33
  Returns:
29
34
  The result of the tool execution.
30
35
  """
31
36
  # Special handling for report_back tool (not registered in global registry)
32
37
  if tool_call.tool_name == tools.REPORT_BACK:
33
- tool_result = await ReportBackTool.call(tool_call.arguments_json)
38
+ tool_result = await ReportBackTool.call(tool_call.arguments_json, context)
34
39
  tool_result.call_id = tool_call.call_id
35
40
  tool_result.tool_name = tool_call.tool_name
36
41
  return tool_result
@@ -43,7 +48,7 @@ async def run_tool(tool_call: ToolCallRequest, registry: dict[str, type[ToolABC]
43
48
  tool_name=tool_call.tool_name,
44
49
  )
45
50
  try:
46
- tool_result = await registry[tool_call.tool_name].call(tool_call.arguments_json)
51
+ tool_result = await registry[tool_call.tool_name].call(tool_call.arguments_json, context)
47
52
  tool_result.call_id = tool_call.call_id
48
53
  tool_result.tool_name = tool_call.tool_name
49
54
  if tool_result.output_text:
@@ -109,9 +114,11 @@ class ToolExecutor:
109
114
  def __init__(
110
115
  self,
111
116
  *,
117
+ context: ToolContext,
112
118
  registry: dict[str, type[ToolABC]],
113
119
  append_history: Callable[[Sequence[message.HistoryEvent]], None],
114
120
  ) -> None:
121
+ self._context = context
115
122
  self._registry = registry
116
123
  self._append_history = append_history
117
124
 
@@ -268,15 +275,11 @@ class ToolExecutor:
268
275
 
269
276
  async def _run_single_tool_call(self, tool_call: ToolCallRequest) -> list[ToolExecutorEvent]:
270
277
  def _record_sub_agent_session_id(session_id: str) -> None:
271
- # Keep the first recorded id if multiple writes happen.
272
278
  if tool_call.call_id not in self._sub_agent_session_ids:
273
279
  self._sub_agent_session_ids[tool_call.call_id] = session_id
274
280
 
275
- recorder_token = current_sub_agent_session_id_recorder.set(_record_sub_agent_session_id)
276
- try:
277
- tool_result: message.ToolResultMessage = await run_tool(tool_call, self._registry)
278
- finally:
279
- current_sub_agent_session_id_recorder.reset(recorder_token)
281
+ call_context = self._context.with_record_sub_agent_session_id(_record_sub_agent_session_id)
282
+ tool_result: message.ToolResultMessage = await run_tool(tool_call, self._registry, call_context)
280
283
 
281
284
  self._append_history([tool_result])
282
285
 
@@ -8,6 +8,7 @@ from pathlib import Path
8
8
  from pydantic import BaseModel, Field
9
9
 
10
10
  from klaude_code.const import MERMAID_LIVE_PREFIX
11
+ from klaude_code.core.tool.context import ToolContext
11
12
  from klaude_code.core.tool.tool_abc import ToolABC, load_desc
12
13
  from klaude_code.core.tool.tool_registry import register
13
14
  from klaude_code.protocol import llm_param, message, model, tools
@@ -40,7 +41,8 @@ class MermaidTool(ToolABC):
40
41
  )
41
42
 
42
43
  @classmethod
43
- async def call(cls, arguments: str) -> message.ToolResultMessage:
44
+ async def call(cls, arguments: str, context: ToolContext) -> message.ToolResultMessage:
45
+ del context
44
46
  try:
45
47
  args = cls.MermaidArguments.model_validate_json(arguments)
46
48
  except Exception as exc: # pragma: no cover - defensive