klaude-code 1.2.1__py3-none-any.whl → 1.2.3__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/cli/main.py +9 -4
- klaude_code/cli/runtime.py +42 -43
- klaude_code/command/__init__.py +7 -5
- klaude_code/command/clear_cmd.py +6 -29
- klaude_code/command/command_abc.py +44 -8
- klaude_code/command/diff_cmd.py +33 -27
- klaude_code/command/export_cmd.py +18 -26
- klaude_code/command/help_cmd.py +10 -8
- klaude_code/command/model_cmd.py +11 -40
- klaude_code/command/{prompt-update-dev-doc.md → prompt-dev-docs-update.md} +3 -2
- klaude_code/command/{prompt-dev-doc.md → prompt-dev-docs.md} +3 -2
- klaude_code/command/prompt-init.md +2 -5
- klaude_code/command/prompt_command.py +6 -6
- klaude_code/command/refresh_cmd.py +4 -5
- klaude_code/command/registry.py +16 -19
- klaude_code/command/terminal_setup_cmd.py +12 -11
- klaude_code/config/__init__.py +4 -0
- klaude_code/config/config.py +25 -26
- klaude_code/config/list_model.py +8 -3
- klaude_code/config/select_model.py +1 -1
- klaude_code/const/__init__.py +1 -1
- klaude_code/core/__init__.py +0 -3
- klaude_code/core/agent.py +25 -50
- klaude_code/core/executor.py +268 -101
- klaude_code/core/prompt.py +12 -12
- klaude_code/core/{prompt → prompts}/prompt-gemini.md +1 -1
- klaude_code/core/reminders.py +76 -95
- klaude_code/core/task.py +21 -14
- klaude_code/core/tool/__init__.py +45 -11
- klaude_code/core/tool/file/apply_patch.py +5 -1
- klaude_code/core/tool/file/apply_patch_tool.py +11 -13
- klaude_code/core/tool/file/edit_tool.py +27 -23
- klaude_code/core/tool/file/multi_edit_tool.py +15 -17
- klaude_code/core/tool/file/read_tool.py +41 -36
- klaude_code/core/tool/file/write_tool.py +13 -15
- klaude_code/core/tool/memory/memory_tool.py +85 -68
- klaude_code/core/tool/memory/skill_tool.py +10 -12
- klaude_code/core/tool/shell/bash_tool.py +24 -22
- klaude_code/core/tool/shell/command_safety.py +12 -1
- klaude_code/core/tool/sub_agent_tool.py +11 -12
- klaude_code/core/tool/todo/todo_write_tool.py +21 -28
- klaude_code/core/tool/todo/update_plan_tool.py +14 -24
- klaude_code/core/tool/tool_abc.py +3 -4
- klaude_code/core/tool/tool_context.py +7 -7
- klaude_code/core/tool/tool_registry.py +30 -47
- klaude_code/core/tool/tool_runner.py +35 -43
- klaude_code/core/tool/truncation.py +14 -20
- klaude_code/core/tool/web/mermaid_tool.py +12 -14
- klaude_code/core/tool/web/web_fetch_tool.py +15 -17
- klaude_code/core/turn.py +19 -7
- klaude_code/llm/__init__.py +3 -4
- klaude_code/llm/anthropic/client.py +30 -46
- klaude_code/llm/anthropic/input.py +4 -11
- klaude_code/llm/client.py +29 -8
- klaude_code/llm/input_common.py +66 -36
- klaude_code/llm/openai_compatible/client.py +42 -84
- klaude_code/llm/openai_compatible/input.py +11 -16
- klaude_code/llm/openai_compatible/tool_call_accumulator.py +2 -2
- klaude_code/llm/openrouter/client.py +40 -289
- klaude_code/llm/openrouter/input.py +13 -35
- klaude_code/llm/openrouter/reasoning_handler.py +209 -0
- klaude_code/llm/registry.py +5 -75
- klaude_code/llm/responses/client.py +34 -55
- klaude_code/llm/responses/input.py +24 -26
- klaude_code/llm/usage.py +109 -0
- klaude_code/protocol/__init__.py +4 -0
- klaude_code/protocol/events.py +3 -2
- klaude_code/protocol/{llm_parameter.py → llm_param.py} +12 -32
- klaude_code/protocol/model.py +49 -4
- klaude_code/protocol/op.py +18 -16
- klaude_code/protocol/op_handler.py +28 -0
- klaude_code/{core → protocol}/sub_agent.py +7 -0
- klaude_code/session/export.py +150 -70
- klaude_code/session/session.py +28 -14
- klaude_code/session/templates/export_session.html +180 -42
- klaude_code/trace/__init__.py +2 -2
- klaude_code/trace/log.py +11 -5
- klaude_code/ui/__init__.py +91 -8
- klaude_code/ui/core/__init__.py +1 -0
- klaude_code/ui/core/display.py +103 -0
- klaude_code/ui/core/input.py +71 -0
- klaude_code/ui/modes/__init__.py +1 -0
- klaude_code/ui/modes/debug/__init__.py +1 -0
- klaude_code/ui/{base/debug_event_display.py → modes/debug/display.py} +9 -5
- klaude_code/ui/modes/exec/__init__.py +1 -0
- klaude_code/ui/{base/exec_display.py → modes/exec/display.py} +28 -2
- klaude_code/ui/{repl → modes/repl}/__init__.py +5 -6
- klaude_code/ui/modes/repl/clipboard.py +152 -0
- klaude_code/ui/modes/repl/completers.py +429 -0
- klaude_code/ui/modes/repl/display.py +60 -0
- klaude_code/ui/modes/repl/event_handler.py +375 -0
- klaude_code/ui/modes/repl/input_prompt_toolkit.py +198 -0
- klaude_code/ui/modes/repl/key_bindings.py +170 -0
- klaude_code/ui/{repl → modes/repl}/renderer.py +109 -132
- klaude_code/ui/renderers/assistant.py +21 -0
- klaude_code/ui/renderers/common.py +0 -16
- klaude_code/ui/renderers/developer.py +18 -18
- klaude_code/ui/renderers/diffs.py +36 -14
- klaude_code/ui/renderers/errors.py +1 -1
- klaude_code/ui/renderers/metadata.py +50 -27
- klaude_code/ui/renderers/sub_agent.py +43 -9
- klaude_code/ui/renderers/thinking.py +33 -1
- klaude_code/ui/renderers/tools.py +212 -20
- klaude_code/ui/renderers/user_input.py +19 -23
- klaude_code/ui/rich/__init__.py +1 -0
- klaude_code/ui/{rich_ext → rich}/searchable_text.py +3 -1
- klaude_code/ui/{renderers → rich}/status.py +29 -18
- klaude_code/ui/{base → rich}/theme.py +8 -2
- klaude_code/ui/terminal/__init__.py +1 -0
- klaude_code/ui/{base/terminal_color.py → terminal/color.py} +4 -1
- klaude_code/ui/{base/terminal_control.py → terminal/control.py} +1 -0
- klaude_code/ui/{base/terminal_notifier.py → terminal/notifier.py} +5 -2
- klaude_code/ui/utils/__init__.py +1 -0
- klaude_code/ui/{base/utils.py → utils/common.py} +35 -3
- {klaude_code-1.2.1.dist-info → klaude_code-1.2.3.dist-info}/METADATA +1 -1
- klaude_code-1.2.3.dist-info/RECORD +161 -0
- klaude_code/core/clipboard_manifest.py +0 -124
- klaude_code/llm/openrouter/tool_call_accumulator.py +0 -80
- klaude_code/ui/base/__init__.py +0 -1
- klaude_code/ui/base/display_abc.py +0 -36
- klaude_code/ui/base/input_abc.py +0 -20
- klaude_code/ui/repl/display.py +0 -36
- klaude_code/ui/repl/event_handler.py +0 -247
- klaude_code/ui/repl/input.py +0 -773
- klaude_code/ui/rich_ext/__init__.py +0 -1
- klaude_code-1.2.1.dist-info/RECORD +0 -151
- /klaude_code/core/{prompt → prompts}/prompt-claude-code.md +0 -0
- /klaude_code/core/{prompt → prompts}/prompt-codex.md +0 -0
- /klaude_code/core/{prompt → prompts}/prompt-subagent-explore.md +0 -0
- /klaude_code/core/{prompt → prompts}/prompt-subagent-oracle.md +0 -0
- /klaude_code/core/{prompt → prompts}/prompt-subagent-webfetch.md +0 -0
- /klaude_code/core/{prompt → prompts}/prompt-subagent.md +0 -0
- /klaude_code/ui/{base → core}/stage_manager.py +0 -0
- /klaude_code/ui/{rich_ext → rich}/live.py +0 -0
- /klaude_code/ui/{rich_ext → rich}/markdown.py +0 -0
- /klaude_code/ui/{rich_ext → rich}/quote.py +0 -0
- /klaude_code/ui/{base → terminal}/progress_bar.py +0 -0
- /klaude_code/ui/{base → utils}/debouncer.py +0 -0
- {klaude_code-1.2.1.dist-info → klaude_code-1.2.3.dist-info}/WHEEL +0 -0
- {klaude_code-1.2.1.dist-info → klaude_code-1.2.3.dist-info}/entry_points.txt +0 -0
klaude_code/core/reminders.py
CHANGED
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
import json
|
|
2
|
-
import re
|
|
3
2
|
from pathlib import Path
|
|
4
3
|
from typing import Awaitable, Callable
|
|
5
4
|
|
|
6
5
|
from pydantic import BaseModel
|
|
7
6
|
|
|
8
|
-
from klaude_code
|
|
9
|
-
from klaude_code.core.
|
|
10
|
-
from klaude_code.core.tool.file.read_tool import ReadTool
|
|
11
|
-
from klaude_code.core.tool.shell.bash_tool import BashTool
|
|
12
|
-
from klaude_code.core.tool.tool_context import reset_tool_context, set_tool_context_from_session
|
|
7
|
+
from klaude_code import const
|
|
8
|
+
from klaude_code.core.tool import BashTool, ReadTool, reset_tool_context, set_tool_context_from_session
|
|
13
9
|
from klaude_code.protocol import model, tools
|
|
14
10
|
from klaude_code.session import Session
|
|
15
11
|
|
|
@@ -30,7 +26,9 @@ def get_last_new_user_input(session: Session) -> str | None:
|
|
|
30
26
|
return "\n\n".join(result)
|
|
31
27
|
|
|
32
28
|
|
|
33
|
-
async def at_file_reader_reminder(
|
|
29
|
+
async def at_file_reader_reminder(
|
|
30
|
+
session: Session,
|
|
31
|
+
) -> model.DeveloperMessageItem | None:
|
|
34
32
|
"""Parse @foo/bar to read"""
|
|
35
33
|
last_user_input = get_last_new_user_input(session)
|
|
36
34
|
if not last_user_input or "@" not in last_user_input.strip():
|
|
@@ -124,7 +122,9 @@ async def empty_todo_reminder(session: Session) -> model.DeveloperMessageItem |
|
|
|
124
122
|
return None
|
|
125
123
|
|
|
126
124
|
|
|
127
|
-
async def todo_not_used_recently_reminder(
|
|
125
|
+
async def todo_not_used_recently_reminder(
|
|
126
|
+
session: Session,
|
|
127
|
+
) -> model.DeveloperMessageItem | None:
|
|
128
128
|
"""Remind agent to use TodoWrite tool if it hasn't been used recently (>=10 other tool calls), with cooldown.
|
|
129
129
|
|
|
130
130
|
Cooldown behavior:
|
|
@@ -147,10 +147,10 @@ async def todo_not_used_recently_reminder(session: Session) -> model.DeveloperMe
|
|
|
147
147
|
if item.name in (tools.TODO_WRITE, tools.UPDATE_PLAN):
|
|
148
148
|
break
|
|
149
149
|
other_tool_call_count_befor_last_todo += 1
|
|
150
|
-
if other_tool_call_count_befor_last_todo >= TODO_REMINDER_TOOL_CALL_THRESHOLD:
|
|
150
|
+
if other_tool_call_count_befor_last_todo >= const.TODO_REMINDER_TOOL_CALL_THRESHOLD:
|
|
151
151
|
break
|
|
152
152
|
|
|
153
|
-
not_used_recently = other_tool_call_count_befor_last_todo >= TODO_REMINDER_TOOL_CALL_THRESHOLD
|
|
153
|
+
not_used_recently = other_tool_call_count_befor_last_todo >= const.TODO_REMINDER_TOOL_CALL_THRESHOLD
|
|
154
154
|
|
|
155
155
|
if not not_used_recently:
|
|
156
156
|
return None
|
|
@@ -173,7 +173,9 @@ Here are the existing contents of your todo list:
|
|
|
173
173
|
return None
|
|
174
174
|
|
|
175
175
|
|
|
176
|
-
async def file_changed_externally_reminder(
|
|
176
|
+
async def file_changed_externally_reminder(
|
|
177
|
+
session: Session,
|
|
178
|
+
) -> model.DeveloperMessageItem | None:
|
|
177
179
|
"""Remind agent about user/linter' changes to the files in FileTracker, provding the newest content of the file."""
|
|
178
180
|
changed_files: list[tuple[str, str, list[model.ImageURLPart] | None]] = []
|
|
179
181
|
collected_images: list[model.ImageURLPart] = []
|
|
@@ -192,7 +194,13 @@ async def file_changed_externally_reminder(session: Session) -> model.DeveloperM
|
|
|
192
194
|
collected_images.extend(tool_result.images)
|
|
193
195
|
finally:
|
|
194
196
|
reset_tool_context(context_token)
|
|
195
|
-
except (
|
|
197
|
+
except (
|
|
198
|
+
FileNotFoundError,
|
|
199
|
+
IsADirectoryError,
|
|
200
|
+
OSError,
|
|
201
|
+
PermissionError,
|
|
202
|
+
UnicodeDecodeError,
|
|
203
|
+
):
|
|
196
204
|
continue
|
|
197
205
|
if len(changed_files) > 0:
|
|
198
206
|
changed_files_str = "\n\n".join(
|
|
@@ -213,8 +221,14 @@ async def file_changed_externally_reminder(session: Session) -> model.DeveloperM
|
|
|
213
221
|
|
|
214
222
|
def get_memory_paths() -> list[tuple[Path, str]]:
|
|
215
223
|
return [
|
|
216
|
-
(
|
|
217
|
-
|
|
224
|
+
(
|
|
225
|
+
Path.home() / ".claude" / "CLAUDE.md",
|
|
226
|
+
"user's private global instructions for all projects",
|
|
227
|
+
),
|
|
228
|
+
(
|
|
229
|
+
Path.home() / ".codex" / "AGENTS.md",
|
|
230
|
+
"user's private global instructions for all projects",
|
|
231
|
+
),
|
|
218
232
|
(Path.cwd() / "AGENTS.md", "project instructions, checked into the codebase"),
|
|
219
233
|
(Path.cwd() / "AGENT.md", "project instructions, checked into the codebase"),
|
|
220
234
|
(Path.cwd() / "CLAUDE.md", "project instructions, checked into the codebase"),
|
|
@@ -227,6 +241,28 @@ class Memory(BaseModel):
|
|
|
227
241
|
content: str
|
|
228
242
|
|
|
229
243
|
|
|
244
|
+
def get_last_user_message_image_count(session: Session) -> int:
|
|
245
|
+
"""Get image count from the last user message in conversation history."""
|
|
246
|
+
for item in reversed(session.conversation_history):
|
|
247
|
+
if isinstance(item, model.ToolResultItem):
|
|
248
|
+
return 0
|
|
249
|
+
if isinstance(item, model.UserMessageItem):
|
|
250
|
+
return len(item.images) if item.images else 0
|
|
251
|
+
return 0
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
async def image_reminder(session: Session) -> model.DeveloperMessageItem | None:
|
|
255
|
+
"""Remind agent about images attached by user in the last message."""
|
|
256
|
+
image_count = get_last_user_message_image_count(session)
|
|
257
|
+
if image_count == 0:
|
|
258
|
+
return None
|
|
259
|
+
|
|
260
|
+
return model.DeveloperMessageItem(
|
|
261
|
+
content=f"<system-reminder>User attached {image_count} image{'s' if image_count > 1 else ''} in their message. Make sure to analyze and reference these images as needed.</system-reminder>",
|
|
262
|
+
user_image_count=image_count,
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
|
|
230
266
|
async def memory_reminder(session: Session) -> model.DeveloperMessageItem | None:
|
|
231
267
|
"""CLAUDE.md AGENTS.md"""
|
|
232
268
|
memory_paths = get_memory_paths()
|
|
@@ -268,7 +304,14 @@ def get_last_turn_tool_call(session: Session) -> list[model.ToolCallItem]:
|
|
|
268
304
|
for item in reversed(session.conversation_history):
|
|
269
305
|
if isinstance(item, model.ToolCallItem):
|
|
270
306
|
tool_calls.append(item)
|
|
271
|
-
if isinstance(
|
|
307
|
+
if isinstance(
|
|
308
|
+
item,
|
|
309
|
+
(
|
|
310
|
+
model.ReasoningEncryptedItem,
|
|
311
|
+
model.ReasoningTextItem,
|
|
312
|
+
model.AssistantMessageItem,
|
|
313
|
+
),
|
|
314
|
+
):
|
|
272
315
|
break
|
|
273
316
|
return tool_calls
|
|
274
317
|
|
|
@@ -276,7 +319,9 @@ def get_last_turn_tool_call(session: Session) -> list[model.ToolCallItem]:
|
|
|
276
319
|
MEMORY_FILE_NAMES = ["CLAUDE.md", "AGENTS.md", "AGENT.md"]
|
|
277
320
|
|
|
278
321
|
|
|
279
|
-
async def last_path_memory_reminder(
|
|
322
|
+
async def last_path_memory_reminder(
|
|
323
|
+
session: Session,
|
|
324
|
+
) -> model.DeveloperMessageItem | None:
|
|
280
325
|
"""When last turn tool call entered a directory (or parent directory) with CLAUDE.md AGENTS.md"""
|
|
281
326
|
tool_calls = get_last_turn_tool_call(session)
|
|
282
327
|
if len(tool_calls) == 0:
|
|
@@ -356,63 +401,6 @@ async def last_path_memory_reminder(session: Session) -> model.DeveloperMessageI
|
|
|
356
401
|
)
|
|
357
402
|
|
|
358
403
|
|
|
359
|
-
async def clipboard_image_reminder(session: Session) -> model.DeveloperMessageItem | None:
|
|
360
|
-
"""Parse [Image #N] and attach images from clipboard history."""
|
|
361
|
-
last_user_input = get_last_new_user_input(session)
|
|
362
|
-
if not last_user_input or "[Image #" not in last_user_input:
|
|
363
|
-
return None
|
|
364
|
-
|
|
365
|
-
manifest = load_latest_clipboard_manifest()
|
|
366
|
-
if manifest is None:
|
|
367
|
-
return None
|
|
368
|
-
manifest_source = manifest.source_id
|
|
369
|
-
current_source = next_session_token()
|
|
370
|
-
if manifest_source and manifest_source != current_source:
|
|
371
|
-
return None
|
|
372
|
-
image_map = manifest.tag_map()
|
|
373
|
-
if not image_map:
|
|
374
|
-
return None
|
|
375
|
-
|
|
376
|
-
collected_images: list[model.ImageURLPart] = []
|
|
377
|
-
|
|
378
|
-
# Find all tokens
|
|
379
|
-
# Regex for [Image #(\d+)]
|
|
380
|
-
matches = re.findall(r"\[Image #(\d+)\]", last_user_input)
|
|
381
|
-
requested_tags = [f"[Image #{num}]" for num in matches]
|
|
382
|
-
|
|
383
|
-
processed_paths: set[str] = set()
|
|
384
|
-
|
|
385
|
-
attached_tags: list[str] = []
|
|
386
|
-
|
|
387
|
-
for tag in requested_tags:
|
|
388
|
-
if tag in image_map:
|
|
389
|
-
path = image_map[tag]
|
|
390
|
-
if path in processed_paths:
|
|
391
|
-
continue
|
|
392
|
-
|
|
393
|
-
context_token = set_tool_context_from_session(session)
|
|
394
|
-
try:
|
|
395
|
-
# We use ReadTool to get the image object in the correct format
|
|
396
|
-
# This assumes ReadTool handles image files correctly
|
|
397
|
-
args = ReadTool.ReadArguments(file_path=path)
|
|
398
|
-
tool_result = await ReadTool.call_with_args(args)
|
|
399
|
-
if tool_result.images:
|
|
400
|
-
collected_images.extend(tool_result.images)
|
|
401
|
-
processed_paths.add(path)
|
|
402
|
-
attached_tags.append(tag)
|
|
403
|
-
finally:
|
|
404
|
-
reset_tool_context(context_token)
|
|
405
|
-
|
|
406
|
-
if not collected_images:
|
|
407
|
-
return None
|
|
408
|
-
|
|
409
|
-
return model.DeveloperMessageItem(
|
|
410
|
-
content="",
|
|
411
|
-
images=collected_images,
|
|
412
|
-
clipboard_images=attached_tags,
|
|
413
|
-
)
|
|
414
|
-
|
|
415
|
-
|
|
416
404
|
ALL_REMINDERS = [
|
|
417
405
|
empty_todo_reminder,
|
|
418
406
|
todo_not_used_recently_reminder,
|
|
@@ -420,19 +408,27 @@ ALL_REMINDERS = [
|
|
|
420
408
|
memory_reminder,
|
|
421
409
|
last_path_memory_reminder,
|
|
422
410
|
at_file_reader_reminder,
|
|
423
|
-
|
|
411
|
+
image_reminder,
|
|
424
412
|
]
|
|
425
413
|
|
|
426
414
|
|
|
427
|
-
def
|
|
428
|
-
|
|
415
|
+
def load_agent_reminders(
|
|
416
|
+
model_name: str, sub_agent_type: str | None = None, *, vanilla: bool = False
|
|
417
|
+
) -> list[Reminder]:
|
|
418
|
+
"""Get reminders for an agent based on model and agent type.
|
|
429
419
|
|
|
420
|
+
Args:
|
|
421
|
+
model_name: The model name.
|
|
422
|
+
sub_agent_type: If None, returns main agent reminders. Otherwise returns sub-agent reminders.
|
|
423
|
+
vanilla: If True, returns minimal vanilla reminders (ignores sub_agent_type).
|
|
424
|
+
"""
|
|
425
|
+
if vanilla:
|
|
426
|
+
return [at_file_reader_reminder]
|
|
430
427
|
|
|
431
|
-
def get_main_agent_reminders(model_name: str) -> list[Reminder]:
|
|
432
428
|
reminders: list[Reminder] = []
|
|
433
429
|
|
|
434
|
-
#
|
|
435
|
-
if "gpt-5" not in model_name:
|
|
430
|
+
# Only main agent (not sub-agent) gets todo reminders, and not for GPT-5
|
|
431
|
+
if sub_agent_type is None and "gpt-5" not in model_name:
|
|
436
432
|
reminders.append(empty_todo_reminder)
|
|
437
433
|
reminders.append(todo_not_used_recently_reminder)
|
|
438
434
|
|
|
@@ -441,23 +437,8 @@ def get_main_agent_reminders(model_name: str) -> list[Reminder]:
|
|
|
441
437
|
memory_reminder,
|
|
442
438
|
last_path_memory_reminder,
|
|
443
439
|
at_file_reader_reminder,
|
|
444
|
-
clipboard_image_reminder,
|
|
445
|
-
file_changed_externally_reminder,
|
|
446
|
-
]
|
|
447
|
-
)
|
|
448
|
-
|
|
449
|
-
return reminders
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
def get_sub_agent_reminders(model_name: str) -> list[Reminder]:
|
|
453
|
-
reminders: list[Reminder] = []
|
|
454
|
-
reminders.extend(
|
|
455
|
-
[
|
|
456
|
-
memory_reminder,
|
|
457
|
-
last_path_memory_reminder,
|
|
458
|
-
at_file_reader_reminder,
|
|
459
|
-
clipboard_image_reminder,
|
|
460
440
|
file_changed_externally_reminder,
|
|
441
|
+
image_reminder,
|
|
461
442
|
]
|
|
462
443
|
)
|
|
463
444
|
|
klaude_code/core/task.py
CHANGED
|
@@ -6,10 +6,9 @@ from collections.abc import AsyncGenerator, Callable, MutableMapping, Sequence
|
|
|
6
6
|
from dataclasses import dataclass
|
|
7
7
|
from typing import TYPE_CHECKING
|
|
8
8
|
|
|
9
|
-
from klaude_code
|
|
9
|
+
from klaude_code import const
|
|
10
10
|
from klaude_code.core.reminders import Reminder
|
|
11
|
-
from klaude_code.core.tool
|
|
12
|
-
from klaude_code.core.tool.tool_context import TodoContext
|
|
11
|
+
from klaude_code.core.tool import TodoContext, ToolABC
|
|
13
12
|
from klaude_code.core.turn import TurnError, TurnExecutionContext, TurnExecutor
|
|
14
13
|
from klaude_code.protocol import events, model
|
|
15
14
|
from klaude_code.trace import DebugType, log_debug
|
|
@@ -63,6 +62,16 @@ class MetadataAccumulator:
|
|
|
63
62
|
self._throughput_weighted_sum += usage.throughput_tps * current_output
|
|
64
63
|
self._throughput_tracked_tokens += current_output
|
|
65
64
|
|
|
65
|
+
# Accumulate costs
|
|
66
|
+
if usage.input_cost is not None:
|
|
67
|
+
acc_usage.input_cost = (acc_usage.input_cost or 0.0) + usage.input_cost
|
|
68
|
+
if usage.output_cost is not None:
|
|
69
|
+
acc_usage.output_cost = (acc_usage.output_cost or 0.0) + usage.output_cost
|
|
70
|
+
if usage.cache_read_cost is not None:
|
|
71
|
+
acc_usage.cache_read_cost = (acc_usage.cache_read_cost or 0.0) + usage.cache_read_cost
|
|
72
|
+
if usage.total_cost is not None:
|
|
73
|
+
acc_usage.total_cost = (acc_usage.total_cost or 0.0) + usage.total_cost
|
|
74
|
+
|
|
66
75
|
if turn_metadata.provider is not None:
|
|
67
76
|
accumulated.provider = turn_metadata.provider
|
|
68
77
|
if turn_metadata.model_name:
|
|
@@ -79,9 +88,7 @@ class MetadataAccumulator:
|
|
|
79
88
|
accumulated = self._accumulated
|
|
80
89
|
if accumulated.usage is not None:
|
|
81
90
|
if self._throughput_tracked_tokens > 0:
|
|
82
|
-
accumulated.usage.throughput_tps =
|
|
83
|
-
self._throughput_weighted_sum / self._throughput_tracked_tokens
|
|
84
|
-
)
|
|
91
|
+
accumulated.usage.throughput_tps = self._throughput_weighted_sum / self._throughput_tracked_tokens
|
|
85
92
|
else:
|
|
86
93
|
accumulated.usage.throughput_tps = None
|
|
87
94
|
|
|
@@ -128,7 +135,7 @@ class TaskExecutor:
|
|
|
128
135
|
self._current_turn = None
|
|
129
136
|
return ui_events
|
|
130
137
|
|
|
131
|
-
async def run(self, user_input:
|
|
138
|
+
async def run(self, user_input: model.UserInputPayload) -> AsyncGenerator[events.Event, None]:
|
|
132
139
|
"""Execute the task, yielding events as they occur."""
|
|
133
140
|
ctx = self._context
|
|
134
141
|
self._started_at = time.perf_counter()
|
|
@@ -138,7 +145,7 @@ class TaskExecutor:
|
|
|
138
145
|
sub_agent_state=ctx.sub_agent_state,
|
|
139
146
|
)
|
|
140
147
|
|
|
141
|
-
ctx.append_history([model.UserMessageItem(content=user_input)])
|
|
148
|
+
ctx.append_history([model.UserMessageItem(content=user_input.text, images=user_input.images)])
|
|
142
149
|
|
|
143
150
|
profile = ctx.profile
|
|
144
151
|
metadata_accumulator = MetadataAccumulator(model_name=profile.llm_client.model_name)
|
|
@@ -166,7 +173,7 @@ class TaskExecutor:
|
|
|
166
173
|
turn_succeeded = False
|
|
167
174
|
last_error_message: str | None = None
|
|
168
175
|
|
|
169
|
-
for attempt in range(MAX_FAILED_TURN_RETRIES + 1):
|
|
176
|
+
for attempt in range(const.MAX_FAILED_TURN_RETRIES + 1):
|
|
170
177
|
turn = TurnExecutor(turn_context)
|
|
171
178
|
self._current_turn = turn
|
|
172
179
|
|
|
@@ -186,9 +193,9 @@ class TaskExecutor:
|
|
|
186
193
|
break
|
|
187
194
|
except TurnError as e:
|
|
188
195
|
last_error_message = str(e)
|
|
189
|
-
if attempt < MAX_FAILED_TURN_RETRIES:
|
|
196
|
+
if attempt < const.MAX_FAILED_TURN_RETRIES:
|
|
190
197
|
delay = _retry_delay_seconds(attempt + 1)
|
|
191
|
-
error_msg = f"Retrying {attempt + 1}/{MAX_FAILED_TURN_RETRIES} in {delay:.1f}s"
|
|
198
|
+
error_msg = f"Retrying {attempt + 1}/{const.MAX_FAILED_TURN_RETRIES} in {delay:.1f}s"
|
|
192
199
|
if last_error_message:
|
|
193
200
|
error_msg = f"{error_msg} - {last_error_message}"
|
|
194
201
|
yield events.ErrorEvent(error_message=error_msg, can_retry=True)
|
|
@@ -202,7 +209,7 @@ class TaskExecutor:
|
|
|
202
209
|
style="red",
|
|
203
210
|
debug_type=DebugType.EXECUTION,
|
|
204
211
|
)
|
|
205
|
-
final_error = f"Turn failed after {MAX_FAILED_TURN_RETRIES} retries."
|
|
212
|
+
final_error = f"Turn failed after {const.MAX_FAILED_TURN_RETRIES} retries."
|
|
206
213
|
if last_error_message:
|
|
207
214
|
final_error = f"{last_error_message}\n{final_error}"
|
|
208
215
|
yield events.ErrorEvent(error_message=final_error, can_retry=False)
|
|
@@ -226,5 +233,5 @@ class TaskExecutor:
|
|
|
226
233
|
def _retry_delay_seconds(attempt: int) -> float:
|
|
227
234
|
"""Compute exponential backoff delay for the given attempt count."""
|
|
228
235
|
capped_attempt = max(1, attempt)
|
|
229
|
-
delay = INITIAL_RETRY_DELAY_S * (2 ** (capped_attempt - 1))
|
|
230
|
-
return min(delay, MAX_RETRY_DELAY_S)
|
|
236
|
+
delay = const.INITIAL_RETRY_DELAY_S * (2 ** (capped_attempt - 1))
|
|
237
|
+
return min(delay, const.MAX_RETRY_DELAY_S)
|
|
@@ -1,41 +1,75 @@
|
|
|
1
|
+
from .file.apply_patch import DiffError, process_patch
|
|
1
2
|
from .file.apply_patch_tool import ApplyPatchTool
|
|
2
3
|
from .file.edit_tool import EditTool
|
|
3
4
|
from .file.multi_edit_tool import MultiEditTool
|
|
4
5
|
from .file.read_tool import ReadTool
|
|
5
6
|
from .file.write_tool import WriteTool
|
|
6
|
-
from .memory.memory_tool import MemoryTool
|
|
7
|
+
from .memory.memory_tool import MEMORY_DIR_NAME, MemoryTool
|
|
8
|
+
from .memory.skill_loader import Skill, SkillLoader
|
|
7
9
|
from .memory.skill_tool import SkillTool
|
|
8
10
|
from .shell.bash_tool import BashTool
|
|
11
|
+
from .shell.command_safety import SafetyCheckResult, is_safe_command
|
|
9
12
|
from .sub_agent_tool import SubAgentTool
|
|
10
13
|
from .todo.todo_write_tool import TodoWriteTool
|
|
11
14
|
from .todo.update_plan_tool import UpdatePlanTool
|
|
12
|
-
from .
|
|
15
|
+
from .tool_abc import ToolABC
|
|
16
|
+
from .tool_context import (
|
|
17
|
+
TodoContext,
|
|
18
|
+
ToolContextToken,
|
|
19
|
+
current_run_subtask_callback,
|
|
20
|
+
reset_tool_context,
|
|
21
|
+
set_tool_context_from_session,
|
|
22
|
+
tool_context,
|
|
23
|
+
)
|
|
24
|
+
from .tool_registry import get_registry, get_tool_schemas, load_agent_tools
|
|
13
25
|
from .tool_runner import run_tool
|
|
14
26
|
from .truncation import SimpleTruncationStrategy, TruncationStrategy, get_truncation_strategy, set_truncation_strategy
|
|
15
27
|
from .web.mermaid_tool import MermaidTool
|
|
16
28
|
from .web.web_fetch_tool import WebFetchTool
|
|
17
29
|
|
|
18
30
|
__all__ = [
|
|
31
|
+
# Tools
|
|
32
|
+
"ApplyPatchTool",
|
|
19
33
|
"BashTool",
|
|
20
|
-
"ReadTool",
|
|
21
34
|
"EditTool",
|
|
22
35
|
"MemoryTool",
|
|
36
|
+
"MermaidTool",
|
|
23
37
|
"MultiEditTool",
|
|
38
|
+
"ReadTool",
|
|
39
|
+
"SkillTool",
|
|
24
40
|
"SubAgentTool",
|
|
25
41
|
"TodoWriteTool",
|
|
26
|
-
"WriteTool",
|
|
27
|
-
"SkillTool",
|
|
28
42
|
"UpdatePlanTool",
|
|
29
|
-
"ApplyPatchTool",
|
|
30
|
-
"MermaidTool",
|
|
31
43
|
"WebFetchTool",
|
|
32
|
-
"
|
|
44
|
+
"WriteTool",
|
|
45
|
+
# Tool ABC
|
|
46
|
+
"ToolABC",
|
|
47
|
+
# Tool context
|
|
48
|
+
"TodoContext",
|
|
49
|
+
"ToolContextToken",
|
|
50
|
+
"current_run_subtask_callback",
|
|
51
|
+
"reset_tool_context",
|
|
52
|
+
"set_tool_context_from_session",
|
|
53
|
+
"tool_context",
|
|
54
|
+
# Tool registry
|
|
55
|
+
"load_agent_tools",
|
|
33
56
|
"get_registry",
|
|
57
|
+
"get_tool_schemas",
|
|
34
58
|
"run_tool",
|
|
35
|
-
|
|
36
|
-
"get_main_agent_tools",
|
|
37
|
-
"TruncationStrategy",
|
|
59
|
+
# Truncation
|
|
38
60
|
"SimpleTruncationStrategy",
|
|
61
|
+
"TruncationStrategy",
|
|
39
62
|
"get_truncation_strategy",
|
|
40
63
|
"set_truncation_strategy",
|
|
64
|
+
# Command safety
|
|
65
|
+
"SafetyCheckResult",
|
|
66
|
+
"is_safe_command",
|
|
67
|
+
# Skill
|
|
68
|
+
"Skill",
|
|
69
|
+
"SkillLoader",
|
|
70
|
+
# Memory
|
|
71
|
+
"MEMORY_DIR_NAME",
|
|
72
|
+
# Apply patch
|
|
73
|
+
"DiffError",
|
|
74
|
+
"process_patch",
|
|
41
75
|
]
|
|
@@ -417,7 +417,11 @@ def load_files(paths: list[str], open_fn: Callable[[str], str]) -> dict[str, str
|
|
|
417
417
|
return orig
|
|
418
418
|
|
|
419
419
|
|
|
420
|
-
def apply_commit(
|
|
420
|
+
def apply_commit(
|
|
421
|
+
commit: Commit,
|
|
422
|
+
write_fn: Callable[[str, str], None],
|
|
423
|
+
remove_fn: Callable[[str], None],
|
|
424
|
+
) -> None:
|
|
421
425
|
for path, change in commit.changes.items():
|
|
422
426
|
if change.type == ActionType.DELETE:
|
|
423
427
|
remove_fn(path)
|
|
@@ -11,24 +11,22 @@ from klaude_code.core.tool.file import apply_patch as apply_patch_module
|
|
|
11
11
|
from klaude_code.core.tool.tool_abc import ToolABC, load_desc
|
|
12
12
|
from klaude_code.core.tool.tool_context import get_current_file_tracker
|
|
13
13
|
from klaude_code.core.tool.tool_registry import register
|
|
14
|
-
from klaude_code.protocol import tools
|
|
15
|
-
from klaude_code.protocol.llm_parameter import ToolSchema
|
|
16
|
-
from klaude_code.protocol.model import ToolResultItem, ToolResultUIExtra, ToolResultUIExtraType
|
|
14
|
+
from klaude_code.protocol import llm_param, model, tools
|
|
17
15
|
|
|
18
16
|
|
|
19
17
|
class ApplyPatchHandler:
|
|
20
18
|
@classmethod
|
|
21
|
-
async def handle_apply_patch(cls, patch_text: str) -> ToolResultItem:
|
|
19
|
+
async def handle_apply_patch(cls, patch_text: str) -> model.ToolResultItem:
|
|
22
20
|
try:
|
|
23
21
|
output, diff_text = await asyncio.to_thread(cls._apply_patch_in_thread, patch_text)
|
|
24
22
|
except apply_patch_module.DiffError as error:
|
|
25
|
-
return ToolResultItem(status="error", output=str(error))
|
|
23
|
+
return model.ToolResultItem(status="error", output=str(error))
|
|
26
24
|
except Exception as error: # pragma: no cover # unexpected errors bubbled to tool result
|
|
27
|
-
return ToolResultItem(status="error", output=f"Execution error: {error}")
|
|
28
|
-
return ToolResultItem(
|
|
25
|
+
return model.ToolResultItem(status="error", output=f"Execution error: {error}")
|
|
26
|
+
return model.ToolResultItem(
|
|
29
27
|
status="success",
|
|
30
28
|
output=output,
|
|
31
|
-
ui_extra=ToolResultUIExtra(type=ToolResultUIExtraType.DIFF_TEXT, diff_text=diff_text),
|
|
29
|
+
ui_extra=model.ToolResultUIExtra(type=model.ToolResultUIExtraType.DIFF_TEXT, diff_text=diff_text),
|
|
32
30
|
)
|
|
33
31
|
|
|
34
32
|
@staticmethod
|
|
@@ -176,8 +174,8 @@ class ApplyPatchTool(ToolABC):
|
|
|
176
174
|
patch: str
|
|
177
175
|
|
|
178
176
|
@classmethod
|
|
179
|
-
def schema(cls) -> ToolSchema:
|
|
180
|
-
return ToolSchema(
|
|
177
|
+
def schema(cls) -> llm_param.ToolSchema:
|
|
178
|
+
return llm_param.ToolSchema(
|
|
181
179
|
name=tools.APPLY_PATCH,
|
|
182
180
|
type="function",
|
|
183
181
|
description=load_desc(Path(__file__).parent / "apply_patch_tool.md"),
|
|
@@ -194,13 +192,13 @@ class ApplyPatchTool(ToolABC):
|
|
|
194
192
|
)
|
|
195
193
|
|
|
196
194
|
@classmethod
|
|
197
|
-
async def call(cls, arguments: str) -> ToolResultItem:
|
|
195
|
+
async def call(cls, arguments: str) -> model.ToolResultItem:
|
|
198
196
|
try:
|
|
199
197
|
args = cls.ApplyPatchArguments.model_validate_json(arguments)
|
|
200
198
|
except ValueError as exc:
|
|
201
|
-
return ToolResultItem(status="error", output=f"Invalid arguments: {exc}")
|
|
199
|
+
return model.ToolResultItem(status="error", output=f"Invalid arguments: {exc}")
|
|
202
200
|
return await cls.call_with_args(args)
|
|
203
201
|
|
|
204
202
|
@classmethod
|
|
205
|
-
async def call_with_args(cls, args: ApplyPatchArguments) -> ToolResultItem:
|
|
203
|
+
async def call_with_args(cls, args: ApplyPatchArguments) -> model.ToolResultItem:
|
|
206
204
|
return await ApplyPatchHandler.handle_apply_patch(args.patch)
|
|
@@ -10,9 +10,7 @@ from pydantic import BaseModel, Field
|
|
|
10
10
|
from klaude_code.core.tool.tool_abc import ToolABC, load_desc
|
|
11
11
|
from klaude_code.core.tool.tool_context import get_current_file_tracker
|
|
12
12
|
from klaude_code.core.tool.tool_registry import register
|
|
13
|
-
from klaude_code.protocol
|
|
14
|
-
from klaude_code.protocol.model import ToolResultItem, ToolResultUIExtra, ToolResultUIExtraType
|
|
15
|
-
from klaude_code.protocol.tools import EDIT
|
|
13
|
+
from klaude_code.protocol import llm_param, model, tools
|
|
16
14
|
|
|
17
15
|
|
|
18
16
|
def _is_directory(path: str) -> bool:
|
|
@@ -41,7 +39,7 @@ def _write_text(path: str, content: str) -> None:
|
|
|
41
39
|
f.write(content)
|
|
42
40
|
|
|
43
41
|
|
|
44
|
-
@register(EDIT)
|
|
42
|
+
@register(tools.EDIT)
|
|
45
43
|
class EditTool(ToolABC):
|
|
46
44
|
class EditArguments(BaseModel):
|
|
47
45
|
file_path: str
|
|
@@ -50,9 +48,9 @@ class EditTool(ToolABC):
|
|
|
50
48
|
replace_all: bool = Field(default=False)
|
|
51
49
|
|
|
52
50
|
@classmethod
|
|
53
|
-
def schema(cls) -> ToolSchema:
|
|
54
|
-
return ToolSchema(
|
|
55
|
-
name=EDIT,
|
|
51
|
+
def schema(cls) -> llm_param.ToolSchema:
|
|
52
|
+
return llm_param.ToolSchema(
|
|
53
|
+
name=tools.EDIT,
|
|
56
54
|
type="function",
|
|
57
55
|
description=load_desc(Path(__file__).parent / "edit_tool.md"),
|
|
58
56
|
parameters={
|
|
@@ -112,23 +110,23 @@ class EditTool(ToolABC):
|
|
|
112
110
|
return content.replace(old_string, new_string, 1)
|
|
113
111
|
|
|
114
112
|
@classmethod
|
|
115
|
-
async def call(cls, arguments: str) -> ToolResultItem:
|
|
113
|
+
async def call(cls, arguments: str) -> model.ToolResultItem:
|
|
116
114
|
try:
|
|
117
115
|
args = EditTool.EditArguments.model_validate_json(arguments)
|
|
118
116
|
except Exception as e: # pragma: no cover - defensive
|
|
119
|
-
return ToolResultItem(status="error", output=f"Invalid arguments: {e}")
|
|
117
|
+
return model.ToolResultItem(status="error", output=f"Invalid arguments: {e}")
|
|
120
118
|
|
|
121
119
|
file_path = os.path.abspath(args.file_path)
|
|
122
120
|
|
|
123
121
|
# Common file errors
|
|
124
122
|
if _is_directory(file_path):
|
|
125
|
-
return ToolResultItem(
|
|
123
|
+
return model.ToolResultItem(
|
|
126
124
|
status="error",
|
|
127
125
|
output="<tool_use_error>Illegal operation on a directory. edit</tool_use_error>",
|
|
128
126
|
)
|
|
129
127
|
|
|
130
128
|
if args.old_string == "":
|
|
131
|
-
return ToolResultItem(
|
|
129
|
+
return model.ToolResultItem(
|
|
132
130
|
status="error",
|
|
133
131
|
output=(
|
|
134
132
|
"<tool_use_error>old_string must not be empty for Edit. "
|
|
@@ -140,14 +138,14 @@ class EditTool(ToolABC):
|
|
|
140
138
|
file_tracker = get_current_file_tracker()
|
|
141
139
|
if not _file_exists(file_path):
|
|
142
140
|
# We require reading before editing
|
|
143
|
-
return ToolResultItem(
|
|
141
|
+
return model.ToolResultItem(
|
|
144
142
|
status="error",
|
|
145
143
|
output=("File has not been read yet. Read it first before writing to it."),
|
|
146
144
|
)
|
|
147
145
|
if file_tracker is not None:
|
|
148
146
|
tracked = file_tracker.get(file_path)
|
|
149
147
|
if tracked is None:
|
|
150
|
-
return ToolResultItem(
|
|
148
|
+
return model.ToolResultItem(
|
|
151
149
|
status="error",
|
|
152
150
|
output=("File has not been read yet. Read it first before writing to it."),
|
|
153
151
|
)
|
|
@@ -156,7 +154,7 @@ class EditTool(ToolABC):
|
|
|
156
154
|
except Exception:
|
|
157
155
|
current_mtime = tracked
|
|
158
156
|
if current_mtime != tracked:
|
|
159
|
-
return ToolResultItem(
|
|
157
|
+
return model.ToolResultItem(
|
|
160
158
|
status="error",
|
|
161
159
|
output=(
|
|
162
160
|
"File has been modified externally. Either by user or a linter. Read it first before writing to it."
|
|
@@ -167,24 +165,30 @@ class EditTool(ToolABC):
|
|
|
167
165
|
try:
|
|
168
166
|
before = await asyncio.to_thread(_read_text, file_path)
|
|
169
167
|
except FileNotFoundError:
|
|
170
|
-
return ToolResultItem(
|
|
168
|
+
return model.ToolResultItem(
|
|
171
169
|
status="error",
|
|
172
170
|
output="File has not been read yet. Read it first before writing to it.",
|
|
173
171
|
)
|
|
174
172
|
|
|
175
173
|
err = cls.valid(
|
|
176
|
-
content=before,
|
|
174
|
+
content=before,
|
|
175
|
+
old_string=args.old_string,
|
|
176
|
+
new_string=args.new_string,
|
|
177
|
+
replace_all=args.replace_all,
|
|
177
178
|
)
|
|
178
179
|
if err is not None:
|
|
179
|
-
return ToolResultItem(status="error", output=err)
|
|
180
|
+
return model.ToolResultItem(status="error", output=err)
|
|
180
181
|
|
|
181
182
|
after = cls.execute(
|
|
182
|
-
content=before,
|
|
183
|
+
content=before,
|
|
184
|
+
old_string=args.old_string,
|
|
185
|
+
new_string=args.new_string,
|
|
186
|
+
replace_all=args.replace_all,
|
|
183
187
|
)
|
|
184
188
|
|
|
185
189
|
# If nothing changed due to replacement semantics (should not happen after valid), guard anyway
|
|
186
190
|
if before == after:
|
|
187
|
-
return ToolResultItem(
|
|
191
|
+
return model.ToolResultItem(
|
|
188
192
|
status="error",
|
|
189
193
|
output=(
|
|
190
194
|
"<tool_use_error>No changes to make: old_string and new_string are exactly the same.</tool_use_error>"
|
|
@@ -195,7 +199,7 @@ class EditTool(ToolABC):
|
|
|
195
199
|
try:
|
|
196
200
|
await asyncio.to_thread(_write_text, file_path, after)
|
|
197
201
|
except Exception as e: # pragma: no cover
|
|
198
|
-
return ToolResultItem(status="error", output=f"<tool_use_error>{e}</tool_use_error>")
|
|
202
|
+
return model.ToolResultItem(status="error", output=f"<tool_use_error>{e}</tool_use_error>")
|
|
199
203
|
|
|
200
204
|
# Prepare UI extra: unified diff with 3 context lines
|
|
201
205
|
diff_lines = list(
|
|
@@ -208,7 +212,7 @@ class EditTool(ToolABC):
|
|
|
208
212
|
)
|
|
209
213
|
)
|
|
210
214
|
diff_text = "\n".join(diff_lines)
|
|
211
|
-
ui_extra = ToolResultUIExtra(type=ToolResultUIExtraType.DIFF_TEXT, diff_text=diff_text)
|
|
215
|
+
ui_extra = model.ToolResultUIExtra(type=model.ToolResultUIExtraType.DIFF_TEXT, diff_text=diff_text)
|
|
212
216
|
|
|
213
217
|
# Update tracker with new mtime
|
|
214
218
|
if file_tracker is not None:
|
|
@@ -220,7 +224,7 @@ class EditTool(ToolABC):
|
|
|
220
224
|
# Build output message
|
|
221
225
|
if args.replace_all:
|
|
222
226
|
msg = f"The file {file_path} has been updated. All occurrences of '{args.old_string}' were successfully replaced with '{args.new_string}'."
|
|
223
|
-
return ToolResultItem(status="success", output=msg, ui_extra=ui_extra)
|
|
227
|
+
return model.ToolResultItem(status="success", output=msg, ui_extra=ui_extra)
|
|
224
228
|
|
|
225
229
|
# For single replacement, show a snippet consisting of context + added lines only
|
|
226
230
|
# Parse the diff to collect target line numbers in the 'after' file
|
|
@@ -267,4 +271,4 @@ class EditTool(ToolABC):
|
|
|
267
271
|
f"The file {file_path} has been updated. Here's the result of running `cat -n` on a snippet of the edited file:\n"
|
|
268
272
|
f"{snippet}"
|
|
269
273
|
)
|
|
270
|
-
return ToolResultItem(status="success", output=output, ui_extra=ui_extra)
|
|
274
|
+
return model.ToolResultItem(status="success", output=output, ui_extra=ui_extra)
|