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
@@ -6,9 +6,9 @@ from klaude_code.config import Config
6
6
  from klaude_code.core.manager.llm_clients import LLMClients
7
7
  from klaude_code.llm.client import LLMClientABC
8
8
  from klaude_code.llm.registry import create_llm_client
9
+ from klaude_code.log import DebugType, log_debug
9
10
  from klaude_code.protocol.sub_agent import iter_sub_agent_profiles
10
11
  from klaude_code.protocol.tools import SubAgentType
11
- from klaude_code.trace import DebugType, log_debug
12
12
 
13
13
 
14
14
  def build_llm_clients(
@@ -9,15 +9,15 @@ from __future__ import annotations
9
9
 
10
10
  import asyncio
11
11
  import json
12
+ from collections.abc import Callable
12
13
 
13
- from klaude_code.core.agent import Agent, AgentProfile, ModelProfileProvider
14
+ from klaude_code.core.agent import Agent
15
+ from klaude_code.core.agent_profile import ModelProfileProvider
14
16
  from klaude_code.core.manager.llm_clients import LLMClients
15
- from klaude_code.core.tool import ReportBackTool
16
- from klaude_code.core.tool.tool_context import record_sub_agent_session_id
17
+ from klaude_code.log import DebugType, log_debug
17
18
  from klaude_code.protocol import events, message, model
18
19
  from klaude_code.protocol.sub_agent import SubAgentResult
19
20
  from klaude_code.session.session import Session
20
- from klaude_code.trace import DebugType, log_debug
21
21
 
22
22
 
23
23
  class SubAgentManager:
@@ -38,7 +38,13 @@ class SubAgentManager:
38
38
 
39
39
  await self._event_queue.put(event)
40
40
 
41
- async def run_sub_agent(self, parent_agent: Agent, state: model.SubAgentState) -> SubAgentResult:
41
+ async def run_sub_agent(
42
+ self,
43
+ parent_agent: Agent,
44
+ state: model.SubAgentState,
45
+ *,
46
+ record_session_id: Callable[[str], None] | None = None,
47
+ ) -> SubAgentResult:
42
48
  """Run a nested sub-agent task and return its result."""
43
49
 
44
50
  parent_session = parent_agent.session
@@ -77,9 +83,8 @@ class SubAgentManager:
77
83
  error=True,
78
84
  )
79
85
 
80
- # Expose the session id immediately so ToolExecutor.cancel() can attach
81
- # it to the synthesized cancellation ToolResult.
82
- record_sub_agent_session_id(child_session.id)
86
+ if record_session_id is not None:
87
+ record_session_id(child_session.id)
83
88
 
84
89
  # Update persisted sub-agent state to reflect the current invocation.
85
90
  child_session.sub_agent_state.sub_agent_desc = state.sub_agent_desc
@@ -92,33 +97,15 @@ class SubAgentManager:
92
97
  child_session = Session(work_dir=parent_session.work_dir)
93
98
  child_session.sub_agent_state = state
94
99
 
95
- # Expose the new session id immediately so ToolExecutor.cancel() can attach
96
- # it to the synthesized cancellation ToolResult.
97
- record_sub_agent_session_id(child_session.id)
100
+ if record_session_id is not None:
101
+ record_session_id(child_session.id)
98
102
 
99
103
  child_profile = self._model_profile_provider.build_profile(
100
104
  self._llm_clients.get_client(state.sub_agent_type),
101
105
  state.sub_agent_type,
106
+ output_schema=state.output_schema,
102
107
  )
103
108
 
104
- # Inject report_back tool if output_schema is provided
105
- if state.output_schema:
106
- report_back_tool_class = ReportBackTool.for_schema(state.output_schema)
107
- report_back_prompt = """\
108
-
109
- # Structured Output
110
- You have a `report_back` tool available. When you complete the task,\
111
- you MUST call `report_back` with the structured result matching the required schema.\
112
- Only the content passed to `report_back` will be returned to user.\
113
- """
114
- base_prompt = child_profile.system_prompt or ""
115
- child_profile = AgentProfile(
116
- llm_client=child_profile.llm_client,
117
- system_prompt=base_prompt + report_back_prompt,
118
- tools=[*child_profile.tools, report_back_tool_class.schema()],
119
- reminders=child_profile.reminders,
120
- )
121
-
122
109
  child_agent = Agent(session=child_session, profile=child_profile)
123
110
 
124
111
  log_debug(
@@ -1,22 +1,19 @@
1
1
  import hashlib
2
2
  import re
3
3
  import shlex
4
- from collections.abc import Awaitable, Callable
5
4
  from dataclasses import dataclass
6
5
  from pathlib import Path
7
6
 
8
7
  from pydantic import BaseModel
9
8
 
10
9
  from klaude_code.const import MEMORY_FILE_NAMES, REMINDER_COOLDOWN_TURNS, TODO_REMINDER_TOOL_CALL_THRESHOLD
11
- from klaude_code.core.tool import BashTool, ReadTool, reset_tool_context, set_tool_context_from_session
10
+ from klaude_code.core.tool import BashTool, ReadTool, build_todo_context
11
+ from klaude_code.core.tool.context import ToolContext
12
12
  from klaude_code.core.tool.file._utils import hash_text_sha256
13
13
  from klaude_code.protocol import message, model, tools
14
14
  from klaude_code.session import Session
15
15
  from klaude_code.skill import get_skill
16
16
 
17
- type Reminder = Callable[[Session], Awaitable[message.DeveloperMessage | None]]
18
-
19
-
20
17
  # Match @ preceded by whitespace, start of line, or → (ReadTool line number arrow)
21
18
  AT_FILE_PATTERN = re.compile(r'(?:(?<!\S)|(?<=\u2192))@("(?P<quoted>[^\"]+)"|(?P<plain>\S+))')
22
19
 
@@ -131,57 +128,59 @@ async def _load_at_file_recursive(
131
128
  return
132
129
  visited.add(path_str)
133
130
 
134
- context_token = set_tool_context_from_session(session)
135
- try:
136
- if path.exists() and path.is_file():
137
- if _is_tracked_file_unchanged(session, path_str):
138
- return
139
- args = ReadTool.ReadArguments(file_path=path_str)
140
- tool_result = await ReadTool.call_with_args(args)
141
- images = [part for part in tool_result.parts if isinstance(part, message.ImageURLPart)]
142
-
143
- tool_args = args.model_dump_json(exclude_none=True)
144
- formatted_blocks.append(
145
- f"""Called the {tools.READ} tool with the following input: {tool_args}
131
+ tool_context = ToolContext(
132
+ file_tracker=session.file_tracker,
133
+ todo_context=build_todo_context(session),
134
+ session_id=session.id,
135
+ )
136
+
137
+ if path.exists() and path.is_file():
138
+ if _is_tracked_file_unchanged(session, path_str):
139
+ return
140
+ args = ReadTool.ReadArguments(file_path=path_str)
141
+ tool_result = await ReadTool.call_with_args(args, tool_context)
142
+ images = [part for part in tool_result.parts if isinstance(part, message.ImageURLPart)]
143
+
144
+ tool_args = args.model_dump_json(exclude_none=True)
145
+ formatted_blocks.append(
146
+ f"""Called the {tools.READ} tool with the following input: {tool_args}
146
147
  Result of calling the {tools.READ} tool:
147
148
  {tool_result.output_text}
148
149
  """
149
- )
150
- at_ops.append(model.AtFileOp(operation="Read", path=path_str, mentioned_in=mentioned_in))
151
- if images:
152
- collected_images.extend(images)
153
-
154
- # Recursively parse @ references from ReadTool output
155
- output = tool_result.output_text
156
- if "@" in output:
157
- for match in AT_FILE_PATTERN.finditer(output):
158
- nested = match.group("quoted") or match.group("plain")
159
- if nested:
160
- await _load_at_file_recursive(
161
- session,
162
- nested,
163
- at_ops,
164
- formatted_blocks,
165
- collected_images,
166
- visited,
167
- base_dir=path.parent,
168
- mentioned_in=path_str,
169
- )
170
- elif path.exists() and path.is_dir():
171
- quoted_path = shlex.quote(path_str)
172
- args = BashTool.BashArguments(command=f"ls {quoted_path}")
173
- tool_result = await BashTool.call_with_args(args)
174
-
175
- tool_args = args.model_dump_json(exclude_none=True)
176
- formatted_blocks.append(
177
- f"""Called the {tools.BASH} tool with the following input: {tool_args}
150
+ )
151
+ at_ops.append(model.AtFileOp(operation="Read", path=path_str, mentioned_in=mentioned_in))
152
+ if images:
153
+ collected_images.extend(images)
154
+
155
+ # Recursively parse @ references from ReadTool output
156
+ output = tool_result.output_text
157
+ if "@" in output:
158
+ for match in AT_FILE_PATTERN.finditer(output):
159
+ nested = match.group("quoted") or match.group("plain")
160
+ if nested:
161
+ await _load_at_file_recursive(
162
+ session,
163
+ nested,
164
+ at_ops,
165
+ formatted_blocks,
166
+ collected_images,
167
+ visited,
168
+ base_dir=path.parent,
169
+ mentioned_in=path_str,
170
+ )
171
+ elif path.exists() and path.is_dir():
172
+ quoted_path = shlex.quote(path_str)
173
+ args = BashTool.BashArguments(command=f"ls {quoted_path}")
174
+ tool_result = await BashTool.call_with_args(args, tool_context)
175
+
176
+ tool_args = args.model_dump_json(exclude_none=True)
177
+ formatted_blocks.append(
178
+ f"""Called the {tools.BASH} tool with the following input: {tool_args}
178
179
  Result of calling the {tools.BASH} tool:
179
180
  {tool_result.output_text}
180
181
  """
181
- )
182
- at_ops.append(model.AtFileOp(operation="List", path=path_str + "/", mentioned_in=mentioned_in))
183
- finally:
184
- reset_tool_context(context_token)
182
+ )
183
+ at_ops.append(model.AtFileOp(operation="List", path=path_str + "/", mentioned_in=mentioned_in))
185
184
 
186
185
 
187
186
  async def at_file_reader_reminder(
@@ -329,18 +328,20 @@ async def file_changed_externally_reminder(
329
328
  changed = current_mtime != status.mtime
330
329
 
331
330
  if changed:
332
- context_token = set_tool_context_from_session(session)
333
- try:
334
- tool_result = await ReadTool.call_with_args(
335
- ReadTool.ReadArguments(file_path=path)
336
- ) # This tool will update file tracker
337
- if tool_result.status == "success":
338
- images = [part for part in tool_result.parts if isinstance(part, message.ImageURLPart)]
339
- changed_files.append((path, tool_result.output_text, images or None))
340
- if images:
341
- collected_images.extend(images)
342
- finally:
343
- reset_tool_context(context_token)
331
+ tool_context = ToolContext(
332
+ file_tracker=session.file_tracker,
333
+ todo_context=build_todo_context(session),
334
+ session_id=session.id,
335
+ )
336
+ tool_result = await ReadTool.call_with_args(
337
+ ReadTool.ReadArguments(file_path=path),
338
+ tool_context,
339
+ ) # This tool will update file tracker
340
+ if tool_result.status == "success":
341
+ images = [part for part in tool_result.parts if isinstance(part, message.ImageURLPart)]
342
+ changed_files.append((path, tool_result.output_text, images or None))
343
+ if images:
344
+ collected_images.extend(images)
344
345
  except (
345
346
  FileNotFoundError,
346
347
  IsADirectoryError,
@@ -427,9 +428,7 @@ async def image_reminder(session: Session) -> message.DeveloperMessage | None:
427
428
  return None
428
429
 
429
430
  return message.DeveloperMessage(
430
- parts=message.text_parts_from_str(
431
- 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>"
432
- ),
431
+ parts=[],
433
432
  ui_extra=model.DeveloperUIExtra(items=[model.UserImagesUIItem(count=image_count)]),
434
433
  )
435
434
 
@@ -603,37 +602,3 @@ ALL_REMINDERS = [
603
602
  image_reminder,
604
603
  skill_reminder,
605
604
  ]
606
-
607
-
608
- def load_agent_reminders(
609
- model_name: str, sub_agent_type: str | None = None, *, vanilla: bool = False
610
- ) -> list[Reminder]:
611
- """Get reminders for an agent based on model and agent type.
612
-
613
- Args:
614
- model_name: The model name.
615
- sub_agent_type: If None, returns main agent reminders. Otherwise returns sub-agent reminders.
616
- vanilla: If True, returns minimal vanilla reminders (ignores sub_agent_type).
617
- """
618
- if vanilla:
619
- return [at_file_reader_reminder]
620
-
621
- reminders: list[Reminder] = []
622
-
623
- # Only main agent (not sub-agent) gets todo reminders, and not for GPT-5
624
- if sub_agent_type is None and "gpt-5" not in model_name:
625
- reminders.append(empty_todo_reminder)
626
- reminders.append(todo_not_used_recently_reminder)
627
-
628
- reminders.extend(
629
- [
630
- memory_reminder,
631
- at_file_reader_reminder,
632
- last_path_memory_reminder,
633
- file_changed_externally_reminder,
634
- image_reminder,
635
- skill_reminder,
636
- ]
637
- )
638
-
639
- return reminders
klaude_code/core/task.py CHANGED
@@ -4,17 +4,14 @@ import asyncio
4
4
  import time
5
5
  from collections.abc import AsyncGenerator, Callable, Sequence
6
6
  from dataclasses import dataclass
7
- from typing import TYPE_CHECKING
8
7
 
9
8
  from klaude_code.const import INITIAL_RETRY_DELAY_S, MAX_FAILED_TURN_RETRIES, MAX_RETRY_DELAY_S
10
- from klaude_code.core.reminders import Reminder
9
+ from klaude_code.core.agent_profile import AgentProfile, Reminder
11
10
  from klaude_code.core.tool import FileTracker, TodoContext, ToolABC
11
+ from klaude_code.core.tool.context import RunSubtask
12
12
  from klaude_code.core.turn import TurnError, TurnExecutionContext, TurnExecutor
13
+ from klaude_code.log import DebugType, log_debug
13
14
  from klaude_code.protocol import events, message, model
14
- from klaude_code.trace import DebugType, log_debug
15
-
16
- if TYPE_CHECKING:
17
- from klaude_code.core.agent import AgentProfile
18
15
 
19
16
 
20
17
  class MetadataAccumulator:
@@ -101,6 +98,7 @@ class SessionContext:
101
98
  append_history: Callable[[Sequence[message.HistoryEvent]], None]
102
99
  file_tracker: FileTracker
103
100
  todo_context: TodoContext
101
+ run_subtask: RunSubtask | None
104
102
 
105
103
 
106
104
  @dataclass
@@ -189,26 +187,20 @@ class TaskExecutor:
189
187
  self._current_turn = turn
190
188
 
191
189
  try:
192
- async for turn_event in turn.run():
193
- match turn_event:
194
- case events.AssistantMessageEvent() as am:
190
+ async for e in turn.run():
191
+ match e:
192
+ case events.ResponseCompleteEvent() as am:
195
193
  yield am
196
- case events.ResponseMetadataEvent() as e:
197
- metadata_accumulator.add(e.metadata)
198
- # Emit context usage event if available
199
- context_percent = e.metadata.context_usage_percent
200
- if context_percent is not None:
201
- yield events.ContextUsageEvent(
202
- session_id=session_ctx.session_id,
203
- context_percent=context_percent,
204
- )
194
+ case events.UsageEvent() as e:
195
+ metadata_accumulator.add(e.usage)
196
+ yield e
205
197
  case events.ToolResultEvent() as e:
206
198
  # Collect sub-agent task metadata from tool results
207
199
  if e.task_metadata is not None:
208
200
  metadata_accumulator.add_sub_agent_metadata(e.task_metadata)
209
- yield turn_event
201
+ yield e
210
202
  case _:
211
- yield turn_event
203
+ yield e
212
204
 
213
205
  turn_succeeded = True
214
206
  break
@@ -1,3 +1,4 @@
1
+ from .context import FileTracker, RunSubtask, SubAgentResumeClaims, TodoContext, ToolContext, build_todo_context
1
2
  from .file.apply_patch import DiffError, process_patch
2
3
  from .file.apply_patch_tool import ApplyPatchTool
3
4
  from .file.edit_tool import EditTool
@@ -11,17 +12,7 @@ from .sub_agent_tool import SubAgentTool
11
12
  from .todo.todo_write_tool import TodoWriteTool
12
13
  from .todo.update_plan_tool import UpdatePlanTool
13
14
  from .tool_abc import ToolABC
14
- from .tool_context import (
15
- FileTracker,
16
- TodoContext,
17
- ToolContextToken,
18
- build_todo_context,
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
15
+ from .tool_registry import get_registry, get_tool_schemas
25
16
  from .tool_runner import run_tool
26
17
  from .truncation import SimpleTruncationStrategy, TruncationStrategy, get_truncation_strategy, set_truncation_strategy
27
18
  from .web.mermaid_tool import MermaidTool
@@ -37,30 +28,27 @@ __all__ = [
37
28
  "MermaidTool",
38
29
  "ReadTool",
39
30
  "ReportBackTool",
31
+ "RunSubtask",
40
32
  "SafetyCheckResult",
41
33
  "SimpleTruncationStrategy",
42
34
  "SkillTool",
35
+ "SubAgentResumeClaims",
43
36
  "SubAgentTool",
44
37
  "TodoContext",
45
38
  "TodoWriteTool",
46
39
  "ToolABC",
47
- "ToolContextToken",
40
+ "ToolContext",
48
41
  "TruncationStrategy",
49
42
  "UpdatePlanTool",
50
43
  "WebFetchTool",
51
44
  "WebSearchTool",
52
45
  "WriteTool",
53
46
  "build_todo_context",
54
- "current_run_subtask_callback",
55
47
  "get_registry",
56
48
  "get_tool_schemas",
57
49
  "get_truncation_strategy",
58
50
  "is_safe_command",
59
- "load_agent_tools",
60
51
  "process_patch",
61
- "reset_tool_context",
62
52
  "run_tool",
63
- "set_tool_context_from_session",
64
53
  "set_truncation_strategy",
65
- "tool_context",
66
54
  ]
@@ -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)