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.
- klaude_code/app/__init__.py +12 -0
- klaude_code/app/runtime.py +215 -0
- klaude_code/cli/auth_cmd.py +2 -2
- klaude_code/cli/config_cmd.py +2 -2
- klaude_code/cli/cost_cmd.py +1 -1
- klaude_code/cli/debug.py +12 -36
- klaude_code/cli/list_model.py +3 -3
- klaude_code/cli/main.py +17 -60
- klaude_code/cli/self_update.py +2 -187
- klaude_code/cli/session_cmd.py +2 -2
- klaude_code/config/config.py +1 -1
- klaude_code/config/select_model.py +1 -1
- klaude_code/const.py +10 -1
- klaude_code/core/agent.py +9 -62
- klaude_code/core/agent_profile.py +284 -0
- klaude_code/core/executor.py +343 -230
- klaude_code/core/manager/llm_clients_builder.py +1 -1
- klaude_code/core/manager/sub_agent_manager.py +16 -29
- klaude_code/core/reminders.py +107 -155
- klaude_code/core/task.py +12 -20
- klaude_code/core/tool/__init__.py +5 -19
- klaude_code/core/tool/context.py +84 -0
- klaude_code/core/tool/file/apply_patch_tool.py +18 -21
- klaude_code/core/tool/file/edit_tool.py +42 -44
- klaude_code/core/tool/file/read_tool.py +14 -9
- klaude_code/core/tool/file/write_tool.py +12 -13
- klaude_code/core/tool/report_back_tool.py +4 -1
- klaude_code/core/tool/shell/bash_tool.py +6 -11
- klaude_code/core/tool/skill/skill_tool.py +3 -1
- klaude_code/core/tool/sub_agent_tool.py +8 -7
- klaude_code/core/tool/todo/todo_write_tool.py +3 -9
- klaude_code/core/tool/todo/update_plan_tool.py +3 -5
- klaude_code/core/tool/tool_abc.py +2 -1
- klaude_code/core/tool/tool_registry.py +2 -33
- klaude_code/core/tool/tool_runner.py +13 -10
- klaude_code/core/tool/web/mermaid_tool.py +3 -1
- klaude_code/core/tool/web/web_fetch_tool.py +5 -3
- klaude_code/core/tool/web/web_search_tool.py +5 -3
- klaude_code/core/turn.py +86 -26
- klaude_code/llm/anthropic/client.py +1 -1
- klaude_code/llm/bedrock/client.py +1 -1
- klaude_code/llm/claude/client.py +1 -1
- klaude_code/llm/codex/client.py +1 -1
- klaude_code/llm/google/client.py +1 -1
- klaude_code/llm/openai_compatible/client.py +1 -1
- klaude_code/llm/openai_compatible/tool_call_accumulator.py +1 -1
- klaude_code/llm/openrouter/client.py +1 -1
- klaude_code/llm/openrouter/reasoning.py +1 -1
- klaude_code/llm/responses/client.py +1 -1
- klaude_code/protocol/events/__init__.py +57 -0
- klaude_code/protocol/events/base.py +18 -0
- klaude_code/protocol/events/chat.py +20 -0
- klaude_code/protocol/events/lifecycle.py +22 -0
- klaude_code/protocol/events/metadata.py +15 -0
- klaude_code/protocol/events/streaming.py +43 -0
- klaude_code/protocol/events/system.py +53 -0
- klaude_code/protocol/events/tools.py +23 -0
- klaude_code/protocol/message.py +3 -11
- klaude_code/protocol/model.py +78 -9
- klaude_code/protocol/op.py +5 -0
- klaude_code/protocol/sub_agent/explore.py +0 -15
- klaude_code/protocol/sub_agent/task.py +1 -1
- klaude_code/protocol/sub_agent/web.py +1 -17
- klaude_code/protocol/tools.py +0 -1
- klaude_code/session/session.py +6 -5
- klaude_code/skill/assets/create-plan/SKILL.md +76 -0
- klaude_code/skill/loader.py +1 -1
- klaude_code/skill/system_skills.py +1 -1
- klaude_code/tui/__init__.py +8 -0
- klaude_code/{command → tui/command}/clear_cmd.py +2 -1
- klaude_code/{command → tui/command}/debug_cmd.py +4 -3
- klaude_code/{command → tui/command}/export_cmd.py +2 -1
- klaude_code/{command → tui/command}/export_online_cmd.py +6 -5
- klaude_code/{command → tui/command}/fork_session_cmd.py +10 -9
- klaude_code/{command → tui/command}/help_cmd.py +3 -2
- klaude_code/{command → tui/command}/model_cmd.py +5 -4
- klaude_code/{command → tui/command}/model_select.py +2 -2
- klaude_code/{command → tui/command}/prompt_command.py +4 -3
- klaude_code/{command → tui/command}/refresh_cmd.py +3 -1
- klaude_code/{command → tui/command}/registry.py +16 -6
- klaude_code/{command → tui/command}/release_notes_cmd.py +3 -2
- klaude_code/{command → tui/command}/resume_cmd.py +6 -5
- klaude_code/{command → tui/command}/status_cmd.py +4 -3
- klaude_code/{command → tui/command}/terminal_setup_cmd.py +4 -3
- klaude_code/{command → tui/command}/thinking_cmd.py +4 -3
- klaude_code/tui/commands.py +164 -0
- klaude_code/{ui/renderers → tui/components}/assistant.py +3 -3
- klaude_code/{ui/renderers → tui/components}/bash_syntax.py +2 -2
- klaude_code/{ui/renderers → tui/components}/common.py +1 -1
- klaude_code/tui/components/developer.py +231 -0
- klaude_code/{ui/renderers → tui/components}/diffs.py +2 -2
- klaude_code/{ui/renderers → tui/components}/errors.py +2 -2
- klaude_code/{ui/renderers → tui/components}/metadata.py +34 -21
- klaude_code/{ui → tui/components}/rich/markdown.py +78 -34
- klaude_code/{ui → tui/components}/rich/status.py +2 -2
- klaude_code/{ui → tui/components}/rich/theme.py +12 -5
- klaude_code/{ui/renderers → tui/components}/sub_agent.py +23 -43
- klaude_code/{ui/renderers → tui/components}/thinking.py +3 -3
- klaude_code/{ui/renderers → tui/components}/tools.py +11 -48
- klaude_code/{ui/renderers → tui/components}/user_input.py +3 -20
- klaude_code/tui/display.py +85 -0
- klaude_code/{ui/modes/repl → tui/input}/__init__.py +1 -1
- klaude_code/{ui/modes/repl → tui/input}/completers.py +1 -1
- klaude_code/{ui/modes/repl/input_prompt_toolkit.py → tui/input/prompt_toolkit.py} +11 -7
- klaude_code/tui/machine.py +606 -0
- klaude_code/tui/renderer.py +707 -0
- klaude_code/tui/runner.py +321 -0
- klaude_code/tui/terminal/__init__.py +56 -0
- klaude_code/{ui → tui}/terminal/color.py +1 -1
- klaude_code/{ui → tui}/terminal/control.py +1 -1
- klaude_code/{ui → tui}/terminal/notifier.py +1 -1
- klaude_code/{ui → tui}/terminal/selector.py +36 -17
- klaude_code/ui/__init__.py +6 -50
- klaude_code/ui/core/display.py +3 -3
- klaude_code/ui/core/input.py +2 -1
- klaude_code/ui/{modes/debug/display.py → debug_mode.py} +1 -1
- klaude_code/ui/{modes/exec/display.py → exec_mode.py} +1 -4
- klaude_code/ui/terminal/__init__.py +6 -54
- klaude_code/ui/terminal/title.py +31 -0
- klaude_code/update.py +163 -0
- {klaude_code-2.0.1.dist-info → klaude_code-2.1.0.dist-info}/METADATA +1 -1
- klaude_code-2.1.0.dist-info/RECORD +235 -0
- klaude_code/cli/runtime.py +0 -525
- klaude_code/core/prompt.py +0 -108
- klaude_code/core/tool/file/move_tool.md +0 -41
- klaude_code/core/tool/file/move_tool.py +0 -435
- klaude_code/core/tool/tool_context.py +0 -148
- klaude_code/protocol/events.py +0 -194
- klaude_code/skill/assets/dev-docs/SKILL.md +0 -108
- klaude_code/trace/__init__.py +0 -21
- klaude_code/ui/core/stage_manager.py +0 -48
- klaude_code/ui/modes/__init__.py +0 -1
- klaude_code/ui/modes/debug/__init__.py +0 -1
- klaude_code/ui/modes/exec/__init__.py +0 -1
- klaude_code/ui/modes/repl/display.py +0 -61
- klaude_code/ui/modes/repl/event_handler.py +0 -634
- klaude_code/ui/modes/repl/renderer.py +0 -463
- klaude_code/ui/renderers/developer.py +0 -215
- klaude_code/ui/utils/__init__.py +0 -1
- klaude_code-2.0.1.dist-info/RECORD +0 -229
- /klaude_code/{trace/log.py → log.py} +0 -0
- /klaude_code/{command → tui/command}/__init__.py +0 -0
- /klaude_code/{command → tui/command}/command_abc.py +0 -0
- /klaude_code/{command → tui/command}/prompt-commit.md +0 -0
- /klaude_code/{command → tui/command}/prompt-init.md +0 -0
- /klaude_code/{ui/renderers → tui/components}/__init__.py +0 -0
- /klaude_code/{ui/renderers → tui/components}/mermaid_viewer.py +0 -0
- /klaude_code/{ui → tui/components}/rich/__init__.py +0 -0
- /klaude_code/{ui → tui/components}/rich/cjk_wrap.py +0 -0
- /klaude_code/{ui → tui/components}/rich/code_panel.py +0 -0
- /klaude_code/{ui → tui/components}/rich/live.py +0 -0
- /klaude_code/{ui → tui/components}/rich/quote.py +0 -0
- /klaude_code/{ui → tui/components}/rich/searchable_text.py +0 -0
- /klaude_code/{ui/modes/repl → tui/input}/clipboard.py +0 -0
- /klaude_code/{ui/modes/repl → tui/input}/key_bindings.py +0 -0
- /klaude_code/{ui → tui}/terminal/image.py +0 -0
- /klaude_code/{ui → tui}/terminal/progress_bar.py +0 -0
- /klaude_code/ui/{utils/common.py → common.py} +0 -0
- {klaude_code-2.0.1.dist-info → klaude_code-2.1.0.dist-info}/WHEEL +0 -0
- {klaude_code-2.0.1.dist-info → klaude_code-2.1.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from collections.abc import Awaitable, Callable, MutableMapping
|
|
5
|
+
from dataclasses import dataclass, replace
|
|
6
|
+
|
|
7
|
+
from klaude_code.protocol import model
|
|
8
|
+
from klaude_code.protocol.sub_agent import SubAgentResult
|
|
9
|
+
from klaude_code.session.session import Session
|
|
10
|
+
|
|
11
|
+
type FileTracker = MutableMapping[str, model.FileStatus]
|
|
12
|
+
|
|
13
|
+
RunSubtask = Callable[[model.SubAgentState, Callable[[str], None] | None], Awaitable[SubAgentResult]]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class TodoContext:
|
|
18
|
+
"""Todo access interface exposed to tools.
|
|
19
|
+
|
|
20
|
+
Tools can only read the current todo list and replace it with
|
|
21
|
+
a new list; they cannot access the full Session object.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
get_todos: Callable[[], list[model.TodoItem]]
|
|
25
|
+
set_todos: Callable[[list[model.TodoItem]], None]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class SessionTodoStore:
|
|
30
|
+
"""Adapter exposing session todos through an explicit interface."""
|
|
31
|
+
|
|
32
|
+
session: Session
|
|
33
|
+
|
|
34
|
+
def get(self) -> list[model.TodoItem]:
|
|
35
|
+
return self.session.todos
|
|
36
|
+
|
|
37
|
+
def set(self, todos: list[model.TodoItem]) -> None:
|
|
38
|
+
self.session.todos = todos
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def build_todo_context(session: Session) -> TodoContext:
|
|
42
|
+
"""Create a TodoContext backed by the given session."""
|
|
43
|
+
|
|
44
|
+
store = SessionTodoStore(session)
|
|
45
|
+
return TodoContext(get_todos=store.get, set_todos=store.set)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class SubAgentResumeClaims:
|
|
49
|
+
"""Track sub-agent resume claims for a single turn.
|
|
50
|
+
|
|
51
|
+
Multiple concurrent sub-agent tool calls can attempt to resume the same
|
|
52
|
+
session id in a single model response. This class provides an atomic
|
|
53
|
+
`claim()` operation to reject duplicates.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(self) -> None:
|
|
57
|
+
self._claims: set[str] = set()
|
|
58
|
+
self._lock = asyncio.Lock()
|
|
59
|
+
|
|
60
|
+
async def claim(self, session_id: str) -> bool:
|
|
61
|
+
async with self._lock:
|
|
62
|
+
if session_id in self._claims:
|
|
63
|
+
return False
|
|
64
|
+
self._claims.add(session_id)
|
|
65
|
+
return True
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass(frozen=True)
|
|
69
|
+
class ToolContext:
|
|
70
|
+
"""Tool execution context.
|
|
71
|
+
|
|
72
|
+
This object is shallow-immutable: fields cannot be reassigned, but fields
|
|
73
|
+
may reference mutable objects (e.g., FileTracker).
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
file_tracker: FileTracker
|
|
77
|
+
todo_context: TodoContext
|
|
78
|
+
session_id: str
|
|
79
|
+
run_subtask: RunSubtask | None = None
|
|
80
|
+
sub_agent_resume_claims: SubAgentResumeClaims | None = None
|
|
81
|
+
record_sub_agent_session_id: Callable[[str], None] | None = None
|
|
82
|
+
|
|
83
|
+
def with_record_sub_agent_session_id(self, callback: Callable[[str], None] | None) -> ToolContext:
|
|
84
|
+
return replace(self, record_sub_agent_session_id=callback)
|
|
@@ -7,20 +7,20 @@ from pathlib import Path
|
|
|
7
7
|
|
|
8
8
|
from pydantic import BaseModel
|
|
9
9
|
|
|
10
|
+
from klaude_code.core.tool.context import FileTracker, ToolContext
|
|
10
11
|
from klaude_code.core.tool.file import apply_patch as apply_patch_module
|
|
11
12
|
from klaude_code.core.tool.file._utils import hash_text_sha256
|
|
12
13
|
from klaude_code.core.tool.file.diff_builder import build_structured_file_diff
|
|
13
14
|
from klaude_code.core.tool.tool_abc import ToolABC, load_desc
|
|
14
|
-
from klaude_code.core.tool.tool_context import get_current_file_tracker
|
|
15
15
|
from klaude_code.core.tool.tool_registry import register
|
|
16
16
|
from klaude_code.protocol import llm_param, message, model, tools
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
class ApplyPatchHandler:
|
|
20
20
|
@classmethod
|
|
21
|
-
async def handle_apply_patch(cls, patch_text: str) -> message.ToolResultMessage:
|
|
21
|
+
async def handle_apply_patch(cls, patch_text: str, context: ToolContext) -> message.ToolResultMessage:
|
|
22
22
|
try:
|
|
23
|
-
output, ui_extra = await asyncio.to_thread(cls._apply_patch_in_thread, patch_text)
|
|
23
|
+
output, ui_extra = await asyncio.to_thread(cls._apply_patch_in_thread, patch_text, context.file_tracker)
|
|
24
24
|
except apply_patch_module.DiffError as error:
|
|
25
25
|
return message.ToolResultMessage(status="error", output_text=str(error))
|
|
26
26
|
except Exception as error: # pragma: no cover # unexpected errors bubbled to tool result
|
|
@@ -32,14 +32,13 @@ class ApplyPatchHandler:
|
|
|
32
32
|
)
|
|
33
33
|
|
|
34
34
|
@staticmethod
|
|
35
|
-
def _apply_patch_in_thread(patch_text: str) -> tuple[str, model.ToolResultUIExtra]:
|
|
35
|
+
def _apply_patch_in_thread(patch_text: str, file_tracker: FileTracker) -> tuple[str, model.ToolResultUIExtra]:
|
|
36
36
|
ap = apply_patch_module
|
|
37
37
|
normalized_start = patch_text.lstrip()
|
|
38
38
|
if not normalized_start.startswith("*** Begin Patch"):
|
|
39
39
|
raise ap.DiffError("apply_patch content must start with *** Begin Patch")
|
|
40
40
|
|
|
41
41
|
workspace_root = os.path.realpath(os.getcwd())
|
|
42
|
-
file_tracker = get_current_file_tracker()
|
|
43
42
|
|
|
44
43
|
def resolve_path(path: str) -> str:
|
|
45
44
|
candidate = os.path.realpath(path if os.path.isabs(path) else os.path.join(workspace_root, path))
|
|
@@ -89,15 +88,14 @@ class ApplyPatchHandler:
|
|
|
89
88
|
with open(resolved, "w", encoding="utf-8") as handle:
|
|
90
89
|
handle.write(content)
|
|
91
90
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
file_tracker.pop(resolved, None)
|
|
108
|
+
with contextlib.suppress(Exception): # pragma: no cover - file tracker best-effort
|
|
109
|
+
file_tracker.pop(resolved, None)
|
|
113
110
|
|
|
114
111
|
ap.apply_commit(commit, write_fn, remove_fn)
|
|
115
112
|
|
|
@@ -172,13 +169,13 @@ class ApplyPatchTool(ToolABC):
|
|
|
172
169
|
)
|
|
173
170
|
|
|
174
171
|
@classmethod
|
|
175
|
-
async def call(cls, arguments: str) -> message.ToolResultMessage:
|
|
172
|
+
async def call(cls, arguments: str, context: ToolContext) -> message.ToolResultMessage:
|
|
176
173
|
try:
|
|
177
174
|
args = cls.ApplyPatchArguments.model_validate_json(arguments)
|
|
178
175
|
except ValueError as exc:
|
|
179
176
|
return message.ToolResultMessage(status="error", output_text=f"Invalid arguments: {exc}")
|
|
180
|
-
return await cls.call_with_args(args)
|
|
177
|
+
return await cls.call_with_args(args, context)
|
|
181
178
|
|
|
182
179
|
@classmethod
|
|
183
|
-
async def call_with_args(cls, args: ApplyPatchArguments) -> message.ToolResultMessage:
|
|
184
|
-
return await ApplyPatchHandler.handle_apply_patch(args.patch)
|
|
180
|
+
async def call_with_args(cls, args: ApplyPatchArguments, context: ToolContext) -> message.ToolResultMessage:
|
|
181
|
+
return await ApplyPatchHandler.handle_apply_patch(args.patch, context)
|
|
@@ -8,10 +8,11 @@ from pathlib import Path
|
|
|
8
8
|
|
|
9
9
|
from pydantic import BaseModel, Field
|
|
10
10
|
|
|
11
|
+
from klaude_code.const import DIFF_DEFAULT_CONTEXT_LINES
|
|
12
|
+
from klaude_code.core.tool.context import ToolContext
|
|
11
13
|
from klaude_code.core.tool.file._utils import file_exists, hash_text_sha256, is_directory, read_text, write_text
|
|
12
14
|
from klaude_code.core.tool.file.diff_builder import build_structured_diff
|
|
13
15
|
from klaude_code.core.tool.tool_abc import ToolABC, load_desc
|
|
14
|
-
from klaude_code.core.tool.tool_context import get_current_file_tracker
|
|
15
16
|
from klaude_code.core.tool.tool_registry import register
|
|
16
17
|
from klaude_code.protocol import llm_param, message, model, tools
|
|
17
18
|
|
|
@@ -85,7 +86,7 @@ class EditTool(ToolABC):
|
|
|
85
86
|
return content.replace(old_string, new_string, 1)
|
|
86
87
|
|
|
87
88
|
@classmethod
|
|
88
|
-
async def call(cls, arguments: str) -> message.ToolResultMessage:
|
|
89
|
+
async def call(cls, arguments: str, context: ToolContext) -> message.ToolResultMessage:
|
|
89
90
|
try:
|
|
90
91
|
args = EditTool.EditArguments.model_validate_json(arguments)
|
|
91
92
|
except ValueError as e: # pragma: no cover - defensive
|
|
@@ -110,7 +111,7 @@ class EditTool(ToolABC):
|
|
|
110
111
|
)
|
|
111
112
|
|
|
112
113
|
# FileTracker checks (only for editing existing files)
|
|
113
|
-
file_tracker =
|
|
114
|
+
file_tracker = context.file_tracker
|
|
114
115
|
tracked_status: model.FileStatus | None = None
|
|
115
116
|
if not file_exists(file_path):
|
|
116
117
|
# We require reading before editing
|
|
@@ -118,13 +119,12 @@ class EditTool(ToolABC):
|
|
|
118
119
|
status="error",
|
|
119
120
|
output_text=("File has not been read yet. Read it first before writing to it."),
|
|
120
121
|
)
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
)
|
|
122
|
+
tracked_status = file_tracker.get(file_path)
|
|
123
|
+
if tracked_status is None:
|
|
124
|
+
return message.ToolResultMessage(
|
|
125
|
+
status="error",
|
|
126
|
+
output_text=("File has not been read yet. Read it first before writing to it."),
|
|
127
|
+
)
|
|
128
128
|
|
|
129
129
|
# Edit existing file: validate and apply
|
|
130
130
|
try:
|
|
@@ -136,29 +136,28 @@ class EditTool(ToolABC):
|
|
|
136
136
|
)
|
|
137
137
|
|
|
138
138
|
# Re-check external modifications using content hash when available.
|
|
139
|
-
if tracked_status is not None:
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
)
|
|
139
|
+
if tracked_status.content_sha256 is not None:
|
|
140
|
+
current_sha256 = hash_text_sha256(before)
|
|
141
|
+
if current_sha256 != tracked_status.content_sha256:
|
|
142
|
+
return message.ToolResultMessage(
|
|
143
|
+
status="error",
|
|
144
|
+
output_text=(
|
|
145
|
+
"File has been modified externally. Either by user or a linter. Read it first before writing to it."
|
|
146
|
+
),
|
|
147
|
+
)
|
|
148
|
+
else:
|
|
149
|
+
# Backward-compat: old sessions only stored mtime.
|
|
150
|
+
try:
|
|
151
|
+
current_mtime = Path(file_path).stat().st_mtime
|
|
152
|
+
except OSError:
|
|
153
|
+
current_mtime = tracked_status.mtime
|
|
154
|
+
if current_mtime != tracked_status.mtime:
|
|
155
|
+
return message.ToolResultMessage(
|
|
156
|
+
status="error",
|
|
157
|
+
output_text=(
|
|
158
|
+
"File has been modified externally. Either by user or a linter. Read it first before writing to it."
|
|
159
|
+
),
|
|
160
|
+
)
|
|
162
161
|
|
|
163
162
|
err = cls.valid(
|
|
164
163
|
content=before,
|
|
@@ -191,28 +190,27 @@ class EditTool(ToolABC):
|
|
|
191
190
|
except (OSError, UnicodeError) as e: # pragma: no cover
|
|
192
191
|
return message.ToolResultMessage(status="error", output_text=f"<tool_use_error>{e}</tool_use_error>")
|
|
193
192
|
|
|
194
|
-
# Prepare UI extra: unified diff with
|
|
193
|
+
# Prepare UI extra: unified diff with default context lines
|
|
195
194
|
diff_lines = list(
|
|
196
195
|
difflib.unified_diff(
|
|
197
196
|
before.splitlines(),
|
|
198
197
|
after.splitlines(),
|
|
199
198
|
fromfile=file_path,
|
|
200
199
|
tofile=file_path,
|
|
201
|
-
n=
|
|
200
|
+
n=DIFF_DEFAULT_CONTEXT_LINES,
|
|
202
201
|
)
|
|
203
202
|
)
|
|
204
203
|
ui_extra = build_structured_diff(before, after, file_path=file_path)
|
|
205
204
|
|
|
206
205
|
# Update tracker with new mtime and content hash
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
)
|
|
206
|
+
with contextlib.suppress(Exception):
|
|
207
|
+
existing = file_tracker.get(file_path)
|
|
208
|
+
is_mem = existing.is_memory if existing else False
|
|
209
|
+
file_tracker[file_path] = model.FileStatus(
|
|
210
|
+
mtime=Path(file_path).stat().st_mtime,
|
|
211
|
+
content_sha256=hash_text_sha256(after),
|
|
212
|
+
is_memory=is_mem,
|
|
213
|
+
)
|
|
216
214
|
|
|
217
215
|
# Build output message
|
|
218
216
|
if args.replace_all:
|
|
@@ -17,9 +17,9 @@ from klaude_code.const import (
|
|
|
17
17
|
READ_MAX_CHARS,
|
|
18
18
|
READ_MAX_IMAGE_BYTES,
|
|
19
19
|
)
|
|
20
|
+
from klaude_code.core.tool.context import FileTracker, ToolContext
|
|
20
21
|
from klaude_code.core.tool.file._utils import file_exists, is_directory
|
|
21
22
|
from klaude_code.core.tool.tool_abc import ToolABC, load_desc
|
|
22
|
-
from klaude_code.core.tool.tool_context import get_current_file_tracker
|
|
23
23
|
from klaude_code.core.tool.tool_registry import register
|
|
24
24
|
from klaude_code.protocol import llm_param, message, model, tools
|
|
25
25
|
|
|
@@ -121,8 +121,13 @@ def _read_segment(options: ReadOptions) -> ReadSegmentResult:
|
|
|
121
121
|
)
|
|
122
122
|
|
|
123
123
|
|
|
124
|
-
def _track_file_access(
|
|
125
|
-
file_tracker
|
|
124
|
+
def _track_file_access(
|
|
125
|
+
file_tracker: FileTracker | None,
|
|
126
|
+
file_path: str,
|
|
127
|
+
*,
|
|
128
|
+
content_sha256: str | None = None,
|
|
129
|
+
is_memory: bool = False,
|
|
130
|
+
) -> None:
|
|
126
131
|
if file_tracker is None or not file_exists(file_path) or is_directory(file_path):
|
|
127
132
|
return
|
|
128
133
|
with contextlib.suppress(Exception):
|
|
@@ -182,12 +187,12 @@ class ReadTool(ToolABC):
|
|
|
182
187
|
)
|
|
183
188
|
|
|
184
189
|
@classmethod
|
|
185
|
-
async def call(cls, arguments: str) -> message.ToolResultMessage:
|
|
190
|
+
async def call(cls, arguments: str, context: ToolContext) -> message.ToolResultMessage:
|
|
186
191
|
try:
|
|
187
192
|
args = ReadTool.ReadArguments.model_validate_json(arguments)
|
|
188
193
|
except Exception as e: # pragma: no cover - defensive
|
|
189
194
|
return message.ToolResultMessage(status="error", output_text=f"Invalid arguments: {e}")
|
|
190
|
-
return await cls.call_with_args(args)
|
|
195
|
+
return await cls.call_with_args(args, context)
|
|
191
196
|
|
|
192
197
|
@classmethod
|
|
193
198
|
def _effective_limits(cls) -> tuple[int | None, int | None, int | None]:
|
|
@@ -198,7 +203,7 @@ class ReadTool(ToolABC):
|
|
|
198
203
|
)
|
|
199
204
|
|
|
200
205
|
@classmethod
|
|
201
|
-
async def call_with_args(cls, args: ReadTool.ReadArguments) -> message.ToolResultMessage:
|
|
206
|
+
async def call_with_args(cls, args: ReadTool.ReadArguments, context: ToolContext) -> message.ToolResultMessage:
|
|
202
207
|
file_path = os.path.abspath(args.file_path)
|
|
203
208
|
char_per_line, line_cap, max_chars = cls._effective_limits()
|
|
204
209
|
|
|
@@ -271,7 +276,7 @@ class ReadTool(ToolABC):
|
|
|
271
276
|
output_text=f"<tool_use_error>Failed to read image file: {exc}</tool_use_error>",
|
|
272
277
|
)
|
|
273
278
|
|
|
274
|
-
_track_file_access(file_path, content_sha256=hashlib.sha256(image_bytes).hexdigest())
|
|
279
|
+
_track_file_access(context.file_tracker, file_path, content_sha256=hashlib.sha256(image_bytes).hexdigest())
|
|
275
280
|
size_kb = size_bytes / 1024.0 if size_bytes else 0.0
|
|
276
281
|
output_text = f"[image] {Path(file_path).name} ({size_kb:.1f}KB)"
|
|
277
282
|
image_part = message.ImageURLPart(url=data_url, id=None)
|
|
@@ -308,7 +313,7 @@ class ReadTool(ToolABC):
|
|
|
308
313
|
|
|
309
314
|
if offset > max(read_result.total_lines, 0):
|
|
310
315
|
warn = f"<system-reminder>Warning: the file exists but is shorter than the provided offset ({offset}). The file has {read_result.total_lines} lines.</system-reminder>"
|
|
311
|
-
_track_file_access(file_path, content_sha256=read_result.content_sha256)
|
|
316
|
+
_track_file_access(context.file_tracker, file_path, content_sha256=read_result.content_sha256)
|
|
312
317
|
return message.ToolResultMessage(status="success", output_text=warn)
|
|
313
318
|
|
|
314
319
|
lines_out: list[str] = [_format_numbered_line(no, content) for no, content in read_result.selected_lines]
|
|
@@ -326,6 +331,6 @@ class ReadTool(ToolABC):
|
|
|
326
331
|
)
|
|
327
332
|
|
|
328
333
|
read_result_str = "\n".join(lines_out)
|
|
329
|
-
_track_file_access(file_path, content_sha256=read_result.content_sha256)
|
|
334
|
+
_track_file_access(context.file_tracker, file_path, content_sha256=read_result.content_sha256)
|
|
330
335
|
|
|
331
336
|
return message.ToolResultMessage(status="success", output_text=read_result_str)
|
|
@@ -7,10 +7,10 @@ from pathlib import Path
|
|
|
7
7
|
|
|
8
8
|
from pydantic import BaseModel
|
|
9
9
|
|
|
10
|
+
from klaude_code.core.tool.context import ToolContext
|
|
10
11
|
from klaude_code.core.tool.file._utils import file_exists, hash_text_sha256, is_directory, read_text, write_text
|
|
11
12
|
from klaude_code.core.tool.file.diff_builder import build_structured_diff
|
|
12
13
|
from klaude_code.core.tool.tool_abc import ToolABC, load_desc
|
|
13
|
-
from klaude_code.core.tool.tool_context import get_current_file_tracker
|
|
14
14
|
from klaude_code.core.tool.tool_registry import register
|
|
15
15
|
from klaude_code.protocol import llm_param, message, model, tools
|
|
16
16
|
|
|
@@ -46,7 +46,7 @@ class WriteTool(ToolABC):
|
|
|
46
46
|
)
|
|
47
47
|
|
|
48
48
|
@classmethod
|
|
49
|
-
async def call(cls, arguments: str) -> message.ToolResultMessage:
|
|
49
|
+
async def call(cls, arguments: str, context: ToolContext) -> message.ToolResultMessage:
|
|
50
50
|
try:
|
|
51
51
|
args = WriteArguments.model_validate_json(arguments)
|
|
52
52
|
except ValueError as e: # pragma: no cover - defensive
|
|
@@ -60,12 +60,12 @@ class WriteTool(ToolABC):
|
|
|
60
60
|
output_text="<tool_use_error>Illegal operation on a directory. write</tool_use_error>",
|
|
61
61
|
)
|
|
62
62
|
|
|
63
|
-
file_tracker =
|
|
63
|
+
file_tracker = context.file_tracker
|
|
64
64
|
exists = file_exists(file_path)
|
|
65
65
|
tracked_status: model.FileStatus | None = None
|
|
66
66
|
|
|
67
67
|
if exists:
|
|
68
|
-
tracked_status = file_tracker.get(file_path)
|
|
68
|
+
tracked_status = file_tracker.get(file_path)
|
|
69
69
|
if tracked_status is None:
|
|
70
70
|
return message.ToolResultMessage(
|
|
71
71
|
status="error",
|
|
@@ -114,15 +114,14 @@ class WriteTool(ToolABC):
|
|
|
114
114
|
except (OSError, UnicodeError) as e: # pragma: no cover
|
|
115
115
|
return message.ToolResultMessage(status="error", output_text=f"<tool_use_error>{e}</tool_use_error>")
|
|
116
116
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
)
|
|
117
|
+
with contextlib.suppress(Exception):
|
|
118
|
+
existing = file_tracker.get(file_path)
|
|
119
|
+
is_mem = existing.is_memory if existing else False
|
|
120
|
+
file_tracker[file_path] = model.FileStatus(
|
|
121
|
+
mtime=Path(file_path).stat().st_mtime,
|
|
122
|
+
content_sha256=hash_text_sha256(args.content),
|
|
123
|
+
is_memory=is_mem,
|
|
124
|
+
)
|
|
126
125
|
|
|
127
126
|
# For markdown files, use MarkdownDocUIExtra to render content as markdown
|
|
128
127
|
# Otherwise, build diff between previous and new content
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from typing import Any, ClassVar, cast
|
|
4
4
|
|
|
5
|
+
from klaude_code.core.tool.context import ToolContext
|
|
5
6
|
from klaude_code.protocol import llm_param, message, tools
|
|
6
7
|
|
|
7
8
|
|
|
@@ -72,7 +73,9 @@ class ReportBackTool:
|
|
|
72
73
|
)
|
|
73
74
|
|
|
74
75
|
@classmethod
|
|
75
|
-
async def call(cls, arguments: str) -> message.ToolResultMessage:
|
|
76
|
+
async def call(cls, arguments: str, context: ToolContext) -> message.ToolResultMessage:
|
|
77
|
+
del arguments
|
|
78
|
+
del context
|
|
76
79
|
"""Execute the report_back tool.
|
|
77
80
|
|
|
78
81
|
The actual handling of report_back results is done by TurnExecutor.
|
|
@@ -11,9 +11,9 @@ from typing import Any
|
|
|
11
11
|
from pydantic import BaseModel
|
|
12
12
|
|
|
13
13
|
from klaude_code.const import BASH_DEFAULT_TIMEOUT_MS, BASH_TERMINATE_TIMEOUT_SEC
|
|
14
|
+
from klaude_code.core.tool.context import ToolContext
|
|
14
15
|
from klaude_code.core.tool.shell.command_safety import is_safe_command
|
|
15
16
|
from klaude_code.core.tool.tool_abc import ToolABC, load_desc
|
|
16
|
-
from klaude_code.core.tool.tool_context import get_current_file_tracker
|
|
17
17
|
from klaude_code.core.tool.tool_registry import register
|
|
18
18
|
from klaude_code.protocol import llm_param, message, model, tools
|
|
19
19
|
|
|
@@ -71,7 +71,7 @@ class BashTool(ToolABC):
|
|
|
71
71
|
timeout_ms: int = BASH_DEFAULT_TIMEOUT_MS
|
|
72
72
|
|
|
73
73
|
@classmethod
|
|
74
|
-
async def call(cls, arguments: str) -> message.ToolResultMessage:
|
|
74
|
+
async def call(cls, arguments: str, context: ToolContext) -> message.ToolResultMessage:
|
|
75
75
|
try:
|
|
76
76
|
args = BashTool.BashArguments.model_validate_json(arguments)
|
|
77
77
|
except ValueError as e:
|
|
@@ -79,10 +79,10 @@ class BashTool(ToolABC):
|
|
|
79
79
|
status="error",
|
|
80
80
|
output_text=f"Invalid arguments: {e}",
|
|
81
81
|
)
|
|
82
|
-
return await cls.call_with_args(args)
|
|
82
|
+
return await cls.call_with_args(args, context)
|
|
83
83
|
|
|
84
84
|
@classmethod
|
|
85
|
-
async def call_with_args(cls, args: BashArguments) -> message.ToolResultMessage:
|
|
85
|
+
async def call_with_args(cls, args: BashArguments, context: ToolContext) -> message.ToolResultMessage:
|
|
86
86
|
# Safety check: only execute commands proven as "known safe"
|
|
87
87
|
result = is_safe_command(args.command)
|
|
88
88
|
if not result.is_safe:
|
|
@@ -119,6 +119,8 @@ class BashTool(ToolABC):
|
|
|
119
119
|
}
|
|
120
120
|
)
|
|
121
121
|
|
|
122
|
+
file_tracker = context.file_tracker
|
|
123
|
+
|
|
122
124
|
def _hash_file_content_sha256(file_path: str) -> str | None:
|
|
123
125
|
try:
|
|
124
126
|
suffix = Path(file_path).suffix.lower()
|
|
@@ -144,9 +146,6 @@ class BashTool(ToolABC):
|
|
|
144
146
|
return os.path.abspath(os.path.join(base_dir, path))
|
|
145
147
|
|
|
146
148
|
def _track_files_read(file_paths: list[str], *, base_dir: str) -> None:
|
|
147
|
-
file_tracker = get_current_file_tracker()
|
|
148
|
-
if file_tracker is None:
|
|
149
|
-
return
|
|
150
149
|
for p in file_paths:
|
|
151
150
|
abs_path = _resolve_in_dir(base_dir, p)
|
|
152
151
|
if not os.path.exists(abs_path) or os.path.isdir(abs_path):
|
|
@@ -168,10 +167,6 @@ class BashTool(ToolABC):
|
|
|
168
167
|
_track_files_read(file_paths, base_dir=base_dir)
|
|
169
168
|
|
|
170
169
|
def _track_mv(src_paths: list[str], dest_path: str, *, base_dir: str) -> None:
|
|
171
|
-
file_tracker = get_current_file_tracker()
|
|
172
|
-
if file_tracker is None:
|
|
173
|
-
return
|
|
174
|
-
|
|
175
170
|
abs_dest = _resolve_in_dir(base_dir, dest_path)
|
|
176
171
|
dest_is_dir = os.path.isdir(abs_dest)
|
|
177
172
|
|
|
@@ -4,6 +4,7 @@ from pathlib import Path
|
|
|
4
4
|
|
|
5
5
|
from pydantic import BaseModel
|
|
6
6
|
|
|
7
|
+
from klaude_code.core.tool.context import ToolContext
|
|
7
8
|
from klaude_code.core.tool.tool_abc import ToolABC, load_desc
|
|
8
9
|
from klaude_code.core.tool.tool_registry import register
|
|
9
10
|
from klaude_code.protocol import llm_param, message, tools
|
|
@@ -55,7 +56,8 @@ class SkillTool(ToolABC):
|
|
|
55
56
|
command: str
|
|
56
57
|
|
|
57
58
|
@classmethod
|
|
58
|
-
async def call(cls, arguments: str) -> message.ToolResultMessage:
|
|
59
|
+
async def call(cls, arguments: str, context: ToolContext) -> message.ToolResultMessage:
|
|
60
|
+
del context
|
|
59
61
|
"""Load and return full skill content."""
|
|
60
62
|
try:
|
|
61
63
|
args = cls.SkillArguments.model_validate_json(arguments)
|
|
@@ -10,8 +10,8 @@ import asyncio
|
|
|
10
10
|
import json
|
|
11
11
|
from typing import TYPE_CHECKING, Any, ClassVar, cast
|
|
12
12
|
|
|
13
|
+
from klaude_code.core.tool.context import ToolContext
|
|
13
14
|
from klaude_code.core.tool.tool_abc import ToolABC, ToolConcurrencyPolicy, ToolMetadata
|
|
14
|
-
from klaude_code.core.tool.tool_context import current_run_subtask_callback, current_sub_agent_resume_claims
|
|
15
15
|
from klaude_code.protocol import llm_param, message, model
|
|
16
16
|
from klaude_code.session.session import Session
|
|
17
17
|
|
|
@@ -52,7 +52,7 @@ class SubAgentTool(ToolABC):
|
|
|
52
52
|
)
|
|
53
53
|
|
|
54
54
|
@classmethod
|
|
55
|
-
async def call(cls, arguments: str) -> message.ToolResultMessage:
|
|
55
|
+
async def call(cls, arguments: str, context: ToolContext) -> message.ToolResultMessage:
|
|
56
56
|
profile = cls._profile
|
|
57
57
|
|
|
58
58
|
try:
|
|
@@ -60,7 +60,7 @@ class SubAgentTool(ToolABC):
|
|
|
60
60
|
except json.JSONDecodeError as e:
|
|
61
61
|
return message.ToolResultMessage(status="error", output_text=f"Invalid JSON arguments: {e}")
|
|
62
62
|
|
|
63
|
-
runner =
|
|
63
|
+
runner = context.run_subtask
|
|
64
64
|
if runner is None:
|
|
65
65
|
return message.ToolResultMessage(status="error", output_text="No subtask runner available in this context")
|
|
66
66
|
|
|
@@ -76,9 +76,10 @@ class SubAgentTool(ToolABC):
|
|
|
76
76
|
except ValueError as exc:
|
|
77
77
|
return message.ToolResultMessage(status="error", output_text=str(exc))
|
|
78
78
|
|
|
79
|
-
claims =
|
|
79
|
+
claims = context.sub_agent_resume_claims
|
|
80
80
|
if claims is not None:
|
|
81
|
-
|
|
81
|
+
ok = await claims.claim(resume_session_id)
|
|
82
|
+
if not ok:
|
|
82
83
|
return message.ToolResultMessage(
|
|
83
84
|
status="error",
|
|
84
85
|
output_text=(
|
|
@@ -87,7 +88,6 @@ class SubAgentTool(ToolABC):
|
|
|
87
88
|
"Merge into a single call or resume in a later turn."
|
|
88
89
|
),
|
|
89
90
|
)
|
|
90
|
-
claims.add(resume_session_id)
|
|
91
91
|
|
|
92
92
|
generation = args.get("generation")
|
|
93
93
|
generation_dict: dict[str, Any] | None = (
|
|
@@ -108,7 +108,8 @@ class SubAgentTool(ToolABC):
|
|
|
108
108
|
resume=resume_session_id,
|
|
109
109
|
output_schema=output_schema,
|
|
110
110
|
generation=generation_dict,
|
|
111
|
-
)
|
|
111
|
+
),
|
|
112
|
+
context.record_sub_agent_session_id,
|
|
112
113
|
)
|
|
113
114
|
except asyncio.CancelledError:
|
|
114
115
|
raise
|
|
@@ -2,8 +2,8 @@ from pathlib import Path
|
|
|
2
2
|
|
|
3
3
|
from pydantic import BaseModel
|
|
4
4
|
|
|
5
|
+
from klaude_code.core.tool.context import ToolContext
|
|
5
6
|
from klaude_code.core.tool.tool_abc import ToolABC, load_desc
|
|
6
|
-
from klaude_code.core.tool.tool_context import get_current_todo_context
|
|
7
7
|
from klaude_code.core.tool.tool_registry import register
|
|
8
8
|
from klaude_code.protocol import llm_param, message, model, tools
|
|
9
9
|
|
|
@@ -76,7 +76,7 @@ class TodoWriteTool(ToolABC):
|
|
|
76
76
|
)
|
|
77
77
|
|
|
78
78
|
@classmethod
|
|
79
|
-
async def call(cls, arguments: str) -> message.ToolResultMessage:
|
|
79
|
+
async def call(cls, arguments: str, context: ToolContext) -> message.ToolResultMessage:
|
|
80
80
|
try:
|
|
81
81
|
args = TodoWriteArguments.model_validate_json(arguments)
|
|
82
82
|
except ValueError as e:
|
|
@@ -85,13 +85,7 @@ class TodoWriteTool(ToolABC):
|
|
|
85
85
|
output_text=f"Invalid arguments: {e}",
|
|
86
86
|
)
|
|
87
87
|
|
|
88
|
-
|
|
89
|
-
todo_context = get_current_todo_context()
|
|
90
|
-
if todo_context is None:
|
|
91
|
-
return message.ToolResultMessage(
|
|
92
|
-
status="error",
|
|
93
|
-
output_text="No active session found",
|
|
94
|
-
)
|
|
88
|
+
todo_context = context.todo_context
|
|
95
89
|
|
|
96
90
|
# Get current todos before updating (for comparison)
|
|
97
91
|
old_todos = todo_context.get_todos()
|