klaude-code 1.9.0__py3-none-any.whl → 2.0.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/auth/base.py +2 -6
- klaude_code/cli/auth_cmd.py +4 -4
- klaude_code/cli/list_model.py +1 -1
- klaude_code/cli/main.py +1 -1
- klaude_code/cli/runtime.py +7 -5
- klaude_code/cli/self_update.py +1 -1
- klaude_code/cli/session_cmd.py +1 -1
- klaude_code/command/clear_cmd.py +6 -2
- klaude_code/command/command_abc.py +2 -2
- klaude_code/command/debug_cmd.py +4 -4
- klaude_code/command/export_cmd.py +2 -2
- klaude_code/command/export_online_cmd.py +12 -12
- klaude_code/command/fork_session_cmd.py +29 -23
- klaude_code/command/help_cmd.py +4 -4
- klaude_code/command/model_cmd.py +4 -4
- klaude_code/command/model_select.py +1 -1
- klaude_code/command/prompt-commit.md +11 -2
- klaude_code/command/prompt_command.py +3 -3
- klaude_code/command/refresh_cmd.py +2 -2
- klaude_code/command/registry.py +7 -5
- klaude_code/command/release_notes_cmd.py +4 -4
- klaude_code/command/resume_cmd.py +15 -11
- klaude_code/command/status_cmd.py +4 -4
- klaude_code/command/terminal_setup_cmd.py +8 -8
- klaude_code/command/thinking_cmd.py +4 -4
- klaude_code/config/assets/builtin_config.yaml +16 -0
- klaude_code/config/builtin_config.py +16 -5
- klaude_code/config/config.py +7 -2
- klaude_code/const.py +146 -91
- klaude_code/core/agent.py +3 -12
- klaude_code/core/executor.py +21 -13
- klaude_code/core/manager/sub_agent_manager.py +71 -7
- klaude_code/core/prompts/prompt-sub-agent-image-gen.md +1 -0
- klaude_code/core/prompts/prompt-sub-agent-web.md +27 -1
- klaude_code/core/reminders.py +88 -69
- klaude_code/core/task.py +44 -45
- klaude_code/core/tool/file/apply_patch_tool.py +9 -9
- klaude_code/core/tool/file/diff_builder.py +3 -5
- klaude_code/core/tool/file/edit_tool.py +23 -23
- klaude_code/core/tool/file/move_tool.py +43 -43
- klaude_code/core/tool/file/read_tool.py +44 -39
- klaude_code/core/tool/file/write_tool.py +14 -14
- klaude_code/core/tool/report_back_tool.py +4 -4
- klaude_code/core/tool/shell/bash_tool.py +23 -23
- klaude_code/core/tool/skill/skill_tool.py +7 -7
- klaude_code/core/tool/sub_agent_tool.py +38 -9
- klaude_code/core/tool/todo/todo_write_tool.py +8 -8
- klaude_code/core/tool/todo/update_plan_tool.py +6 -6
- klaude_code/core/tool/tool_abc.py +2 -2
- klaude_code/core/tool/tool_context.py +27 -0
- klaude_code/core/tool/tool_runner.py +88 -42
- klaude_code/core/tool/truncation.py +38 -20
- klaude_code/core/tool/web/mermaid_tool.py +6 -7
- klaude_code/core/tool/web/web_fetch_tool.py +68 -30
- klaude_code/core/tool/web/web_search_tool.py +15 -17
- klaude_code/core/turn.py +120 -73
- klaude_code/llm/anthropic/client.py +79 -44
- klaude_code/llm/anthropic/input.py +116 -108
- klaude_code/llm/bedrock/client.py +8 -5
- klaude_code/llm/claude/client.py +18 -8
- klaude_code/llm/client.py +4 -3
- klaude_code/llm/codex/client.py +15 -9
- klaude_code/llm/google/client.py +122 -60
- klaude_code/llm/google/input.py +94 -108
- klaude_code/llm/image.py +123 -0
- klaude_code/llm/input_common.py +136 -189
- klaude_code/llm/openai_compatible/client.py +17 -7
- klaude_code/llm/openai_compatible/input.py +36 -66
- klaude_code/llm/openai_compatible/stream.py +119 -67
- klaude_code/llm/openai_compatible/tool_call_accumulator.py +23 -11
- klaude_code/llm/openrouter/client.py +34 -9
- klaude_code/llm/openrouter/input.py +63 -64
- klaude_code/llm/openrouter/reasoning.py +22 -24
- klaude_code/llm/registry.py +20 -17
- klaude_code/llm/responses/client.py +107 -45
- klaude_code/llm/responses/input.py +115 -98
- klaude_code/llm/usage.py +52 -25
- klaude_code/protocol/__init__.py +1 -0
- klaude_code/protocol/events.py +16 -12
- klaude_code/protocol/llm_param.py +20 -2
- klaude_code/protocol/message.py +250 -0
- klaude_code/protocol/model.py +94 -281
- klaude_code/protocol/op.py +2 -2
- klaude_code/protocol/sub_agent/__init__.py +1 -0
- klaude_code/protocol/sub_agent/explore.py +10 -0
- klaude_code/protocol/sub_agent/image_gen.py +119 -0
- klaude_code/protocol/sub_agent/task.py +10 -0
- klaude_code/protocol/sub_agent/web.py +10 -0
- klaude_code/session/codec.py +6 -6
- klaude_code/session/export.py +261 -62
- klaude_code/session/selector.py +7 -24
- klaude_code/session/session.py +126 -54
- klaude_code/session/store.py +5 -32
- klaude_code/session/templates/export_session.html +1 -1
- klaude_code/session/templates/mermaid_viewer.html +1 -1
- klaude_code/trace/log.py +11 -6
- klaude_code/ui/core/input.py +1 -1
- klaude_code/ui/core/stage_manager.py +1 -8
- klaude_code/ui/modes/debug/display.py +2 -2
- klaude_code/ui/modes/repl/clipboard.py +2 -2
- klaude_code/ui/modes/repl/completers.py +18 -10
- klaude_code/ui/modes/repl/event_handler.py +136 -127
- klaude_code/ui/modes/repl/input_prompt_toolkit.py +1 -1
- klaude_code/ui/modes/repl/key_bindings.py +1 -1
- klaude_code/ui/modes/repl/renderer.py +107 -15
- klaude_code/ui/renderers/assistant.py +2 -2
- klaude_code/ui/renderers/common.py +65 -7
- klaude_code/ui/renderers/developer.py +7 -6
- klaude_code/ui/renderers/diffs.py +11 -11
- klaude_code/ui/renderers/mermaid_viewer.py +49 -2
- klaude_code/ui/renderers/metadata.py +33 -5
- klaude_code/ui/renderers/sub_agent.py +57 -16
- klaude_code/ui/renderers/thinking.py +37 -2
- klaude_code/ui/renderers/tools.py +180 -165
- klaude_code/ui/rich/live.py +3 -1
- klaude_code/ui/rich/markdown.py +39 -7
- klaude_code/ui/rich/quote.py +76 -1
- klaude_code/ui/rich/status.py +14 -8
- klaude_code/ui/rich/theme.py +8 -2
- klaude_code/ui/terminal/image.py +34 -0
- klaude_code/ui/terminal/notifier.py +2 -1
- klaude_code/ui/terminal/progress_bar.py +4 -4
- klaude_code/ui/terminal/selector.py +22 -4
- klaude_code/ui/utils/common.py +11 -2
- {klaude_code-1.9.0.dist-info → klaude_code-2.0.0.dist-info}/METADATA +4 -2
- klaude_code-2.0.0.dist-info/RECORD +229 -0
- klaude_code-1.9.0.dist-info/RECORD +0 -224
- {klaude_code-1.9.0.dist-info → klaude_code-2.0.0.dist-info}/WHEEL +0 -0
- {klaude_code-1.9.0.dist-info → klaude_code-2.0.0.dist-info}/entry_points.txt +0 -0
|
@@ -10,12 +10,18 @@ from pathlib import Path
|
|
|
10
10
|
|
|
11
11
|
from pydantic import BaseModel, Field
|
|
12
12
|
|
|
13
|
-
from klaude_code import
|
|
13
|
+
from klaude_code.const import (
|
|
14
|
+
BINARY_CHECK_SIZE,
|
|
15
|
+
READ_CHAR_LIMIT_PER_LINE,
|
|
16
|
+
READ_GLOBAL_LINE_CAP,
|
|
17
|
+
READ_MAX_CHARS,
|
|
18
|
+
READ_MAX_IMAGE_BYTES,
|
|
19
|
+
)
|
|
14
20
|
from klaude_code.core.tool.file._utils import file_exists, is_directory
|
|
15
21
|
from klaude_code.core.tool.tool_abc import ToolABC, load_desc
|
|
16
22
|
from klaude_code.core.tool.tool_context import get_current_file_tracker
|
|
17
23
|
from klaude_code.core.tool.tool_registry import register
|
|
18
|
-
from klaude_code.protocol import llm_param, model, tools
|
|
24
|
+
from klaude_code.protocol import llm_param, message, model, tools
|
|
19
25
|
|
|
20
26
|
_IMAGE_MIME_TYPES: dict[str, str] = {
|
|
21
27
|
".png": "image/png",
|
|
@@ -25,14 +31,12 @@ _IMAGE_MIME_TYPES: dict[str, str] = {
|
|
|
25
31
|
".webp": "image/webp",
|
|
26
32
|
}
|
|
27
33
|
|
|
28
|
-
_BINARY_CHECK_SIZE = 8192
|
|
29
|
-
|
|
30
34
|
|
|
31
35
|
def _is_binary_file(file_path: str) -> bool:
|
|
32
36
|
"""Check if a file is binary by looking for null bytes in the first chunk."""
|
|
33
37
|
try:
|
|
34
38
|
with open(file_path, "rb") as f:
|
|
35
|
-
chunk = f.read(
|
|
39
|
+
chunk = f.read(BINARY_CHECK_SIZE)
|
|
36
40
|
return b"\x00" in chunk
|
|
37
41
|
except OSError:
|
|
38
42
|
return False
|
|
@@ -48,9 +52,9 @@ class ReadOptions:
|
|
|
48
52
|
file_path: str
|
|
49
53
|
offset: int
|
|
50
54
|
limit: int | None
|
|
51
|
-
char_limit_per_line: int | None =
|
|
52
|
-
global_line_cap: int | None =
|
|
53
|
-
max_total_chars: int | None =
|
|
55
|
+
char_limit_per_line: int | None = READ_CHAR_LIMIT_PER_LINE
|
|
56
|
+
global_line_cap: int | None = READ_GLOBAL_LINE_CAP
|
|
57
|
+
max_total_chars: int | None = READ_MAX_CHARS
|
|
54
58
|
|
|
55
59
|
|
|
56
60
|
@dataclass
|
|
@@ -92,7 +96,7 @@ def _read_segment(options: ReadOptions) -> ReadSegmentResult:
|
|
|
92
96
|
truncated_chars = original_len - options.char_limit_per_line
|
|
93
97
|
content = (
|
|
94
98
|
content[: options.char_limit_per_line]
|
|
95
|
-
+ f"
|
|
99
|
+
+ f" … (more {truncated_chars} characters in this line are truncated)"
|
|
96
100
|
)
|
|
97
101
|
line_chars = len(content) + 1
|
|
98
102
|
selected_chars += line_chars
|
|
@@ -178,42 +182,42 @@ class ReadTool(ToolABC):
|
|
|
178
182
|
)
|
|
179
183
|
|
|
180
184
|
@classmethod
|
|
181
|
-
async def call(cls, arguments: str) ->
|
|
185
|
+
async def call(cls, arguments: str) -> message.ToolResultMessage:
|
|
182
186
|
try:
|
|
183
187
|
args = ReadTool.ReadArguments.model_validate_json(arguments)
|
|
184
188
|
except Exception as e: # pragma: no cover - defensive
|
|
185
|
-
return
|
|
189
|
+
return message.ToolResultMessage(status="error", output_text=f"Invalid arguments: {e}")
|
|
186
190
|
return await cls.call_with_args(args)
|
|
187
191
|
|
|
188
192
|
@classmethod
|
|
189
193
|
def _effective_limits(cls) -> tuple[int | None, int | None, int | None]:
|
|
190
194
|
return (
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
195
|
+
READ_CHAR_LIMIT_PER_LINE,
|
|
196
|
+
READ_GLOBAL_LINE_CAP,
|
|
197
|
+
READ_MAX_CHARS,
|
|
194
198
|
)
|
|
195
199
|
|
|
196
200
|
@classmethod
|
|
197
|
-
async def call_with_args(cls, args: ReadTool.ReadArguments) ->
|
|
201
|
+
async def call_with_args(cls, args: ReadTool.ReadArguments) -> message.ToolResultMessage:
|
|
198
202
|
file_path = os.path.abspath(args.file_path)
|
|
199
203
|
char_per_line, line_cap, max_chars = cls._effective_limits()
|
|
200
204
|
|
|
201
205
|
if is_directory(file_path):
|
|
202
|
-
return
|
|
206
|
+
return message.ToolResultMessage(
|
|
203
207
|
status="error",
|
|
204
|
-
|
|
208
|
+
output_text="<tool_use_error>Illegal operation on a directory. read</tool_use_error>",
|
|
205
209
|
)
|
|
206
210
|
if not file_exists(file_path):
|
|
207
|
-
return
|
|
211
|
+
return message.ToolResultMessage(
|
|
208
212
|
status="error",
|
|
209
|
-
|
|
213
|
+
output_text="<tool_use_error>File does not exist.</tool_use_error>",
|
|
210
214
|
)
|
|
211
215
|
|
|
212
216
|
# Check for PDF files
|
|
213
217
|
if Path(file_path).suffix.lower() == ".pdf":
|
|
214
|
-
return
|
|
218
|
+
return message.ToolResultMessage(
|
|
215
219
|
status="error",
|
|
216
|
-
|
|
220
|
+
output_text=(
|
|
217
221
|
"<tool_use_error>PDF files are not supported by this tool.\n"
|
|
218
222
|
"If there's an available skill for PDF, use it.\n"
|
|
219
223
|
"Or use a Python script with `pdfplumber` to extract text/tables:\n\n"
|
|
@@ -233,9 +237,9 @@ class ReadTool(ToolABC):
|
|
|
233
237
|
is_image_file = _is_supported_image_file(file_path)
|
|
234
238
|
# Check for binary files (skip for images which are handled separately)
|
|
235
239
|
if not is_image_file and _is_binary_file(file_path):
|
|
236
|
-
return
|
|
240
|
+
return message.ToolResultMessage(
|
|
237
241
|
status="error",
|
|
238
|
-
|
|
242
|
+
output_text=(
|
|
239
243
|
"<tool_use_error>This appears to be a binary file and cannot be read as text. "
|
|
240
244
|
"Use appropriate tools or libraries to handle binary files.</tool_use_error>"
|
|
241
245
|
),
|
|
@@ -247,12 +251,13 @@ class ReadTool(ToolABC):
|
|
|
247
251
|
size_bytes = 0
|
|
248
252
|
|
|
249
253
|
if is_image_file:
|
|
250
|
-
if size_bytes >
|
|
254
|
+
if size_bytes > READ_MAX_IMAGE_BYTES:
|
|
251
255
|
size_mb = size_bytes / (1024 * 1024)
|
|
252
|
-
|
|
256
|
+
limit_mb = READ_MAX_IMAGE_BYTES / (1024 * 1024)
|
|
257
|
+
return message.ToolResultMessage(
|
|
253
258
|
status="error",
|
|
254
|
-
|
|
255
|
-
f"<tool_use_error>Image size ({size_mb:.2f}MB) exceeds maximum supported size (
|
|
259
|
+
output_text=(
|
|
260
|
+
f"<tool_use_error>Image size ({size_mb:.2f}MB) exceeds maximum supported size ({limit_mb:.2f}MB) for inline transfer.</tool_use_error>"
|
|
256
261
|
),
|
|
257
262
|
)
|
|
258
263
|
try:
|
|
@@ -261,16 +266,16 @@ class ReadTool(ToolABC):
|
|
|
261
266
|
image_bytes = image_file.read()
|
|
262
267
|
data_url = f"data:{mime_type};base64,{b64encode(image_bytes).decode('ascii')}"
|
|
263
268
|
except Exception as exc:
|
|
264
|
-
return
|
|
269
|
+
return message.ToolResultMessage(
|
|
265
270
|
status="error",
|
|
266
|
-
|
|
271
|
+
output_text=f"<tool_use_error>Failed to read image file: {exc}</tool_use_error>",
|
|
267
272
|
)
|
|
268
273
|
|
|
269
274
|
_track_file_access(file_path, content_sha256=hashlib.sha256(image_bytes).hexdigest())
|
|
270
275
|
size_kb = size_bytes / 1024.0 if size_bytes else 0.0
|
|
271
276
|
output_text = f"[image] {Path(file_path).name} ({size_kb:.1f}KB)"
|
|
272
|
-
image_part =
|
|
273
|
-
return
|
|
277
|
+
image_part = message.ImageURLPart(url=data_url, id=None)
|
|
278
|
+
return message.ToolResultMessage(status="success", output_text=output_text, parts=[image_part])
|
|
274
279
|
|
|
275
280
|
offset = 1 if args.offset is None or args.offset < 1 else int(args.offset)
|
|
276
281
|
limit = None if args.limit is None else int(args.limit)
|
|
@@ -291,36 +296,36 @@ class ReadTool(ToolABC):
|
|
|
291
296
|
)
|
|
292
297
|
|
|
293
298
|
except FileNotFoundError:
|
|
294
|
-
return
|
|
299
|
+
return message.ToolResultMessage(
|
|
295
300
|
status="error",
|
|
296
|
-
|
|
301
|
+
output_text="<tool_use_error>File does not exist.</tool_use_error>",
|
|
297
302
|
)
|
|
298
303
|
except IsADirectoryError:
|
|
299
|
-
return
|
|
304
|
+
return message.ToolResultMessage(
|
|
300
305
|
status="error",
|
|
301
|
-
|
|
306
|
+
output_text="<tool_use_error>Illegal operation on a directory. read</tool_use_error>",
|
|
302
307
|
)
|
|
303
308
|
|
|
304
309
|
if offset > max(read_result.total_lines, 0):
|
|
305
310
|
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>"
|
|
306
311
|
_track_file_access(file_path, content_sha256=read_result.content_sha256)
|
|
307
|
-
return
|
|
312
|
+
return message.ToolResultMessage(status="success", output_text=warn)
|
|
308
313
|
|
|
309
314
|
lines_out: list[str] = [_format_numbered_line(no, content) for no, content in read_result.selected_lines]
|
|
310
315
|
|
|
311
316
|
# Show truncation info with reason
|
|
312
317
|
if read_result.remaining_due_to_char_limit > 0:
|
|
313
318
|
lines_out.append(
|
|
314
|
-
f"
|
|
319
|
+
f"… ({read_result.remaining_due_to_char_limit} more lines truncated due to {max_chars} char limit, "
|
|
315
320
|
f"file has {read_result.total_lines} lines total, use offset/limit to read other parts)"
|
|
316
321
|
)
|
|
317
322
|
elif read_result.remaining_selected_beyond_cap > 0:
|
|
318
323
|
lines_out.append(
|
|
319
|
-
f"
|
|
324
|
+
f"… ({read_result.remaining_selected_beyond_cap} more lines truncated due to {line_cap} line limit, "
|
|
320
325
|
f"file has {read_result.total_lines} lines total, use offset/limit to read other parts)"
|
|
321
326
|
)
|
|
322
327
|
|
|
323
328
|
read_result_str = "\n".join(lines_out)
|
|
324
329
|
_track_file_access(file_path, content_sha256=read_result.content_sha256)
|
|
325
330
|
|
|
326
|
-
return
|
|
331
|
+
return message.ToolResultMessage(status="success", output_text=read_result_str)
|
|
@@ -12,7 +12,7 @@ from klaude_code.core.tool.file.diff_builder import build_structured_diff
|
|
|
12
12
|
from klaude_code.core.tool.tool_abc import ToolABC, load_desc
|
|
13
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
|
-
from klaude_code.protocol import llm_param, model, tools
|
|
15
|
+
from klaude_code.protocol import llm_param, message, model, tools
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
class WriteArguments(BaseModel):
|
|
@@ -46,18 +46,18 @@ class WriteTool(ToolABC):
|
|
|
46
46
|
)
|
|
47
47
|
|
|
48
48
|
@classmethod
|
|
49
|
-
async def call(cls, arguments: str) ->
|
|
49
|
+
async def call(cls, arguments: str) -> message.ToolResultMessage:
|
|
50
50
|
try:
|
|
51
51
|
args = WriteArguments.model_validate_json(arguments)
|
|
52
52
|
except ValueError as e: # pragma: no cover - defensive
|
|
53
|
-
return
|
|
53
|
+
return message.ToolResultMessage(status="error", output_text=f"Invalid arguments: {e}")
|
|
54
54
|
|
|
55
55
|
file_path = os.path.abspath(args.file_path)
|
|
56
56
|
|
|
57
57
|
if is_directory(file_path):
|
|
58
|
-
return
|
|
58
|
+
return message.ToolResultMessage(
|
|
59
59
|
status="error",
|
|
60
|
-
|
|
60
|
+
output_text="<tool_use_error>Illegal operation on a directory. write</tool_use_error>",
|
|
61
61
|
)
|
|
62
62
|
|
|
63
63
|
file_tracker = get_current_file_tracker()
|
|
@@ -67,9 +67,9 @@ class WriteTool(ToolABC):
|
|
|
67
67
|
if exists:
|
|
68
68
|
tracked_status = file_tracker.get(file_path) if file_tracker is not None else None
|
|
69
69
|
if tracked_status is None:
|
|
70
|
-
return
|
|
70
|
+
return message.ToolResultMessage(
|
|
71
71
|
status="error",
|
|
72
|
-
|
|
72
|
+
output_text=("File has not been read yet. Read it first before writing to it."),
|
|
73
73
|
)
|
|
74
74
|
|
|
75
75
|
# Capture previous content (if any) for diff generation and external-change detection.
|
|
@@ -87,9 +87,9 @@ class WriteTool(ToolABC):
|
|
|
87
87
|
if before_read_ok and tracked_status is not None and tracked_status.content_sha256 is not None:
|
|
88
88
|
current_sha256 = hash_text_sha256(before)
|
|
89
89
|
if current_sha256 != tracked_status.content_sha256:
|
|
90
|
-
return
|
|
90
|
+
return message.ToolResultMessage(
|
|
91
91
|
status="error",
|
|
92
|
-
|
|
92
|
+
output_text=(
|
|
93
93
|
"File has been modified externally. Either by user or a linter. "
|
|
94
94
|
"Read it first before writing to it."
|
|
95
95
|
),
|
|
@@ -101,9 +101,9 @@ class WriteTool(ToolABC):
|
|
|
101
101
|
except OSError:
|
|
102
102
|
current_mtime = tracked_status.mtime
|
|
103
103
|
if current_mtime != tracked_status.mtime:
|
|
104
|
-
return
|
|
104
|
+
return message.ToolResultMessage(
|
|
105
105
|
status="error",
|
|
106
|
-
|
|
106
|
+
output_text=(
|
|
107
107
|
"File has been modified externally. Either by user or a linter. "
|
|
108
108
|
"Read it first before writing to it."
|
|
109
109
|
),
|
|
@@ -112,7 +112,7 @@ class WriteTool(ToolABC):
|
|
|
112
112
|
try:
|
|
113
113
|
await asyncio.to_thread(write_text, file_path, args.content)
|
|
114
114
|
except (OSError, UnicodeError) as e: # pragma: no cover
|
|
115
|
-
return
|
|
115
|
+
return message.ToolResultMessage(status="error", output_text=f"<tool_use_error>{e}</tool_use_error>")
|
|
116
116
|
|
|
117
117
|
if file_tracker is not None:
|
|
118
118
|
with contextlib.suppress(Exception):
|
|
@@ -132,5 +132,5 @@ class WriteTool(ToolABC):
|
|
|
132
132
|
else:
|
|
133
133
|
ui_extra = build_structured_diff(before, args.content, file_path=file_path)
|
|
134
134
|
|
|
135
|
-
|
|
136
|
-
return
|
|
135
|
+
output_msg = f"File {'overwritten' if exists else 'created'} successfully at: {file_path}"
|
|
136
|
+
return message.ToolResultMessage(status="success", output_text=output_msg, ui_extra=ui_extra)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from typing import Any, ClassVar, cast
|
|
4
4
|
|
|
5
|
-
from klaude_code.protocol import llm_param,
|
|
5
|
+
from klaude_code.protocol import llm_param, message, tools
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
def _normalize_schema_types(schema: dict[str, Any]) -> dict[str, Any]:
|
|
@@ -72,13 +72,13 @@ class ReportBackTool:
|
|
|
72
72
|
)
|
|
73
73
|
|
|
74
74
|
@classmethod
|
|
75
|
-
async def call(cls, arguments: str) ->
|
|
75
|
+
async def call(cls, arguments: str) -> message.ToolResultMessage:
|
|
76
76
|
"""Execute the report_back tool.
|
|
77
77
|
|
|
78
78
|
The actual handling of report_back results is done by TurnExecutor.
|
|
79
79
|
This method just returns a success status to maintain the tool call flow.
|
|
80
80
|
"""
|
|
81
|
-
return
|
|
81
|
+
return message.ToolResultMessage(
|
|
82
82
|
status="success",
|
|
83
|
-
|
|
83
|
+
output_text="Result reported successfully. Task will end.",
|
|
84
84
|
)
|
|
@@ -10,12 +10,12 @@ from typing import Any
|
|
|
10
10
|
|
|
11
11
|
from pydantic import BaseModel
|
|
12
12
|
|
|
13
|
-
from klaude_code import
|
|
13
|
+
from klaude_code.const import BASH_DEFAULT_TIMEOUT_MS, BASH_TERMINATE_TIMEOUT_SEC
|
|
14
14
|
from klaude_code.core.tool.shell.command_safety import is_safe_command
|
|
15
15
|
from klaude_code.core.tool.tool_abc import ToolABC, load_desc
|
|
16
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
|
-
from klaude_code.protocol import llm_param, model, tools
|
|
18
|
+
from klaude_code.protocol import llm_param, message, model, tools
|
|
19
19
|
|
|
20
20
|
# Regex to strip ANSI and terminal control sequences from command output
|
|
21
21
|
#
|
|
@@ -58,8 +58,8 @@ class BashTool(ToolABC):
|
|
|
58
58
|
},
|
|
59
59
|
"timeout_ms": {
|
|
60
60
|
"type": "integer",
|
|
61
|
-
"description": f"The timeout for the command in milliseconds, default is {
|
|
62
|
-
"default":
|
|
61
|
+
"description": f"The timeout for the command in milliseconds, default is {BASH_DEFAULT_TIMEOUT_MS}",
|
|
62
|
+
"default": BASH_DEFAULT_TIMEOUT_MS,
|
|
63
63
|
},
|
|
64
64
|
},
|
|
65
65
|
"required": ["command"],
|
|
@@ -68,27 +68,27 @@ class BashTool(ToolABC):
|
|
|
68
68
|
|
|
69
69
|
class BashArguments(BaseModel):
|
|
70
70
|
command: str
|
|
71
|
-
timeout_ms: int =
|
|
71
|
+
timeout_ms: int = BASH_DEFAULT_TIMEOUT_MS
|
|
72
72
|
|
|
73
73
|
@classmethod
|
|
74
|
-
async def call(cls, arguments: str) ->
|
|
74
|
+
async def call(cls, arguments: str) -> message.ToolResultMessage:
|
|
75
75
|
try:
|
|
76
76
|
args = BashTool.BashArguments.model_validate_json(arguments)
|
|
77
77
|
except ValueError as e:
|
|
78
|
-
return
|
|
78
|
+
return message.ToolResultMessage(
|
|
79
79
|
status="error",
|
|
80
|
-
|
|
80
|
+
output_text=f"Invalid arguments: {e}",
|
|
81
81
|
)
|
|
82
82
|
return await cls.call_with_args(args)
|
|
83
83
|
|
|
84
84
|
@classmethod
|
|
85
|
-
async def call_with_args(cls, args: BashArguments) ->
|
|
85
|
+
async def call_with_args(cls, args: BashArguments) -> 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:
|
|
89
|
-
return
|
|
89
|
+
return message.ToolResultMessage(
|
|
90
90
|
status="error",
|
|
91
|
-
|
|
91
|
+
output_text=f"Command rejected: {result.error_msg}",
|
|
92
92
|
)
|
|
93
93
|
|
|
94
94
|
# Run the command using bash -lc so shell semantics work (pipes, &&, etc.)
|
|
@@ -279,7 +279,7 @@ class BashTool(ToolABC):
|
|
|
279
279
|
pass
|
|
280
280
|
|
|
281
281
|
with contextlib.suppress(Exception):
|
|
282
|
-
await asyncio.wait_for(proc.wait(), timeout=
|
|
282
|
+
await asyncio.wait_for(proc.wait(), timeout=BASH_TERMINATE_TIMEOUT_SEC)
|
|
283
283
|
return
|
|
284
284
|
|
|
285
285
|
# Escalate to hard kill if it didn't exit quickly.
|
|
@@ -289,7 +289,7 @@ class BashTool(ToolABC):
|
|
|
289
289
|
else:
|
|
290
290
|
proc.kill()
|
|
291
291
|
with contextlib.suppress(Exception):
|
|
292
|
-
await asyncio.wait_for(proc.wait(), timeout=
|
|
292
|
+
await asyncio.wait_for(proc.wait(), timeout=BASH_TERMINATE_TIMEOUT_SEC)
|
|
293
293
|
|
|
294
294
|
try:
|
|
295
295
|
# Create a dedicated process group so we can terminate the whole tree.
|
|
@@ -311,9 +311,9 @@ class BashTool(ToolABC):
|
|
|
311
311
|
except TimeoutError:
|
|
312
312
|
with contextlib.suppress(Exception):
|
|
313
313
|
await _terminate_process(proc)
|
|
314
|
-
return
|
|
314
|
+
return message.ToolResultMessage(
|
|
315
315
|
status="error",
|
|
316
|
-
|
|
316
|
+
output_text=f"Timeout after {args.timeout_ms} ms running: {args.command}",
|
|
317
317
|
)
|
|
318
318
|
except asyncio.CancelledError:
|
|
319
319
|
# Ensure subprocess is stopped and propagate cancellation.
|
|
@@ -332,11 +332,11 @@ class BashTool(ToolABC):
|
|
|
332
332
|
output = (output + ("\n" if output else "")) + f"[stderr]\n{stderr}"
|
|
333
333
|
|
|
334
334
|
_best_effort_update_file_tracker(args.command)
|
|
335
|
-
return
|
|
335
|
+
return message.ToolResultMessage(
|
|
336
336
|
status="success",
|
|
337
337
|
# Preserve leading whitespace for tools like `nl -ba`.
|
|
338
338
|
# Only trim trailing newlines to avoid adding an extra blank line in the UI.
|
|
339
|
-
|
|
339
|
+
output_text=output.rstrip("\n"),
|
|
340
340
|
)
|
|
341
341
|
else:
|
|
342
342
|
combined = ""
|
|
@@ -346,21 +346,21 @@ class BashTool(ToolABC):
|
|
|
346
346
|
combined += f"[stderr]\n{stderr}"
|
|
347
347
|
if not combined:
|
|
348
348
|
combined = f"Command exited with code {rc}"
|
|
349
|
-
return
|
|
349
|
+
return message.ToolResultMessage(
|
|
350
350
|
status="error",
|
|
351
351
|
# Preserve leading whitespace; only trim trailing newlines.
|
|
352
|
-
|
|
352
|
+
output_text=combined.rstrip("\n"),
|
|
353
353
|
)
|
|
354
354
|
except FileNotFoundError:
|
|
355
|
-
return
|
|
355
|
+
return message.ToolResultMessage(
|
|
356
356
|
status="error",
|
|
357
|
-
|
|
357
|
+
output_text="bash not found on system path",
|
|
358
358
|
)
|
|
359
359
|
except asyncio.CancelledError:
|
|
360
360
|
# Propagate cooperative cancellation so outer layers can handle interrupts correctly.
|
|
361
361
|
raise
|
|
362
362
|
except OSError as e: # safeguard: catch remaining OS-level errors (permissions, resources, etc.)
|
|
363
|
-
return
|
|
363
|
+
return message.ToolResultMessage(
|
|
364
364
|
status="error",
|
|
365
|
-
|
|
365
|
+
output_text=f"Execution error: {e}",
|
|
366
366
|
)
|
|
@@ -6,7 +6,7 @@ from pydantic import BaseModel
|
|
|
6
6
|
|
|
7
7
|
from klaude_code.core.tool.tool_abc import ToolABC, load_desc
|
|
8
8
|
from klaude_code.core.tool.tool_registry import register
|
|
9
|
-
from klaude_code.protocol import llm_param,
|
|
9
|
+
from klaude_code.protocol import llm_param, message, tools
|
|
10
10
|
from klaude_code.skill import get_available_skills, get_skill, list_skill_names
|
|
11
11
|
|
|
12
12
|
|
|
@@ -55,23 +55,23 @@ class SkillTool(ToolABC):
|
|
|
55
55
|
command: str
|
|
56
56
|
|
|
57
57
|
@classmethod
|
|
58
|
-
async def call(cls, arguments: str) ->
|
|
58
|
+
async def call(cls, arguments: str) -> message.ToolResultMessage:
|
|
59
59
|
"""Load and return full skill content."""
|
|
60
60
|
try:
|
|
61
61
|
args = cls.SkillArguments.model_validate_json(arguments)
|
|
62
62
|
except ValueError as e:
|
|
63
|
-
return
|
|
63
|
+
return message.ToolResultMessage(
|
|
64
64
|
status="error",
|
|
65
|
-
|
|
65
|
+
output_text=f"Invalid arguments: {e}",
|
|
66
66
|
)
|
|
67
67
|
|
|
68
68
|
skill = get_skill(args.command)
|
|
69
69
|
|
|
70
70
|
if not skill:
|
|
71
71
|
available = ", ".join(list_skill_names())
|
|
72
|
-
return
|
|
72
|
+
return message.ToolResultMessage(
|
|
73
73
|
status="error",
|
|
74
|
-
|
|
74
|
+
output_text=f"Skill '{args.command}' does not exist. Available skills: {available}",
|
|
75
75
|
)
|
|
76
76
|
|
|
77
77
|
# Get base directory from skill_path
|
|
@@ -84,4 +84,4 @@ class SkillTool(ToolABC):
|
|
|
84
84
|
Base directory for this skill: {base_dir}
|
|
85
85
|
|
|
86
86
|
{skill.to_prompt()}"""
|
|
87
|
-
return
|
|
87
|
+
return message.ToolResultMessage(status="success", output_text=result)
|
|
@@ -8,11 +8,12 @@ from __future__ import annotations
|
|
|
8
8
|
|
|
9
9
|
import asyncio
|
|
10
10
|
import json
|
|
11
|
-
from typing import TYPE_CHECKING, ClassVar
|
|
11
|
+
from typing import TYPE_CHECKING, Any, ClassVar, cast
|
|
12
12
|
|
|
13
13
|
from klaude_code.core.tool.tool_abc import ToolABC, ToolConcurrencyPolicy, ToolMetadata
|
|
14
|
-
from klaude_code.core.tool.tool_context import current_run_subtask_callback
|
|
15
|
-
from klaude_code.protocol import llm_param, model
|
|
14
|
+
from klaude_code.core.tool.tool_context import current_run_subtask_callback, current_sub_agent_resume_claims
|
|
15
|
+
from klaude_code.protocol import llm_param, message, model
|
|
16
|
+
from klaude_code.session.session import Session
|
|
16
17
|
|
|
17
18
|
if TYPE_CHECKING:
|
|
18
19
|
from klaude_code.protocol.sub_agent import SubAgentProfile
|
|
@@ -51,22 +52,48 @@ class SubAgentTool(ToolABC):
|
|
|
51
52
|
)
|
|
52
53
|
|
|
53
54
|
@classmethod
|
|
54
|
-
async def call(cls, arguments: str) ->
|
|
55
|
+
async def call(cls, arguments: str) -> message.ToolResultMessage:
|
|
55
56
|
profile = cls._profile
|
|
56
57
|
|
|
57
58
|
try:
|
|
58
59
|
args = json.loads(arguments)
|
|
59
60
|
except json.JSONDecodeError as e:
|
|
60
|
-
return
|
|
61
|
+
return message.ToolResultMessage(status="error", output_text=f"Invalid JSON arguments: {e}")
|
|
61
62
|
|
|
62
63
|
runner = current_run_subtask_callback.get()
|
|
63
64
|
if runner is None:
|
|
64
|
-
return
|
|
65
|
+
return message.ToolResultMessage(status="error", output_text="No subtask runner available in this context")
|
|
65
66
|
|
|
66
67
|
# Build the prompt using the profile's prompt builder
|
|
67
68
|
prompt = profile.prompt_builder(args)
|
|
68
69
|
description = args.get("description", "")
|
|
69
70
|
|
|
71
|
+
resume_raw = args.get("resume")
|
|
72
|
+
resume_session_id: str | None = None
|
|
73
|
+
if isinstance(resume_raw, str) and resume_raw.strip():
|
|
74
|
+
try:
|
|
75
|
+
resume_session_id = Session.resolve_sub_agent_session_id(resume_raw)
|
|
76
|
+
except ValueError as exc:
|
|
77
|
+
return message.ToolResultMessage(status="error", output_text=str(exc))
|
|
78
|
+
|
|
79
|
+
claims = current_sub_agent_resume_claims.get()
|
|
80
|
+
if claims is not None:
|
|
81
|
+
if resume_session_id in claims:
|
|
82
|
+
return message.ToolResultMessage(
|
|
83
|
+
status="error",
|
|
84
|
+
output_text=(
|
|
85
|
+
"Duplicate sub-agent resume in the same response: "
|
|
86
|
+
f"resume='{resume_raw.strip()}' (resolved='{resume_session_id[:7]}…'). "
|
|
87
|
+
"Merge into a single call or resume in a later turn."
|
|
88
|
+
),
|
|
89
|
+
)
|
|
90
|
+
claims.add(resume_session_id)
|
|
91
|
+
|
|
92
|
+
generation = args.get("generation")
|
|
93
|
+
generation_dict: dict[str, Any] | None = (
|
|
94
|
+
cast(dict[str, Any], generation) if isinstance(generation, dict) else None
|
|
95
|
+
)
|
|
96
|
+
|
|
70
97
|
# Extract output_schema if configured
|
|
71
98
|
output_schema = None
|
|
72
99
|
if profile.output_schema_arg:
|
|
@@ -78,17 +105,19 @@ class SubAgentTool(ToolABC):
|
|
|
78
105
|
sub_agent_type=profile.name,
|
|
79
106
|
sub_agent_desc=description,
|
|
80
107
|
sub_agent_prompt=prompt,
|
|
108
|
+
resume=resume_session_id,
|
|
81
109
|
output_schema=output_schema,
|
|
110
|
+
generation=generation_dict,
|
|
82
111
|
)
|
|
83
112
|
)
|
|
84
113
|
except asyncio.CancelledError:
|
|
85
114
|
raise
|
|
86
115
|
except Exception as e:
|
|
87
|
-
return
|
|
116
|
+
return message.ToolResultMessage(status="error", output_text=f"Failed to run subtask: {e}")
|
|
88
117
|
|
|
89
|
-
return
|
|
118
|
+
return message.ToolResultMessage(
|
|
90
119
|
status="success" if not result.error else "error",
|
|
91
|
-
|
|
120
|
+
output_text=result.task_result,
|
|
92
121
|
ui_extra=model.SessionIdUIExtra(session_id=result.session_id),
|
|
93
122
|
task_metadata=result.task_metadata,
|
|
94
123
|
)
|
|
@@ -5,7 +5,7 @@ from pydantic import BaseModel
|
|
|
5
5
|
from klaude_code.core.tool.tool_abc import ToolABC, load_desc
|
|
6
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
|
-
from klaude_code.protocol import llm_param, model, tools
|
|
8
|
+
from klaude_code.protocol import llm_param, message, model, tools
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
def get_new_completed_todos(old_todos: list[model.TodoItem], new_todos: list[model.TodoItem]) -> list[str]:
|
|
@@ -77,21 +77,21 @@ class TodoWriteTool(ToolABC):
|
|
|
77
77
|
)
|
|
78
78
|
|
|
79
79
|
@classmethod
|
|
80
|
-
async def call(cls, arguments: str) ->
|
|
80
|
+
async def call(cls, arguments: str) -> message.ToolResultMessage:
|
|
81
81
|
try:
|
|
82
82
|
args = TodoWriteArguments.model_validate_json(arguments)
|
|
83
83
|
except ValueError as e:
|
|
84
|
-
return
|
|
84
|
+
return message.ToolResultMessage(
|
|
85
85
|
status="error",
|
|
86
|
-
|
|
86
|
+
output_text=f"Invalid arguments: {e}",
|
|
87
87
|
)
|
|
88
88
|
|
|
89
89
|
# Get current todo context to store todos
|
|
90
90
|
todo_context = get_current_todo_context()
|
|
91
91
|
if todo_context is None:
|
|
92
|
-
return
|
|
92
|
+
return message.ToolResultMessage(
|
|
93
93
|
status="error",
|
|
94
|
-
|
|
94
|
+
output_text="No active session found",
|
|
95
95
|
)
|
|
96
96
|
|
|
97
97
|
# Get current todos before updating (for comparison)
|
|
@@ -113,9 +113,9 @@ Your todo list has changed. DO NOT mention this explicitly to the user. Here are
|
|
|
113
113
|
{model.todo_list_str(args.todos)}. Continue on with the tasks at hand if applicable.
|
|
114
114
|
</system-reminder>"""
|
|
115
115
|
|
|
116
|
-
return
|
|
116
|
+
return message.ToolResultMessage(
|
|
117
117
|
status="success",
|
|
118
|
-
|
|
118
|
+
output_text=response,
|
|
119
119
|
ui_extra=model.TodoListUIExtra(todo_list=ui_extra),
|
|
120
120
|
side_effects=[model.ToolSideEffect.TODO_CHANGE],
|
|
121
121
|
)
|