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
@@ -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
 
@@ -71,10 +68,13 @@ def get_at_patterns_with_source(session: Session) -> list[AtPatternSource]:
71
68
  patterns.append(AtPatternSource(pattern=path_str, mentioned_in=None))
72
69
  break
73
70
 
74
- if isinstance(item, message.DeveloperMessage) and item.memory_mentioned:
75
- for memory_path, mentioned_patterns in item.memory_mentioned.items():
76
- for pattern in mentioned_patterns:
77
- patterns.append(AtPatternSource(pattern=pattern, mentioned_in=memory_path))
71
+ if isinstance(item, message.DeveloperMessage) and item.ui_extra:
72
+ for ui_item in item.ui_extra.items:
73
+ if not isinstance(ui_item, model.MemoryLoadedUIItem):
74
+ continue
75
+ for mem in ui_item.files:
76
+ for pattern in mem.mentioned_patterns:
77
+ patterns.append(AtPatternSource(pattern=pattern, mentioned_in=mem.path))
78
78
  return patterns
79
79
 
80
80
 
@@ -113,7 +113,8 @@ def _is_tracked_file_unchanged(session: Session, path: str) -> bool:
113
113
  async def _load_at_file_recursive(
114
114
  session: Session,
115
115
  pattern: str,
116
- at_files: dict[str, model.AtPatternParseResult],
116
+ at_ops: list[model.AtFileOp],
117
+ formatted_blocks: list[str],
117
118
  collected_images: list[message.ImageURLPart],
118
119
  visited: set[str],
119
120
  base_dir: Path | None = None,
@@ -127,53 +128,59 @@ async def _load_at_file_recursive(
127
128
  return
128
129
  visited.add(path_str)
129
130
 
130
- context_token = set_tool_context_from_session(session)
131
- try:
132
- if path.exists() and path.is_file():
133
- if _is_tracked_file_unchanged(session, path_str):
134
- return
135
- args = ReadTool.ReadArguments(file_path=path_str)
136
- tool_result = await ReadTool.call_with_args(args)
137
- images = [part for part in tool_result.parts if isinstance(part, message.ImageURLPart)]
138
- at_files[path_str] = model.AtPatternParseResult(
139
- path=path_str,
140
- tool_name=tools.READ,
141
- result=tool_result.output_text,
142
- tool_args=args.model_dump_json(exclude_none=True),
143
- operation="Read",
144
- mentioned_in=mentioned_in,
145
- )
146
- if images:
147
- collected_images.extend(images)
148
-
149
- # Recursively parse @ references from ReadTool output
150
- output = tool_result.output_text
151
- if "@" in output:
152
- for match in AT_FILE_PATTERN.finditer(output):
153
- nested = match.group("quoted") or match.group("plain")
154
- if nested:
155
- await _load_at_file_recursive(
156
- session,
157
- nested,
158
- at_files,
159
- collected_images,
160
- visited,
161
- base_dir=path.parent,
162
- mentioned_in=path_str,
163
- )
164
- elif path.exists() and path.is_dir():
165
- quoted_path = shlex.quote(path_str)
166
- args = BashTool.BashArguments(command=f"ls {quoted_path}")
167
- tool_result = await BashTool.call_with_args(args)
168
- at_files[path_str] = model.AtPatternParseResult(
169
- path=path_str + "/",
170
- tool_name=tools.BASH,
171
- result=tool_result.output_text,
172
- tool_args=args.model_dump_json(exclude_none=True),
173
- operation="List",
174
- )
175
- finally:
176
- reset_tool_context(context_token)
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}
147
+ Result of calling the {tools.READ} tool:
148
+ {tool_result.output_text}
149
+ """
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}
179
+ Result of calling the {tools.BASH} tool:
180
+ {tool_result.output_text}
181
+ """
182
+ )
183
+ at_ops.append(model.AtFileOp(operation="List", path=path_str + "/", mentioned_in=mentioned_in))
177
184
 
178
185
 
179
186
  async def at_file_reader_reminder(
@@ -184,7 +191,8 @@ async def at_file_reader_reminder(
184
191
  if not at_pattern_sources:
185
192
  return None
186
193
 
187
- at_files: dict[str, model.AtPatternParseResult] = {} # path -> content
194
+ at_ops: list[model.AtFileOp] = []
195
+ formatted_blocks: list[str] = []
188
196
  collected_images: list[message.ImageURLPart] = []
189
197
  visited: set[str] = set()
190
198
 
@@ -192,30 +200,23 @@ async def at_file_reader_reminder(
192
200
  await _load_at_file_recursive(
193
201
  session,
194
202
  source.pattern,
195
- at_files,
203
+ at_ops,
204
+ formatted_blocks,
196
205
  collected_images,
197
206
  visited,
198
207
  mentioned_in=source.mentioned_in,
199
208
  )
200
209
 
201
- if len(at_files) == 0:
210
+ if len(formatted_blocks) == 0:
202
211
  return None
203
212
 
204
- at_files_str = "\n\n".join(
205
- [
206
- f"""Called the {result.tool_name} tool with the following input: {result.tool_args}
207
- Result of calling the {result.tool_name} tool:
208
- {result.result}
209
- """
210
- for result in at_files.values()
211
- ]
212
- )
213
+ at_files_str = "\n\n".join(formatted_blocks)
213
214
  return message.DeveloperMessage(
214
215
  parts=message.parts_from_text_and_images(
215
216
  f"""<system-reminder>{at_files_str}\n</system-reminder>""",
216
217
  collected_images or None,
217
218
  ),
218
- at_files=list(at_files.values()),
219
+ ui_extra=model.DeveloperUIExtra(items=[model.AtFileOpsUIItem(ops=at_ops)]),
219
220
  )
220
221
 
221
222
 
@@ -299,7 +300,7 @@ Here are the existing contents of your todo list:
299
300
 
300
301
  {model.todo_list_str(session.todos)}</system-reminder>"""
301
302
  ),
302
- todo_use=True,
303
+ ui_extra=model.DeveloperUIExtra(items=[model.TodoReminderUIItem(reason="not_used_recently")]),
303
304
  )
304
305
 
305
306
  if session.need_todo_not_used_cooldown_counter > 0:
@@ -327,18 +328,20 @@ async def file_changed_externally_reminder(
327
328
  changed = current_mtime != status.mtime
328
329
 
329
330
  if changed:
330
- context_token = set_tool_context_from_session(session)
331
- try:
332
- tool_result = await ReadTool.call_with_args(
333
- ReadTool.ReadArguments(file_path=path)
334
- ) # This tool will update file tracker
335
- if tool_result.status == "success":
336
- images = [part for part in tool_result.parts if isinstance(part, message.ImageURLPart)]
337
- changed_files.append((path, tool_result.output_text, images or None))
338
- if images:
339
- collected_images.extend(images)
340
- finally:
341
- 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)
342
345
  except (
343
346
  FileNotFoundError,
344
347
  IsADirectoryError,
@@ -357,10 +360,12 @@ async def file_changed_externally_reminder(
357
360
  )
358
361
  return message.DeveloperMessage(
359
362
  parts=message.parts_from_text_and_images(
360
- f"""<system-reminder>{changed_files_str}""",
363
+ f"""<system-reminder>{changed_files_str}</system-reminder>""",
361
364
  collected_images or None,
362
365
  ),
363
- external_file_changes=[file_path for file_path, _, _ in changed_files],
366
+ ui_extra=model.DeveloperUIExtra(
367
+ items=[model.ExternalFileChangesUIItem(paths=[file_path for file_path, _, _ in changed_files])]
368
+ ),
364
369
  )
365
370
 
366
371
  return None
@@ -423,10 +428,8 @@ async def image_reminder(session: Session) -> message.DeveloperMessage | None:
423
428
  return None
424
429
 
425
430
  return message.DeveloperMessage(
426
- parts=message.text_parts_from_str(
427
- 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>"
428
- ),
429
- user_image_count=image_count,
431
+ parts=[],
432
+ ui_extra=model.DeveloperUIExtra(items=[model.UserImagesUIItem(count=image_count)]),
430
433
  )
431
434
 
432
435
 
@@ -456,7 +459,7 @@ async def skill_reminder(session: Session) -> message.DeveloperMessage | None:
456
459
 
457
460
  return message.DeveloperMessage(
458
461
  parts=message.text_parts_from_str(content),
459
- skill_name=skill.name,
462
+ ui_extra=model.DeveloperUIExtra(items=[model.SkillActivatedUIItem(name=skill.name)]),
460
463
  )
461
464
 
462
465
 
@@ -487,7 +490,7 @@ async def memory_reminder(session: Session) -> message.DeveloperMessage | None:
487
490
  path_str = str(memory_path)
488
491
  if memory_path.exists() and memory_path.is_file() and not _is_memory_loaded(session, path_str):
489
492
  try:
490
- text = memory_path.read_text()
493
+ text = memory_path.read_text(encoding="utf-8", errors="replace")
491
494
  _mark_memory_loaded(session, path_str)
492
495
  memories.append(Memory(path=path_str, instruction=instruction, content=text))
493
496
  except (PermissionError, UnicodeDecodeError, OSError):
@@ -496,32 +499,19 @@ async def memory_reminder(session: Session) -> message.DeveloperMessage | None:
496
499
  memories_str = "\n\n".join(
497
500
  [f"Contents of {memory.path} ({memory.instruction}):\n\n{memory.content}" for memory in memories]
498
501
  )
499
- # Build memory_mentioned: extract @ patterns from each memory's content
500
- memory_mentioned: dict[str, list[str]] = {}
501
- for memory in memories:
502
- patterns = _extract_at_patterns(memory.content)
503
- if patterns:
504
- memory_mentioned[memory.path] = patterns
505
-
502
+ loaded_files = [
503
+ model.MemoryFileLoaded(path=memory.path, mentioned_patterns=_extract_at_patterns(memory.content))
504
+ for memory in memories
505
+ ]
506
506
  return message.DeveloperMessage(
507
507
  parts=message.text_parts_from_str(
508
- f"""<system-reminder>As you answer the user's questions, you can use the following context:
508
+ f"""<system-reminder>
509
+ Loaded memory files. Follow these instructions. Do not mention them to the user unless explicitly asked.
509
510
 
510
- # claudeMd
511
- Codebase and user instructions are shown below. Be sure to adhere to these instructions. IMPORTANT: These instructions OVERRIDE any default behavior and you MUST follow them exactly as written.
512
511
  {memories_str}
513
-
514
- #important-instruction-reminders
515
- Do what has been asked; nothing more, nothing less.
516
- NEVER create files unless they're absolutely necessary for achieving your goal.
517
- ALWAYS prefer editing an existing file to creating a new one.
518
- NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User.
519
-
520
- IMPORTANT: this context may or may not be relevant to your tasks. You should not respond to this context unless it is highly relevant to your task.
521
512
  </system-reminder>"""
522
513
  ),
523
- memory_paths=[memory.path for memory in memories],
524
- memory_mentioned=memory_mentioned or None,
514
+ ui_extra=model.DeveloperUIExtra(items=[model.MemoryLoadedUIItem(files=loaded_files)]),
525
515
  )
526
516
  return None
527
517
 
@@ -572,7 +562,7 @@ async def last_path_memory_reminder(
572
562
  continue
573
563
  if mem_path.exists() and mem_path.is_file():
574
564
  try:
575
- text = mem_path.read_text()
565
+ text = mem_path.read_text(encoding="utf-8", errors="replace")
576
566
  except (PermissionError, UnicodeDecodeError, OSError):
577
567
  continue
578
568
  _mark_memory_loaded(session, mem_path_str)
@@ -589,20 +579,16 @@ async def last_path_memory_reminder(
589
579
  memories_str = "\n\n".join(
590
580
  [f"Contents of {memory.path} ({memory.instruction}):\n\n{memory.content}" for memory in memories]
591
581
  )
592
- # Build memory_mentioned: extract @ patterns from each memory's content
593
- memory_mentioned: dict[str, list[str]] = {}
594
- for memory in memories:
595
- patterns = _extract_at_patterns(memory.content)
596
- if patterns:
597
- memory_mentioned[memory.path] = patterns
598
-
582
+ loaded_files = [
583
+ model.MemoryFileLoaded(path=memory.path, mentioned_patterns=_extract_at_patterns(memory.content))
584
+ for memory in memories
585
+ ]
599
586
  return message.DeveloperMessage(
600
587
  parts=message.text_parts_from_str(
601
588
  f"""<system-reminder>{memories_str}
602
589
  </system-reminder>"""
603
590
  ),
604
- memory_paths=[memory.path for memory in memories],
605
- memory_mentioned=memory_mentioned or None,
591
+ ui_extra=model.DeveloperUIExtra(items=[model.MemoryLoadedUIItem(files=loaded_files)]),
606
592
  )
607
593
 
608
594
 
@@ -616,37 +602,3 @@ ALL_REMINDERS = [
616
602
  image_reminder,
617
603
  skill_reminder,
618
604
  ]
619
-
620
-
621
- def load_agent_reminders(
622
- model_name: str, sub_agent_type: str | None = None, *, vanilla: bool = False
623
- ) -> list[Reminder]:
624
- """Get reminders for an agent based on model and agent type.
625
-
626
- Args:
627
- model_name: The model name.
628
- sub_agent_type: If None, returns main agent reminders. Otherwise returns sub-agent reminders.
629
- vanilla: If True, returns minimal vanilla reminders (ignores sub_agent_type).
630
- """
631
- if vanilla:
632
- return [at_file_reader_reminder]
633
-
634
- reminders: list[Reminder] = []
635
-
636
- # Only main agent (not sub-agent) gets todo reminders, and not for GPT-5
637
- if sub_agent_type is None and "gpt-5" not in model_name:
638
- reminders.append(empty_todo_reminder)
639
- reminders.append(todo_not_used_recently_reminder)
640
-
641
- reminders.extend(
642
- [
643
- memory_reminder,
644
- at_file_reader_reminder,
645
- last_path_memory_reminder,
646
- file_changed_externally_reminder,
647
- image_reminder,
648
- skill_reminder,
649
- ]
650
- )
651
-
652
- 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,7 +1,7 @@
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
4
- from .file.move_tool import MoveTool
5
5
  from .file.read_tool import ReadTool
6
6
  from .file.write_tool import WriteTool
7
7
  from .report_back_tool import ReportBackTool
@@ -12,17 +12,7 @@ from .sub_agent_tool import SubAgentTool
12
12
  from .todo.todo_write_tool import TodoWriteTool
13
13
  from .todo.update_plan_tool import UpdatePlanTool
14
14
  from .tool_abc import ToolABC
15
- from .tool_context import (
16
- FileTracker,
17
- TodoContext,
18
- ToolContextToken,
19
- build_todo_context,
20
- current_run_subtask_callback,
21
- reset_tool_context,
22
- set_tool_context_from_session,
23
- tool_context,
24
- )
25
- from .tool_registry import get_registry, get_tool_schemas, load_agent_tools
15
+ from .tool_registry import get_registry, get_tool_schemas
26
16
  from .tool_runner import run_tool
27
17
  from .truncation import SimpleTruncationStrategy, TruncationStrategy, get_truncation_strategy, set_truncation_strategy
28
18
  from .web.mermaid_tool import MermaidTool
@@ -36,33 +26,29 @@ __all__ = [
36
26
  "EditTool",
37
27
  "FileTracker",
38
28
  "MermaidTool",
39
- "MoveTool",
40
29
  "ReadTool",
41
30
  "ReportBackTool",
31
+ "RunSubtask",
42
32
  "SafetyCheckResult",
43
33
  "SimpleTruncationStrategy",
44
34
  "SkillTool",
35
+ "SubAgentResumeClaims",
45
36
  "SubAgentTool",
46
37
  "TodoContext",
47
38
  "TodoWriteTool",
48
39
  "ToolABC",
49
- "ToolContextToken",
40
+ "ToolContext",
50
41
  "TruncationStrategy",
51
42
  "UpdatePlanTool",
52
43
  "WebFetchTool",
53
44
  "WebSearchTool",
54
45
  "WriteTool",
55
46
  "build_todo_context",
56
- "current_run_subtask_callback",
57
47
  "get_registry",
58
48
  "get_tool_schemas",
59
49
  "get_truncation_strategy",
60
50
  "is_safe_command",
61
- "load_agent_tools",
62
51
  "process_patch",
63
- "reset_tool_context",
64
52
  "run_tool",
65
- "set_tool_context_from_session",
66
53
  "set_truncation_strategy",
67
- "tool_context",
68
54
  ]