klaude-code 2.0.1__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 (160) 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 +10 -1
  14. klaude_code/core/agent.py +9 -62
  15. klaude_code/core/agent_profile.py +284 -0
  16. klaude_code/core/executor.py +343 -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 +107 -155
  20. klaude_code/core/task.py +12 -20
  21. klaude_code/core/tool/__init__.py +5 -19
  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 +42 -44
  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/message.py +3 -11
  59. klaude_code/protocol/model.py +78 -9
  60. klaude_code/protocol/op.py +5 -0
  61. klaude_code/protocol/sub_agent/explore.py +0 -15
  62. klaude_code/protocol/sub_agent/task.py +1 -1
  63. klaude_code/protocol/sub_agent/web.py +1 -17
  64. klaude_code/protocol/tools.py +0 -1
  65. klaude_code/session/session.py +6 -5
  66. klaude_code/skill/assets/create-plan/SKILL.md +76 -0
  67. klaude_code/skill/loader.py +1 -1
  68. klaude_code/skill/system_skills.py +1 -1
  69. klaude_code/tui/__init__.py +8 -0
  70. klaude_code/{command → tui/command}/clear_cmd.py +2 -1
  71. klaude_code/{command → tui/command}/debug_cmd.py +4 -3
  72. klaude_code/{command → tui/command}/export_cmd.py +2 -1
  73. klaude_code/{command → tui/command}/export_online_cmd.py +6 -5
  74. klaude_code/{command → tui/command}/fork_session_cmd.py +10 -9
  75. klaude_code/{command → tui/command}/help_cmd.py +3 -2
  76. klaude_code/{command → tui/command}/model_cmd.py +5 -4
  77. klaude_code/{command → tui/command}/model_select.py +2 -2
  78. klaude_code/{command → tui/command}/prompt_command.py +4 -3
  79. klaude_code/{command → tui/command}/refresh_cmd.py +3 -1
  80. klaude_code/{command → tui/command}/registry.py +16 -6
  81. klaude_code/{command → tui/command}/release_notes_cmd.py +3 -2
  82. klaude_code/{command → tui/command}/resume_cmd.py +6 -5
  83. klaude_code/{command → tui/command}/status_cmd.py +4 -3
  84. klaude_code/{command → tui/command}/terminal_setup_cmd.py +4 -3
  85. klaude_code/{command → tui/command}/thinking_cmd.py +4 -3
  86. klaude_code/tui/commands.py +164 -0
  87. klaude_code/{ui/renderers → tui/components}/assistant.py +3 -3
  88. klaude_code/{ui/renderers → tui/components}/bash_syntax.py +2 -2
  89. klaude_code/{ui/renderers → tui/components}/common.py +1 -1
  90. klaude_code/tui/components/developer.py +231 -0
  91. klaude_code/{ui/renderers → tui/components}/diffs.py +2 -2
  92. klaude_code/{ui/renderers → tui/components}/errors.py +2 -2
  93. klaude_code/{ui/renderers → tui/components}/metadata.py +34 -21
  94. klaude_code/{ui → tui/components}/rich/markdown.py +78 -34
  95. klaude_code/{ui → tui/components}/rich/status.py +2 -2
  96. klaude_code/{ui → tui/components}/rich/theme.py +12 -5
  97. klaude_code/{ui/renderers → tui/components}/sub_agent.py +23 -43
  98. klaude_code/{ui/renderers → tui/components}/thinking.py +3 -3
  99. klaude_code/{ui/renderers → tui/components}/tools.py +11 -48
  100. klaude_code/{ui/renderers → tui/components}/user_input.py +3 -20
  101. klaude_code/tui/display.py +85 -0
  102. klaude_code/{ui/modes/repl → tui/input}/__init__.py +1 -1
  103. klaude_code/{ui/modes/repl → tui/input}/completers.py +1 -1
  104. klaude_code/{ui/modes/repl/input_prompt_toolkit.py → tui/input/prompt_toolkit.py} +11 -7
  105. klaude_code/tui/machine.py +606 -0
  106. klaude_code/tui/renderer.py +707 -0
  107. klaude_code/tui/runner.py +321 -0
  108. klaude_code/tui/terminal/__init__.py +56 -0
  109. klaude_code/{ui → tui}/terminal/color.py +1 -1
  110. klaude_code/{ui → tui}/terminal/control.py +1 -1
  111. klaude_code/{ui → tui}/terminal/notifier.py +1 -1
  112. klaude_code/{ui → tui}/terminal/selector.py +36 -17
  113. klaude_code/ui/__init__.py +6 -50
  114. klaude_code/ui/core/display.py +3 -3
  115. klaude_code/ui/core/input.py +2 -1
  116. klaude_code/ui/{modes/debug/display.py → debug_mode.py} +1 -1
  117. klaude_code/ui/{modes/exec/display.py → exec_mode.py} +1 -4
  118. klaude_code/ui/terminal/__init__.py +6 -54
  119. klaude_code/ui/terminal/title.py +31 -0
  120. klaude_code/update.py +163 -0
  121. {klaude_code-2.0.1.dist-info → klaude_code-2.1.0.dist-info}/METADATA +1 -1
  122. klaude_code-2.1.0.dist-info/RECORD +235 -0
  123. klaude_code/cli/runtime.py +0 -525
  124. klaude_code/core/prompt.py +0 -108
  125. klaude_code/core/tool/file/move_tool.md +0 -41
  126. klaude_code/core/tool/file/move_tool.py +0 -435
  127. klaude_code/core/tool/tool_context.py +0 -148
  128. klaude_code/protocol/events.py +0 -194
  129. klaude_code/skill/assets/dev-docs/SKILL.md +0 -108
  130. klaude_code/trace/__init__.py +0 -21
  131. klaude_code/ui/core/stage_manager.py +0 -48
  132. klaude_code/ui/modes/__init__.py +0 -1
  133. klaude_code/ui/modes/debug/__init__.py +0 -1
  134. klaude_code/ui/modes/exec/__init__.py +0 -1
  135. klaude_code/ui/modes/repl/display.py +0 -61
  136. klaude_code/ui/modes/repl/event_handler.py +0 -634
  137. klaude_code/ui/modes/repl/renderer.py +0 -463
  138. klaude_code/ui/renderers/developer.py +0 -215
  139. klaude_code/ui/utils/__init__.py +0 -1
  140. klaude_code-2.0.1.dist-info/RECORD +0 -229
  141. /klaude_code/{trace/log.py → log.py} +0 -0
  142. /klaude_code/{command → tui/command}/__init__.py +0 -0
  143. /klaude_code/{command → tui/command}/command_abc.py +0 -0
  144. /klaude_code/{command → tui/command}/prompt-commit.md +0 -0
  145. /klaude_code/{command → tui/command}/prompt-init.md +0 -0
  146. /klaude_code/{ui/renderers → tui/components}/__init__.py +0 -0
  147. /klaude_code/{ui/renderers → tui/components}/mermaid_viewer.py +0 -0
  148. /klaude_code/{ui → tui/components}/rich/__init__.py +0 -0
  149. /klaude_code/{ui → tui/components}/rich/cjk_wrap.py +0 -0
  150. /klaude_code/{ui → tui/components}/rich/code_panel.py +0 -0
  151. /klaude_code/{ui → tui/components}/rich/live.py +0 -0
  152. /klaude_code/{ui → tui/components}/rich/quote.py +0 -0
  153. /klaude_code/{ui → tui/components}/rich/searchable_text.py +0 -0
  154. /klaude_code/{ui/modes/repl → tui/input}/clipboard.py +0 -0
  155. /klaude_code/{ui/modes/repl → tui/input}/key_bindings.py +0 -0
  156. /klaude_code/{ui → tui}/terminal/image.py +0 -0
  157. /klaude_code/{ui → tui}/terminal/progress_bar.py +0 -0
  158. /klaude_code/ui/{utils/common.py → common.py} +0 -0
  159. {klaude_code-2.0.1.dist-info → klaude_code-2.1.0.dist-info}/WHEEL +0 -0
  160. {klaude_code-2.0.1.dist-info → klaude_code-2.1.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,84 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from collections.abc import Awaitable, Callable, MutableMapping
5
+ from dataclasses import dataclass, replace
6
+
7
+ from klaude_code.protocol import model
8
+ from klaude_code.protocol.sub_agent import SubAgentResult
9
+ from klaude_code.session.session import Session
10
+
11
+ type FileTracker = MutableMapping[str, model.FileStatus]
12
+
13
+ RunSubtask = Callable[[model.SubAgentState, Callable[[str], None] | None], Awaitable[SubAgentResult]]
14
+
15
+
16
+ @dataclass
17
+ class TodoContext:
18
+ """Todo access interface exposed to tools.
19
+
20
+ Tools can only read the current todo list and replace it with
21
+ a new list; they cannot access the full Session object.
22
+ """
23
+
24
+ get_todos: Callable[[], list[model.TodoItem]]
25
+ set_todos: Callable[[list[model.TodoItem]], None]
26
+
27
+
28
+ @dataclass
29
+ class SessionTodoStore:
30
+ """Adapter exposing session todos through an explicit interface."""
31
+
32
+ session: Session
33
+
34
+ def get(self) -> list[model.TodoItem]:
35
+ return self.session.todos
36
+
37
+ def set(self, todos: list[model.TodoItem]) -> None:
38
+ self.session.todos = todos
39
+
40
+
41
+ def build_todo_context(session: Session) -> TodoContext:
42
+ """Create a TodoContext backed by the given session."""
43
+
44
+ store = SessionTodoStore(session)
45
+ return TodoContext(get_todos=store.get, set_todos=store.set)
46
+
47
+
48
+ class SubAgentResumeClaims:
49
+ """Track sub-agent resume claims for a single turn.
50
+
51
+ Multiple concurrent sub-agent tool calls can attempt to resume the same
52
+ session id in a single model response. This class provides an atomic
53
+ `claim()` operation to reject duplicates.
54
+ """
55
+
56
+ def __init__(self) -> None:
57
+ self._claims: set[str] = set()
58
+ self._lock = asyncio.Lock()
59
+
60
+ async def claim(self, session_id: str) -> bool:
61
+ async with self._lock:
62
+ if session_id in self._claims:
63
+ return False
64
+ self._claims.add(session_id)
65
+ return True
66
+
67
+
68
+ @dataclass(frozen=True)
69
+ class ToolContext:
70
+ """Tool execution context.
71
+
72
+ This object is shallow-immutable: fields cannot be reassigned, but fields
73
+ may reference mutable objects (e.g., FileTracker).
74
+ """
75
+
76
+ file_tracker: FileTracker
77
+ todo_context: TodoContext
78
+ session_id: str
79
+ run_subtask: RunSubtask | None = None
80
+ sub_agent_resume_claims: SubAgentResumeClaims | None = None
81
+ record_sub_agent_session_id: Callable[[str], None] | None = None
82
+
83
+ def with_record_sub_agent_session_id(self, callback: Callable[[str], None] | None) -> ToolContext:
84
+ return replace(self, record_sub_agent_session_id=callback)
@@ -7,20 +7,20 @@ from pathlib import Path
7
7
 
8
8
  from pydantic import BaseModel
9
9
 
10
+ from klaude_code.core.tool.context import FileTracker, ToolContext
10
11
  from klaude_code.core.tool.file import apply_patch as apply_patch_module
11
12
  from klaude_code.core.tool.file._utils import hash_text_sha256
12
13
  from klaude_code.core.tool.file.diff_builder import build_structured_file_diff
13
14
  from klaude_code.core.tool.tool_abc import ToolABC, load_desc
14
- from klaude_code.core.tool.tool_context import get_current_file_tracker
15
15
  from klaude_code.core.tool.tool_registry import register
16
16
  from klaude_code.protocol import llm_param, message, model, tools
17
17
 
18
18
 
19
19
  class ApplyPatchHandler:
20
20
  @classmethod
21
- async def handle_apply_patch(cls, patch_text: str) -> message.ToolResultMessage:
21
+ async def handle_apply_patch(cls, patch_text: str, context: ToolContext) -> message.ToolResultMessage:
22
22
  try:
23
- output, ui_extra = await asyncio.to_thread(cls._apply_patch_in_thread, patch_text)
23
+ output, ui_extra = await asyncio.to_thread(cls._apply_patch_in_thread, patch_text, context.file_tracker)
24
24
  except apply_patch_module.DiffError as error:
25
25
  return message.ToolResultMessage(status="error", output_text=str(error))
26
26
  except Exception as error: # pragma: no cover # unexpected errors bubbled to tool result
@@ -32,14 +32,13 @@ class ApplyPatchHandler:
32
32
  )
33
33
 
34
34
  @staticmethod
35
- def _apply_patch_in_thread(patch_text: str) -> tuple[str, model.ToolResultUIExtra]:
35
+ def _apply_patch_in_thread(patch_text: str, file_tracker: FileTracker) -> tuple[str, model.ToolResultUIExtra]:
36
36
  ap = apply_patch_module
37
37
  normalized_start = patch_text.lstrip()
38
38
  if not normalized_start.startswith("*** Begin Patch"):
39
39
  raise ap.DiffError("apply_patch content must start with *** Begin Patch")
40
40
 
41
41
  workspace_root = os.path.realpath(os.getcwd())
42
- file_tracker = get_current_file_tracker()
43
42
 
44
43
  def resolve_path(path: str) -> str:
45
44
  candidate = os.path.realpath(path if os.path.isabs(path) else os.path.join(workspace_root, path))
@@ -89,15 +88,14 @@ class ApplyPatchHandler:
89
88
  with open(resolved, "w", encoding="utf-8") as handle:
90
89
  handle.write(content)
91
90
 
92
- if file_tracker is not None:
93
- with contextlib.suppress(Exception): # pragma: no cover - file tracker best-effort
94
- existing = file_tracker.get(resolved)
95
- is_mem = existing.is_memory if existing else False
96
- file_tracker[resolved] = model.FileStatus(
97
- mtime=Path(resolved).stat().st_mtime,
98
- content_sha256=hash_text_sha256(content),
99
- is_memory=is_mem,
100
- )
91
+ with contextlib.suppress(Exception): # pragma: no cover - file tracker best-effort
92
+ existing = file_tracker.get(resolved)
93
+ is_mem = existing.is_memory if existing else False
94
+ file_tracker[resolved] = model.FileStatus(
95
+ mtime=Path(resolved).stat().st_mtime,
96
+ content_sha256=hash_text_sha256(content),
97
+ is_memory=is_mem,
98
+ )
101
99
 
102
100
  def remove_fn(path: str) -> None:
103
101
  resolved = resolve_path(path)
@@ -107,9 +105,8 @@ class ApplyPatchHandler:
107
105
  raise ap.DiffError(f"Cannot delete directory: {path}")
108
106
  os.remove(resolved)
109
107
 
110
- if file_tracker is not None:
111
- with contextlib.suppress(Exception): # pragma: no cover - file tracker best-effort
112
- file_tracker.pop(resolved, None)
108
+ with contextlib.suppress(Exception): # pragma: no cover - file tracker best-effort
109
+ file_tracker.pop(resolved, None)
113
110
 
114
111
  ap.apply_commit(commit, write_fn, remove_fn)
115
112
 
@@ -172,13 +169,13 @@ class ApplyPatchTool(ToolABC):
172
169
  )
173
170
 
174
171
  @classmethod
175
- async def call(cls, arguments: str) -> message.ToolResultMessage:
172
+ async def call(cls, arguments: str, context: ToolContext) -> message.ToolResultMessage:
176
173
  try:
177
174
  args = cls.ApplyPatchArguments.model_validate_json(arguments)
178
175
  except ValueError as exc:
179
176
  return message.ToolResultMessage(status="error", output_text=f"Invalid arguments: {exc}")
180
- return await cls.call_with_args(args)
177
+ return await cls.call_with_args(args, context)
181
178
 
182
179
  @classmethod
183
- async def call_with_args(cls, args: ApplyPatchArguments) -> message.ToolResultMessage:
184
- return await ApplyPatchHandler.handle_apply_patch(args.patch)
180
+ async def call_with_args(cls, args: ApplyPatchArguments, context: ToolContext) -> message.ToolResultMessage:
181
+ return await ApplyPatchHandler.handle_apply_patch(args.patch, context)
@@ -8,10 +8,11 @@ from pathlib import Path
8
8
 
9
9
  from pydantic import BaseModel, Field
10
10
 
11
+ from klaude_code.const import DIFF_DEFAULT_CONTEXT_LINES
12
+ from klaude_code.core.tool.context import ToolContext
11
13
  from klaude_code.core.tool.file._utils import file_exists, hash_text_sha256, is_directory, read_text, write_text
12
14
  from klaude_code.core.tool.file.diff_builder import build_structured_diff
13
15
  from klaude_code.core.tool.tool_abc import ToolABC, load_desc
14
- from klaude_code.core.tool.tool_context import get_current_file_tracker
15
16
  from klaude_code.core.tool.tool_registry import register
16
17
  from klaude_code.protocol import llm_param, message, model, tools
17
18
 
@@ -85,7 +86,7 @@ class EditTool(ToolABC):
85
86
  return content.replace(old_string, new_string, 1)
86
87
 
87
88
  @classmethod
88
- async def call(cls, arguments: str) -> message.ToolResultMessage:
89
+ async def call(cls, arguments: str, context: ToolContext) -> message.ToolResultMessage:
89
90
  try:
90
91
  args = EditTool.EditArguments.model_validate_json(arguments)
91
92
  except ValueError as e: # pragma: no cover - defensive
@@ -110,7 +111,7 @@ class EditTool(ToolABC):
110
111
  )
111
112
 
112
113
  # FileTracker checks (only for editing existing files)
113
- file_tracker = get_current_file_tracker()
114
+ file_tracker = context.file_tracker
114
115
  tracked_status: model.FileStatus | None = None
115
116
  if not file_exists(file_path):
116
117
  # We require reading before editing
@@ -118,13 +119,12 @@ class EditTool(ToolABC):
118
119
  status="error",
119
120
  output_text=("File has not been read yet. Read it first before writing to it."),
120
121
  )
121
- if file_tracker is not None:
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
- )
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
+ )
128
128
 
129
129
  # Edit existing file: validate and apply
130
130
  try:
@@ -136,29 +136,28 @@ class EditTool(ToolABC):
136
136
  )
137
137
 
138
138
  # Re-check external modifications using content hash when available.
139
- if tracked_status is not None:
140
- if tracked_status.content_sha256 is not None:
141
- current_sha256 = hash_text_sha256(before)
142
- if current_sha256 != tracked_status.content_sha256:
143
- return message.ToolResultMessage(
144
- status="error",
145
- output_text=(
146
- "File has been modified externally. Either by user or a linter. Read it first before writing to it."
147
- ),
148
- )
149
- else:
150
- # Backward-compat: old sessions only stored mtime.
151
- try:
152
- current_mtime = Path(file_path).stat().st_mtime
153
- except OSError:
154
- current_mtime = tracked_status.mtime
155
- if current_mtime != tracked_status.mtime:
156
- return message.ToolResultMessage(
157
- status="error",
158
- output_text=(
159
- "File has been modified externally. Either by user or a linter. Read it first before writing to it."
160
- ),
161
- )
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
+ )
162
161
 
163
162
  err = cls.valid(
164
163
  content=before,
@@ -191,28 +190,27 @@ class EditTool(ToolABC):
191
190
  except (OSError, UnicodeError) as e: # pragma: no cover
192
191
  return message.ToolResultMessage(status="error", output_text=f"<tool_use_error>{e}</tool_use_error>")
193
192
 
194
- # Prepare UI extra: unified diff with 3 context lines
193
+ # Prepare UI extra: unified diff with default context lines
195
194
  diff_lines = list(
196
195
  difflib.unified_diff(
197
196
  before.splitlines(),
198
197
  after.splitlines(),
199
198
  fromfile=file_path,
200
199
  tofile=file_path,
201
- n=3,
200
+ n=DIFF_DEFAULT_CONTEXT_LINES,
202
201
  )
203
202
  )
204
203
  ui_extra = build_structured_diff(before, after, file_path=file_path)
205
204
 
206
205
  # Update tracker with new mtime and content hash
207
- if file_tracker is not None:
208
- with contextlib.suppress(Exception):
209
- existing = file_tracker.get(file_path)
210
- is_mem = existing.is_memory if existing else False
211
- file_tracker[file_path] = model.FileStatus(
212
- mtime=Path(file_path).stat().st_mtime,
213
- content_sha256=hash_text_sha256(after),
214
- is_memory=is_mem,
215
- )
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
+ )
216
214
 
217
215
  # Build output message
218
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()