klaude-code 1.2.9__py3-none-any.whl → 1.2.11__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 (69) hide show
  1. klaude_code/cli/main.py +11 -5
  2. klaude_code/cli/runtime.py +21 -21
  3. klaude_code/command/__init__.py +68 -23
  4. klaude_code/command/clear_cmd.py +6 -2
  5. klaude_code/command/command_abc.py +5 -2
  6. klaude_code/command/diff_cmd.py +5 -2
  7. klaude_code/command/export_cmd.py +7 -4
  8. klaude_code/command/help_cmd.py +6 -2
  9. klaude_code/command/model_cmd.py +5 -2
  10. klaude_code/command/prompt_command.py +8 -3
  11. klaude_code/command/refresh_cmd.py +6 -2
  12. klaude_code/command/registry.py +17 -5
  13. klaude_code/command/release_notes_cmd.py +5 -2
  14. klaude_code/command/status_cmd.py +8 -4
  15. klaude_code/command/terminal_setup_cmd.py +7 -4
  16. klaude_code/const/__init__.py +1 -1
  17. klaude_code/core/agent.py +62 -9
  18. klaude_code/core/executor.py +1 -4
  19. klaude_code/core/manager/agent_manager.py +19 -14
  20. klaude_code/core/manager/llm_clients.py +47 -22
  21. klaude_code/core/manager/llm_clients_builder.py +22 -13
  22. klaude_code/core/manager/sub_agent_manager.py +1 -1
  23. klaude_code/core/prompt.py +4 -4
  24. klaude_code/core/prompts/prompt-claude-code.md +1 -12
  25. klaude_code/core/prompts/prompt-minimal.md +12 -0
  26. klaude_code/core/reminders.py +0 -3
  27. klaude_code/core/task.py +6 -2
  28. klaude_code/core/tool/file/_utils.py +30 -0
  29. klaude_code/core/tool/file/edit_tool.py +5 -30
  30. klaude_code/core/tool/file/multi_edit_tool.py +6 -31
  31. klaude_code/core/tool/file/read_tool.py +6 -18
  32. klaude_code/core/tool/file/write_tool.py +5 -30
  33. klaude_code/core/tool/memory/__init__.py +5 -0
  34. klaude_code/core/tool/memory/memory_tool.md +4 -0
  35. klaude_code/core/tool/memory/skill_loader.py +3 -2
  36. klaude_code/core/tool/memory/skill_tool.py +13 -0
  37. klaude_code/core/tool/todo/todo_write_tool.md +0 -157
  38. klaude_code/core/tool/todo/todo_write_tool_raw.md +182 -0
  39. klaude_code/core/tool/tool_registry.py +3 -4
  40. klaude_code/llm/__init__.py +2 -12
  41. klaude_code/llm/anthropic/client.py +2 -1
  42. klaude_code/llm/client.py +2 -2
  43. klaude_code/llm/codex/client.py +1 -1
  44. klaude_code/llm/openai_compatible/client.py +3 -2
  45. klaude_code/llm/openrouter/client.py +3 -3
  46. klaude_code/llm/registry.py +33 -7
  47. klaude_code/llm/responses/client.py +2 -1
  48. klaude_code/llm/responses/input.py +1 -1
  49. klaude_code/llm/usage.py +17 -8
  50. klaude_code/protocol/model.py +15 -7
  51. klaude_code/protocol/op.py +5 -1
  52. klaude_code/protocol/sub_agent.py +1 -0
  53. klaude_code/session/export.py +16 -6
  54. klaude_code/session/session.py +10 -4
  55. klaude_code/session/templates/export_session.html +155 -0
  56. klaude_code/ui/core/input.py +1 -1
  57. klaude_code/ui/modes/repl/clipboard.py +5 -5
  58. klaude_code/ui/modes/repl/event_handler.py +1 -5
  59. klaude_code/ui/modes/repl/input_prompt_toolkit.py +3 -34
  60. klaude_code/ui/renderers/metadata.py +22 -1
  61. klaude_code/ui/renderers/tools.py +13 -2
  62. klaude_code/ui/rich/markdown.py +4 -1
  63. klaude_code/ui/terminal/__init__.py +55 -0
  64. klaude_code/ui/terminal/control.py +2 -2
  65. klaude_code/version.py +3 -3
  66. {klaude_code-1.2.9.dist-info → klaude_code-1.2.11.dist-info}/METADATA +1 -4
  67. {klaude_code-1.2.9.dist-info → klaude_code-1.2.11.dist-info}/RECORD +69 -66
  68. {klaude_code-1.2.9.dist-info → klaude_code-1.2.11.dist-info}/WHEEL +0 -0
  69. {klaude_code-1.2.9.dist-info → klaude_code-1.2.11.dist-info}/entry_points.txt +0 -0
klaude_code/core/agent.py CHANGED
@@ -1,8 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
- from collections.abc import AsyncGenerator, Iterable
3
+ from collections.abc import AsyncGenerator, Callable, Iterable
4
4
  from dataclasses import dataclass
5
- from typing import Protocol
5
+ from typing import TYPE_CHECKING, Protocol
6
6
 
7
7
  from klaude_code.core.prompt import get_system_prompt as load_system_prompt
8
8
  from klaude_code.core.reminders import Reminder, load_agent_reminders
@@ -14,21 +14,38 @@ from klaude_code.protocol.model import UserInputPayload
14
14
  from klaude_code.session import Session
15
15
  from klaude_code.trace import DebugType, log_debug
16
16
 
17
+ if TYPE_CHECKING:
18
+ from klaude_code.core.manager.llm_clients import LLMClients
19
+
17
20
 
18
21
  @dataclass(frozen=True)
19
22
  class AgentProfile:
20
23
  """Encapsulates the active LLM client plus prompts/tools/reminders."""
21
24
 
22
- llm_client: LLMClientABC
25
+ llm_client_factory: Callable[[], LLMClientABC]
23
26
  system_prompt: str | None
24
27
  tools: list[llm_param.ToolSchema]
25
28
  reminders: list[Reminder]
26
29
 
30
+ _llm_client: LLMClientABC | None = None
31
+
32
+ @property
33
+ def llm_client(self) -> LLMClientABC:
34
+ if self._llm_client is None:
35
+ object.__setattr__(self, "_llm_client", self.llm_client_factory())
36
+ return self._llm_client # type: ignore[return-value]
37
+
27
38
 
28
39
  class ModelProfileProvider(Protocol):
29
40
  """Strategy interface for constructing agent profiles."""
30
41
 
31
42
  def build_profile(
43
+ self,
44
+ llm_clients: LLMClients,
45
+ sub_agent_type: tools.SubAgentType | None = None,
46
+ ) -> AgentProfile: ...
47
+
48
+ def build_profile_eager(
32
49
  self,
33
50
  llm_client: LLMClientABC,
34
51
  sub_agent_type: tools.SubAgentType | None = None,
@@ -39,13 +56,26 @@ class DefaultModelProfileProvider(ModelProfileProvider):
39
56
  """Default provider backed by global prompts/tool/reminder registries."""
40
57
 
41
58
  def build_profile(
59
+ self,
60
+ llm_clients: LLMClients,
61
+ sub_agent_type: tools.SubAgentType | None = None,
62
+ ) -> AgentProfile:
63
+ model_name = llm_clients.main_model_name
64
+ return AgentProfile(
65
+ llm_client_factory=lambda: llm_clients.main,
66
+ system_prompt=load_system_prompt(model_name, sub_agent_type),
67
+ tools=load_agent_tools(model_name, sub_agent_type),
68
+ reminders=load_agent_reminders(model_name, sub_agent_type),
69
+ )
70
+
71
+ def build_profile_eager(
42
72
  self,
43
73
  llm_client: LLMClientABC,
44
74
  sub_agent_type: tools.SubAgentType | None = None,
45
75
  ) -> AgentProfile:
46
76
  model_name = llm_client.model_name
47
77
  return AgentProfile(
48
- llm_client=llm_client,
78
+ llm_client_factory=lambda: llm_client,
49
79
  system_prompt=load_system_prompt(model_name, sub_agent_type),
50
80
  tools=load_agent_tools(model_name, sub_agent_type),
51
81
  reminders=load_agent_reminders(model_name, sub_agent_type),
@@ -56,13 +86,26 @@ class VanillaModelProfileProvider(ModelProfileProvider):
56
86
  """Provider that strips prompts, reminders, and tools for vanilla mode."""
57
87
 
58
88
  def build_profile(
89
+ self,
90
+ llm_clients: LLMClients,
91
+ sub_agent_type: tools.SubAgentType | None = None,
92
+ ) -> AgentProfile:
93
+ model_name = llm_clients.main_model_name
94
+ return AgentProfile(
95
+ llm_client_factory=lambda: llm_clients.main,
96
+ system_prompt=None,
97
+ tools=load_agent_tools(model_name, vanilla=True),
98
+ reminders=load_agent_reminders(model_name, vanilla=True),
99
+ )
100
+
101
+ def build_profile_eager(
59
102
  self,
60
103
  llm_client: LLMClientABC,
61
104
  sub_agent_type: tools.SubAgentType | None = None,
62
105
  ) -> AgentProfile:
63
106
  model_name = llm_client.model_name
64
107
  return AgentProfile(
65
- llm_client=llm_client,
108
+ llm_client_factory=lambda: llm_client,
66
109
  system_prompt=None,
67
110
  tools=load_agent_tools(model_name, vanilla=True),
68
111
  reminders=load_agent_reminders(model_name, vanilla=True),
@@ -74,12 +117,14 @@ class Agent:
74
117
  self,
75
118
  session: Session,
76
119
  profile: AgentProfile,
120
+ model_name: str | None = None,
77
121
  ):
78
122
  self.session: Session = session
79
123
  self.profile: AgentProfile = profile
80
124
  self._current_task: TaskExecutor | None = None
81
- if not self.session.model_name:
82
- self.session.model_name = profile.llm_client.model_name
125
+ self._prev_context_token: int = 0 # Track context size from previous task for delta calculation
126
+ if not self.session.model_name and model_name:
127
+ self.session.model_name = model_name
83
128
 
84
129
  def cancel(self) -> Iterable[events.Event]:
85
130
  """Handle agent cancellation and persist an interrupt marker and tool cancellations.
@@ -125,6 +170,12 @@ class Agent:
125
170
 
126
171
  try:
127
172
  async for event in task.run(user_input):
173
+ # Compute context_delta for TaskMetadataEvent
174
+ if isinstance(event, events.TaskMetadataEvent):
175
+ usage = event.metadata.main.usage
176
+ if usage is not None and usage.context_token is not None:
177
+ usage.context_delta = usage.context_token - self._prev_context_token
178
+ self._prev_context_token = usage.context_token
128
179
  yield event
129
180
  finally:
130
181
  self._current_task = None
@@ -148,11 +199,13 @@ class Agent:
148
199
  self.session.append_history([item])
149
200
  yield events.DeveloperMessageEvent(session_id=self.session.id, item=item)
150
201
 
151
- def set_model_profile(self, profile: AgentProfile) -> None:
202
+ def set_model_profile(self, profile: AgentProfile, model_name: str | None = None) -> None:
152
203
  """Apply a fully constructed profile to the agent."""
153
204
 
154
205
  self.profile = profile
155
- if not self.session.model_name:
206
+ if model_name:
207
+ self.session.model_name = model_name
208
+ elif not self.session.model_name:
156
209
  self.session.model_name = profile.llm_client.model_name
157
210
 
158
211
  def get_llm_client(self) -> LLMClientABC:
@@ -114,9 +114,6 @@ class ExecutorContext:
114
114
 
115
115
  async def handle_init_agent(self, operation: op.InitAgentOperation) -> None:
116
116
  """Initialize an agent for a session and replay history to UI."""
117
- if operation.session_id is None:
118
- raise ValueError("session_id cannot be None")
119
-
120
117
  await self.agent_manager.ensure_agent(operation.session_id)
121
118
 
122
119
  async def handle_user_input(self, operation: op.UserInputOperation) -> None:
@@ -482,4 +479,4 @@ class Executor:
482
479
 
483
480
  # Static type check: ExecutorContext must satisfy OperationHandler protocol.
484
481
  # If this line causes a type error, ExecutorContext is missing required methods.
485
- _: type[OperationHandler] = ExecutorContext # pyright: ignore[reportUnusedVariable]
482
+ _: type[OperationHandler] = ExecutorContext
@@ -38,16 +38,21 @@ class AgentManager:
38
38
 
39
39
  await self._event_queue.put(event)
40
40
 
41
- async def ensure_agent(self, session_id: str) -> Agent:
42
- """Return an existing agent for the session or create a new one."""
43
-
44
- agent = self._active_agents.get(session_id)
45
- if agent is not None:
46
- return agent
47
-
48
- session = Session.load(session_id)
49
- profile = self._model_profile_provider.build_profile(self._llm_clients.main)
50
- agent = Agent(session=session, profile=profile)
41
+ async def ensure_agent(self, session_id: str | None = None) -> Agent:
42
+ """Return an existing agent for the session or create a new one.
43
+
44
+ If session_id is None, a new session is created with an auto-generated ID.
45
+ If session_id is provided, attempts to load existing session or creates new one.
46
+ """
47
+ if session_id is None:
48
+ session = Session.create()
49
+ else:
50
+ agent = self._active_agents.get(session_id)
51
+ if agent is not None:
52
+ return agent
53
+ session = Session.load(session_id)
54
+ profile = self._model_profile_provider.build_profile(self._llm_clients)
55
+ agent = Agent(session=session, profile=profile, model_name=self._llm_clients.main_model_name)
51
56
 
52
57
  async for evt in agent.replay_history():
53
58
  await self.emit_event(evt)
@@ -55,13 +60,13 @@ class AgentManager:
55
60
  await self.emit_event(
56
61
  events.WelcomeEvent(
57
62
  work_dir=str(session.work_dir),
58
- llm_config=self._llm_clients.main.get_llm_config(),
63
+ llm_config=self._llm_clients.get_llm_config(),
59
64
  )
60
65
  )
61
66
 
62
- self._active_agents[session_id] = agent
67
+ self._active_agents[session.id] = agent
63
68
  log_debug(
64
- f"Initialized agent for session: {session_id}",
69
+ f"Initialized agent for session: {session.id}",
65
70
  style="cyan",
66
71
  debug_type=DebugType.EXECUTION,
67
72
  )
@@ -76,7 +81,7 @@ class AgentManager:
76
81
 
77
82
  llm_config = config.get_model_config(model_name)
78
83
  llm_client = create_llm_client(llm_config)
79
- agent.set_model_profile(self._model_profile_provider.build_profile(llm_client))
84
+ agent.set_model_profile(self._model_profile_provider.build_profile_eager(llm_client), model_name=model_name)
80
85
 
81
86
  developer_item = model.DeveloperMessageItem(
82
87
  content=f"switched to model: {model_name}",
@@ -2,41 +2,66 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from dataclasses import dataclass
6
- from dataclasses import field as dataclass_field
5
+ from collections.abc import Callable
7
6
 
8
7
  from klaude_code.llm.client import LLMClientABC
8
+ from klaude_code.protocol import llm_param
9
9
  from klaude_code.protocol.tools import SubAgentType
10
10
 
11
11
 
12
- def _default_sub_clients() -> dict[SubAgentType, LLMClientABC]:
13
- """Return an empty mapping for sub-agent clients.
12
+ class LLMClients:
13
+ """Container for LLM clients used by main agent and sub-agents."""
14
14
 
15
- Defined separately so static type checkers can infer the dictionary
16
- key and value types instead of treating them as ``Unknown``.
17
- """
15
+ def __init__(
16
+ self,
17
+ main_factory: Callable[[], LLMClientABC],
18
+ main_model_name: str,
19
+ main_llm_config: llm_param.LLMConfigParameter,
20
+ ) -> None:
21
+ self._main_factory: Callable[[], LLMClientABC] | None = main_factory
22
+ self._main_client: LLMClientABC | None = None
23
+ self._main_model_name: str = main_model_name
24
+ self._main_llm_config: llm_param.LLMConfigParameter = main_llm_config
25
+ self._sub_clients: dict[SubAgentType, LLMClientABC] = {}
26
+ self._sub_factories: dict[SubAgentType, Callable[[], LLMClientABC]] = {}
18
27
 
19
- return {}
28
+ @property
29
+ def main_model_name(self) -> str:
30
+ return self._main_model_name
20
31
 
32
+ def get_llm_config(self) -> llm_param.LLMConfigParameter:
33
+ return self._main_llm_config
21
34
 
22
- @dataclass
23
- class LLMClients:
24
- """Container for LLM clients used by main agent and sub-agents."""
35
+ @property
36
+ def main(self) -> LLMClientABC:
37
+ if self._main_client is None:
38
+ if self._main_factory is None:
39
+ raise RuntimeError("Main client factory not set")
40
+ self._main_client = self._main_factory()
41
+ self._main_factory = None
42
+ return self._main_client
25
43
 
26
- main: LLMClientABC
27
- sub_clients: dict[SubAgentType, LLMClientABC] = dataclass_field(default_factory=_default_sub_clients)
44
+ def register_sub_client_factory(
45
+ self,
46
+ sub_agent_type: SubAgentType,
47
+ factory: Callable[[], LLMClientABC],
48
+ ) -> None:
49
+ self._sub_factories[sub_agent_type] = factory
28
50
 
29
51
  def get_client(self, sub_agent_type: SubAgentType | None = None) -> LLMClientABC:
30
- """Return client for a sub-agent type or the main client.
52
+ """Return client for a sub-agent type or the main client."""
31
53
 
32
- Args:
33
- sub_agent_type: Optional sub-agent type whose client should be returned.
54
+ if sub_agent_type is None:
55
+ return self.main
34
56
 
35
- Returns:
36
- The LLM client corresponding to the sub-agent type, or the main client
37
- when no specialized client is available.
38
- """
57
+ existing = self._sub_clients.get(sub_agent_type)
58
+ if existing is not None:
59
+ return existing
39
60
 
40
- if sub_agent_type is None:
61
+ factory = self._sub_factories.get(sub_agent_type)
62
+ if factory is None:
41
63
  return self.main
42
- return self.sub_clients.get(sub_agent_type) or self.main
64
+
65
+ client = factory()
66
+ self._sub_clients[sub_agent_type] = client
67
+ return client
@@ -6,8 +6,7 @@ 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.protocol.sub_agent import get_sub_agent_profile
10
- from klaude_code.protocol.tools import SubAgentType
9
+ from klaude_code.protocol.sub_agent import iter_sub_agent_profiles
11
10
  from klaude_code.trace import DebugType, log_debug
12
11
 
13
12
 
@@ -15,7 +14,6 @@ def build_llm_clients(
15
14
  config: Config,
16
15
  *,
17
16
  model_override: str | None = None,
18
- enabled_sub_agents: list[SubAgentType] | None = None,
19
17
  ) -> LLMClients:
20
18
  """Create an ``LLMClients`` bundle driven by application config."""
21
19
 
@@ -32,18 +30,29 @@ def build_llm_clients(
32
30
  debug_type=DebugType.LLM_CONFIG,
33
31
  )
34
32
 
35
- main_client = create_llm_client(llm_config)
36
- sub_clients: dict[SubAgentType, LLMClientABC] = {}
33
+ main_model_name = str(llm_config.model)
37
34
 
38
- # Initialize sub-agent clients
39
- for sub_agent_type in enabled_sub_agents or []:
40
- model_name = config.subagent_models.get(sub_agent_type)
35
+ def _main_factory() -> LLMClientABC:
36
+ return create_llm_client(llm_config)
37
+
38
+ clients = LLMClients(
39
+ main_factory=_main_factory,
40
+ main_model_name=main_model_name,
41
+ main_llm_config=llm_config,
42
+ )
43
+
44
+ for profile in iter_sub_agent_profiles():
45
+ model_name = config.subagent_models.get(profile.name)
41
46
  if not model_name:
42
47
  continue
43
- profile = get_sub_agent_profile(sub_agent_type)
44
- if not profile.enabled_for_model(main_client.model_name):
48
+
49
+ if not profile.enabled_for_model(main_model_name):
45
50
  continue
46
- sub_llm_config = config.get_model_config(model_name)
47
- sub_clients[sub_agent_type] = create_llm_client(sub_llm_config)
48
51
 
49
- return LLMClients(main=main_client, sub_clients=sub_clients)
52
+ def _factory(model_name_for_factory: str = model_name) -> LLMClientABC:
53
+ sub_llm_config = config.get_model_config(model_name_for_factory)
54
+ return create_llm_client(sub_llm_config)
55
+
56
+ clients.register_sub_client_factory(profile.name, _factory)
57
+
58
+ return clients
@@ -43,7 +43,7 @@ class SubAgentManager:
43
43
  child_session = Session(work_dir=parent_session.work_dir)
44
44
  child_session.sub_agent_state = state
45
45
 
46
- child_profile = self._model_profile_provider.build_profile(
46
+ child_profile = self._model_profile_provider.build_profile_eager(
47
47
  self._llm_clients.get_client(state.sub_agent_type),
48
48
  state.sub_agent_type,
49
49
  )
@@ -6,8 +6,8 @@ from pathlib import Path
6
6
 
7
7
  COMMAND_DESCRIPTIONS: dict[str, str] = {
8
8
  "rg": "ripgrep - fast text search",
9
- "fd": "fd - simple and fast alternative to find",
10
- "tree": "tree - directory listing as a tree",
9
+ "fd": "simple and fast alternative to find",
10
+ "tree": "directory listing as a tree",
11
11
  "sg": "ast-grep - AST-aware code search",
12
12
  }
13
13
 
@@ -15,7 +15,7 @@ COMMAND_DESCRIPTIONS: dict[str, str] = {
15
15
  PROMPT_FILES: dict[str, str] = {
16
16
  "main_gpt_5_1": "prompts/prompt-codex-gpt-5-1.md",
17
17
  "main_gpt_5_1_codex_max": "prompts/prompt-codex-gpt-5-1-codex-max.md",
18
- "main_claude": "prompts/prompt-claude-code.md",
18
+ "main": "prompts/prompt-claude-code.md",
19
19
  "main_gemini": "prompts/prompt-gemini.md", # https://ai.google.dev/gemini-api/docs/prompting-strategies?hl=zh-cn#agentic-si-template
20
20
  # Sub-agent prompts keyed by their name
21
21
  "Task": "prompts/prompt-subagent.md",
@@ -49,7 +49,7 @@ def _get_file_key(model_name: str, sub_agent_type: str | None) -> str:
49
49
  case name if "gemini" in name:
50
50
  return "main_gemini"
51
51
  case _:
52
- return "main_claude"
52
+ return "main"
53
53
 
54
54
 
55
55
  def _build_env_info(model_name: str) -> str:
@@ -84,15 +84,4 @@ assistant: [Uses the Explore tool to find the files that handle client errors in
84
84
  <example>
85
85
  user: What is the codebase structure?
86
86
  assistant: [Uses the Explore tool]
87
- </example>
88
-
89
- ## Memory
90
- MEMORY PROTOCOL:
91
- 1. Use the `view` command of your `Memory` tool to check for earlier progress.
92
- 2. ... (work on the task) ...
93
- - As you make progress, record status / progress / thoughts etc in your memory.
94
- ASSUME INTERRUPTION: Your context window might be reset at any moment, so you risk losing any progress that is not recorded in your memory directory.
95
-
96
- Note: when editing your memory folder, always try to keep its content up-to-date, coherent and organized. You can rename or delete files that are no longer relevant. Do not create new files unless necessary.
97
-
98
- Only write down information relevant to current project in your memory system.
87
+ </example>
@@ -0,0 +1,12 @@
1
+ You are an interactive CLI tool. Use the tools available to you to assist the user.
2
+
3
+ ## Guidelines
4
+ - Never use emojis.
5
+ - Your output will be displayed on a command line interface. Your responses should be short and concise.
6
+ - Output text to communicate with the user; all text you output outside of tool use is displayed to the user. Only use tools to complete tasks. Never use tools like Bash or code comments as means to communicate with the user during the session.
7
+ - NEVER create Markdown files unless they're absolutely necessary for achieving your goal.
8
+ - Use TodoWrite tool to help you manage and plan tasks.
9
+ - For maximum efficiency, whenever you need to perform multiple independent operations, invoke all relevant tools simultaneously rather than sequentially.
10
+
11
+ ## Professional objectivity
12
+ Prioritize technical accuracy and truthfulness over validating the user's beliefs. Focus on facts and problem-solving, providing direct, objective technical info without any unnecessary superlatives, praise, or emotional validation. It is best for the user if you honestly applies the same rigorous standards to all ideas and disagrees when necessary, even if it may not be what the user wants to hear. Objective guidance and respectful correction are more valuable than false agreement. Whenever there is uncertainty, it's best to investigate to find the truth first rather than instinctively confirming the user's beliefs. Avoid using over-the-top validation or excessive praise when responding to users such as "You're absolutely right" or similar phrases.
@@ -344,9 +344,6 @@ async def last_path_memory_reminder(
344
344
  paths.append(path)
345
345
  except json.JSONDecodeError:
346
346
  continue
347
- elif tool_call.name == tools.BASH:
348
- # TODO: haiku check file path
349
- pass
350
347
  paths = list(set(paths))
351
348
  memories: list[Memory] = []
352
349
  if len(paths) == 0:
klaude_code/core/task.py CHANGED
@@ -29,9 +29,11 @@ class MetadataAccumulator:
29
29
  self._sub_agent_metadata: list[model.TaskMetadata] = []
30
30
  self._throughput_weighted_sum: float = 0.0
31
31
  self._throughput_tracked_tokens: int = 0
32
+ self._turn_count: int = 0
32
33
 
33
34
  def add(self, turn_metadata: model.ResponseMetadataItem) -> None:
34
35
  """Merge a turn's metadata into the accumulated state."""
36
+ self._turn_count += 1
35
37
  main = self._main
36
38
  usage = turn_metadata.usage
37
39
 
@@ -43,10 +45,11 @@ class MetadataAccumulator:
43
45
  acc_usage.cached_tokens += usage.cached_tokens
44
46
  acc_usage.reasoning_tokens += usage.reasoning_tokens
45
47
  acc_usage.output_tokens += usage.output_tokens
48
+ acc_usage.last_turn_output_token = usage.output_tokens
46
49
  acc_usage.currency = usage.currency
47
50
 
48
- if usage.context_window_size is not None:
49
- acc_usage.context_window_size = usage.context_window_size
51
+ if usage.context_token is not None:
52
+ acc_usage.context_token = usage.context_token
50
53
  if usage.context_limit is not None:
51
54
  acc_usage.context_limit = usage.context_limit
52
55
 
@@ -91,6 +94,7 @@ class MetadataAccumulator:
91
94
  main.usage.throughput_tps = None
92
95
 
93
96
  main.task_duration_s = task_duration_s
97
+ main.turn_count = self._turn_count
94
98
  return model.TaskMetadataItem(main=main, sub_agent_task_metadata=self._sub_agent_metadata)
95
99
 
96
100
 
@@ -0,0 +1,30 @@
1
+ """Shared utility functions for file tools."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+
8
+
9
+ def is_directory(path: str) -> bool:
10
+ """Check if path is a directory."""
11
+ return os.path.isdir(path)
12
+
13
+
14
+ def file_exists(path: str) -> bool:
15
+ """Check if path exists."""
16
+ return os.path.exists(path)
17
+
18
+
19
+ def read_text(path: str) -> str:
20
+ """Read text from file with UTF-8 encoding."""
21
+ with open(path, "r", encoding="utf-8", errors="replace") as f:
22
+ return f.read()
23
+
24
+
25
+ def write_text(path: str, content: str) -> None:
26
+ """Write text to file, creating parent directories if needed."""
27
+ parent = Path(path).parent
28
+ parent.mkdir(parents=True, exist_ok=True)
29
+ with open(path, "w", encoding="utf-8") as f:
30
+ f.write(content)
@@ -7,38 +7,13 @@ from pathlib import Path
7
7
 
8
8
  from pydantic import BaseModel, Field
9
9
 
10
+ from klaude_code.core.tool.file._utils import file_exists, is_directory, read_text, write_text
10
11
  from klaude_code.core.tool.tool_abc import ToolABC, load_desc
11
12
  from klaude_code.core.tool.tool_context import get_current_file_tracker
12
13
  from klaude_code.core.tool.tool_registry import register
13
14
  from klaude_code.protocol import llm_param, model, tools
14
15
 
15
16
 
16
- def _is_directory(path: str) -> bool:
17
- try:
18
- return Path(path).is_dir()
19
- except Exception:
20
- return False
21
-
22
-
23
- def _file_exists(path: str) -> bool:
24
- try:
25
- return Path(path).exists()
26
- except Exception:
27
- return False
28
-
29
-
30
- def _read_text(path: str) -> str:
31
- with open(path, "r", encoding="utf-8", errors="replace") as f:
32
- return f.read()
33
-
34
-
35
- def _write_text(path: str, content: str) -> None:
36
- parent = Path(path).parent
37
- parent.mkdir(parents=True, exist_ok=True)
38
- with open(path, "w", encoding="utf-8") as f:
39
- f.write(content)
40
-
41
-
42
17
  @register(tools.EDIT)
43
18
  class EditTool(ToolABC):
44
19
  class EditArguments(BaseModel):
@@ -119,7 +94,7 @@ class EditTool(ToolABC):
119
94
  file_path = os.path.abspath(args.file_path)
120
95
 
121
96
  # Common file errors
122
- if _is_directory(file_path):
97
+ if is_directory(file_path):
123
98
  return model.ToolResultItem(
124
99
  status="error",
125
100
  output="<tool_use_error>Illegal operation on a directory. edit</tool_use_error>",
@@ -136,7 +111,7 @@ class EditTool(ToolABC):
136
111
 
137
112
  # FileTracker checks (only for editing existing files)
138
113
  file_tracker = get_current_file_tracker()
139
- if not _file_exists(file_path):
114
+ if not file_exists(file_path):
140
115
  # We require reading before editing
141
116
  return model.ToolResultItem(
142
117
  status="error",
@@ -163,7 +138,7 @@ class EditTool(ToolABC):
163
138
 
164
139
  # Edit existing file: validate and apply
165
140
  try:
166
- before = await asyncio.to_thread(_read_text, file_path)
141
+ before = await asyncio.to_thread(read_text, file_path)
167
142
  except FileNotFoundError:
168
143
  return model.ToolResultItem(
169
144
  status="error",
@@ -197,7 +172,7 @@ class EditTool(ToolABC):
197
172
 
198
173
  # Write back
199
174
  try:
200
- await asyncio.to_thread(_write_text, file_path, after)
175
+ await asyncio.to_thread(write_text, file_path, after)
201
176
  except Exception as e: # pragma: no cover
202
177
  return model.ToolResultItem(status="error", output=f"<tool_use_error>{e}</tool_use_error>")
203
178