klaude-code 2.8.1__py3-none-any.whl → 2.9.1__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/runtime.py +2 -1
- klaude_code/auth/antigravity/oauth.py +33 -38
- klaude_code/auth/antigravity/token_manager.py +0 -18
- klaude_code/auth/base.py +53 -0
- klaude_code/auth/claude/oauth.py +34 -49
- klaude_code/auth/codex/exceptions.py +0 -4
- klaude_code/auth/codex/oauth.py +32 -28
- klaude_code/auth/codex/token_manager.py +0 -18
- klaude_code/cli/cost_cmd.py +128 -39
- klaude_code/cli/list_model.py +27 -10
- klaude_code/cli/main.py +14 -3
- klaude_code/config/assets/builtin_config.yaml +25 -24
- klaude_code/config/config.py +47 -25
- klaude_code/config/sub_agent_model_helper.py +18 -13
- klaude_code/config/thinking.py +0 -8
- klaude_code/const.py +1 -1
- klaude_code/core/agent_profile.py +11 -56
- klaude_code/core/compaction/overflow.py +0 -4
- klaude_code/core/executor.py +33 -5
- klaude_code/core/manager/llm_clients.py +9 -1
- klaude_code/core/prompts/prompt-claude-code.md +4 -4
- klaude_code/core/reminders.py +21 -23
- klaude_code/core/task.py +1 -5
- klaude_code/core/tool/__init__.py +3 -2
- klaude_code/core/tool/file/apply_patch.py +0 -27
- klaude_code/core/tool/file/read_tool.md +3 -2
- klaude_code/core/tool/file/read_tool.py +27 -3
- klaude_code/core/tool/offload.py +0 -35
- klaude_code/core/tool/shell/bash_tool.py +1 -1
- klaude_code/core/tool/sub_agent/__init__.py +6 -0
- klaude_code/core/tool/sub_agent/image_gen.md +16 -0
- klaude_code/core/tool/sub_agent/image_gen.py +146 -0
- klaude_code/core/tool/sub_agent/task.md +20 -0
- klaude_code/core/tool/sub_agent/task.py +205 -0
- klaude_code/core/tool/tool_registry.py +0 -16
- klaude_code/core/turn.py +1 -1
- klaude_code/llm/anthropic/input.py +6 -5
- klaude_code/llm/antigravity/input.py +14 -7
- klaude_code/llm/bedrock_anthropic/__init__.py +3 -0
- klaude_code/llm/google/client.py +8 -6
- klaude_code/llm/google/input.py +20 -12
- klaude_code/llm/image.py +18 -11
- klaude_code/llm/input_common.py +32 -6
- klaude_code/llm/json_stable.py +37 -0
- klaude_code/llm/{codex → openai_codex}/__init__.py +1 -1
- klaude_code/llm/{codex → openai_codex}/client.py +24 -2
- klaude_code/llm/openai_codex/prompt_sync.py +237 -0
- klaude_code/llm/openai_compatible/client.py +3 -1
- klaude_code/llm/openai_compatible/input.py +0 -10
- klaude_code/llm/openai_compatible/stream.py +35 -10
- klaude_code/llm/{responses → openai_responses}/client.py +1 -1
- klaude_code/llm/{responses → openai_responses}/input.py +15 -5
- klaude_code/llm/registry.py +3 -8
- klaude_code/llm/stream_parts.py +3 -1
- klaude_code/llm/usage.py +1 -9
- klaude_code/protocol/events.py +2 -2
- klaude_code/protocol/message.py +3 -2
- klaude_code/protocol/model.py +34 -2
- klaude_code/protocol/op.py +13 -0
- klaude_code/protocol/op_handler.py +5 -0
- klaude_code/protocol/sub_agent/AGENTS.md +5 -5
- klaude_code/protocol/sub_agent/__init__.py +13 -34
- klaude_code/protocol/sub_agent/explore.py +7 -34
- klaude_code/protocol/sub_agent/image_gen.py +3 -74
- klaude_code/protocol/sub_agent/task.py +3 -47
- klaude_code/protocol/sub_agent/web.py +8 -52
- klaude_code/protocol/tools.py +2 -0
- klaude_code/session/session.py +80 -22
- klaude_code/session/store.py +0 -4
- klaude_code/skill/assets/deslop/SKILL.md +9 -0
- klaude_code/skill/system_skills.py +0 -20
- klaude_code/tui/command/fork_session_cmd.py +5 -2
- klaude_code/tui/command/resume_cmd.py +9 -2
- klaude_code/tui/command/sub_agent_model_cmd.py +85 -18
- klaude_code/tui/components/assistant.py +0 -26
- klaude_code/tui/components/bash_syntax.py +4 -0
- klaude_code/tui/components/command_output.py +3 -1
- klaude_code/tui/components/developer.py +3 -0
- klaude_code/tui/components/diffs.py +4 -209
- klaude_code/tui/components/errors.py +4 -0
- klaude_code/tui/components/mermaid_viewer.py +2 -2
- klaude_code/tui/components/metadata.py +0 -3
- klaude_code/tui/components/rich/markdown.py +120 -87
- klaude_code/tui/components/rich/status.py +2 -2
- klaude_code/tui/components/rich/theme.py +11 -6
- klaude_code/tui/components/sub_agent.py +2 -46
- klaude_code/tui/components/thinking.py +0 -33
- klaude_code/tui/components/tools.py +65 -21
- klaude_code/tui/components/user_input.py +2 -0
- klaude_code/tui/input/images.py +21 -18
- klaude_code/tui/input/key_bindings.py +2 -2
- klaude_code/tui/input/prompt_toolkit.py +49 -49
- klaude_code/tui/machine.py +29 -47
- klaude_code/tui/renderer.py +48 -33
- klaude_code/tui/runner.py +2 -1
- klaude_code/tui/terminal/image.py +27 -34
- klaude_code/ui/common.py +0 -70
- {klaude_code-2.8.1.dist-info → klaude_code-2.9.1.dist-info}/METADATA +3 -6
- {klaude_code-2.8.1.dist-info → klaude_code-2.9.1.dist-info}/RECORD +103 -99
- klaude_code/core/tool/sub_agent_tool.py +0 -126
- klaude_code/llm/bedrock/__init__.py +0 -3
- klaude_code/llm/openai_compatible/tool_call_accumulator.py +0 -108
- klaude_code/tui/components/rich/searchable_text.py +0 -68
- /klaude_code/llm/{bedrock → bedrock_anthropic}/client.py +0 -0
- /klaude_code/llm/{responses → openai_responses}/__init__.py +0 -0
- {klaude_code-2.8.1.dist-info → klaude_code-2.9.1.dist-info}/WHEEL +0 -0
- {klaude_code-2.8.1.dist-info → klaude_code-2.9.1.dist-info}/entry_points.txt +0 -0
klaude_code/core/executor.py
CHANGED
|
@@ -676,6 +676,39 @@ class ExecutorContext:
|
|
|
676
676
|
)
|
|
677
677
|
)
|
|
678
678
|
|
|
679
|
+
async def handle_change_compact_model(self, operation: op.ChangeCompactModelOperation) -> None:
|
|
680
|
+
"""Handle a change compact model operation."""
|
|
681
|
+
agent = await self._agent_runtime.ensure_agent(operation.session_id)
|
|
682
|
+
config = load_config()
|
|
683
|
+
|
|
684
|
+
model_name = operation.model_name
|
|
685
|
+
|
|
686
|
+
if model_name is None:
|
|
687
|
+
# Clear explicit override and use main client for compaction
|
|
688
|
+
self.llm_clients.compact = None
|
|
689
|
+
agent.compact_llm_client = None
|
|
690
|
+
display_model = "(inherit from main agent)"
|
|
691
|
+
else:
|
|
692
|
+
# Create new client for compaction
|
|
693
|
+
llm_config = config.get_model_config(model_name)
|
|
694
|
+
new_client = create_llm_client(llm_config)
|
|
695
|
+
self.llm_clients.compact = new_client
|
|
696
|
+
agent.compact_llm_client = new_client
|
|
697
|
+
display_model = new_client.model_name
|
|
698
|
+
|
|
699
|
+
if operation.save_as_default:
|
|
700
|
+
config.compact_model = model_name
|
|
701
|
+
await config.save()
|
|
702
|
+
|
|
703
|
+
saved_note = " (saved in ~/.klaude/klaude-config.yaml)" if operation.save_as_default else ""
|
|
704
|
+
await self.emit_event(
|
|
705
|
+
events.CommandOutputEvent(
|
|
706
|
+
session_id=agent.session.id,
|
|
707
|
+
command_name=commands.CommandName.SUB_AGENT_MODEL,
|
|
708
|
+
content=f"Compact model: {display_model}{saved_note}",
|
|
709
|
+
)
|
|
710
|
+
)
|
|
711
|
+
|
|
679
712
|
async def handle_clear_session(self, operation: op.ClearSessionOperation) -> None:
|
|
680
713
|
await self._agent_runtime.clear_session(operation.session_id)
|
|
681
714
|
|
|
@@ -763,11 +796,6 @@ class ExecutorContext:
|
|
|
763
796
|
return None
|
|
764
797
|
return active.task
|
|
765
798
|
|
|
766
|
-
def has_active_task(self, submission_id: str) -> bool:
|
|
767
|
-
"""Return True if a task is registered for the submission id."""
|
|
768
|
-
|
|
769
|
-
return self.task_manager.get(submission_id) is not None
|
|
770
|
-
|
|
771
799
|
|
|
772
800
|
class Executor:
|
|
773
801
|
"""
|
|
@@ -6,6 +6,7 @@ from dataclasses import dataclass
|
|
|
6
6
|
from dataclasses import field as dataclass_field
|
|
7
7
|
|
|
8
8
|
from klaude_code.llm.client import LLMClientABC
|
|
9
|
+
from klaude_code.protocol import tools
|
|
9
10
|
from klaude_code.protocol.tools import SubAgentType
|
|
10
11
|
|
|
11
12
|
|
|
@@ -26,7 +27,14 @@ class LLMClients:
|
|
|
26
27
|
|
|
27
28
|
if sub_agent_type is None:
|
|
28
29
|
return self.main
|
|
29
|
-
|
|
30
|
+
client = self.sub_clients.get(sub_agent_type)
|
|
31
|
+
if client is not None:
|
|
32
|
+
return client
|
|
33
|
+
if sub_agent_type != tools.TASK:
|
|
34
|
+
fallback = self.sub_clients.get(tools.TASK)
|
|
35
|
+
if fallback is not None:
|
|
36
|
+
return fallback
|
|
37
|
+
return self.main
|
|
30
38
|
|
|
31
39
|
def get_compact_client(self) -> LLMClientABC:
|
|
32
40
|
"""Return compact client if configured, otherwise main client."""
|
|
@@ -72,16 +72,16 @@ The user will primarily request you perform software engineering tasks. This inc
|
|
|
72
72
|
- Tool results and user messages may include <system-reminder> tags. <system-reminder> tags contain useful information and reminders. They are automatically added by the system, and bear no direct relation to the specific tool results or user messages in which they appear.
|
|
73
73
|
|
|
74
74
|
## Tool usage policy
|
|
75
|
-
- When doing file search, prefer to use the
|
|
75
|
+
- When doing file search, prefer to use the Task tool in order to reduce context usage.
|
|
76
76
|
- You can call multiple tools in a single response. If you intend to call multiple tools and there are no dependencies between them, make all independent tool calls in parallel. Maximize use of parallel tool calls where possible to increase efficiency. However, if some tool calls depend on previous calls to inform dependent values, do NOT call these tools in parallel and instead call them sequentially. For instance, if one operation must complete before another starts, run these operations sequentially instead. Never use placeholders or guess missing parameters in tool calls.
|
|
77
77
|
- If the user specifies that they want you to run tools "in parallel", you MUST send a single message with multiple tool use content blocks. For example, if you need to launch multiple agents in parallel, send a single message with multiple Task tool calls.
|
|
78
78
|
- Use specialized tools instead of bash commands when possible, as this provides a better user experience. For file operations, use dedicated tools: Read for reading files instead of cat/head/tail, Edit for editing instead of sed/awk. NEVER use bash echo or other command-line tools to communicate thoughts, explanations, or instructions to the user. Output all communication directly in your response text instead.
|
|
79
|
-
- VERY IMPORTANT: When exploring the codebase to gather context or to answer a question that is not a needle query for a specific file/class/function, it is CRITICAL that you use the
|
|
79
|
+
- VERY IMPORTANT: When exploring the codebase to gather context or to answer a question that is not a needle query for a specific file/class/function, it is CRITICAL that you use the Task tool with subagent_type=Explore instead of running search commands directly.
|
|
80
80
|
<example>
|
|
81
81
|
user: Where are errors from the client handled?
|
|
82
|
-
assistant: [Uses the
|
|
82
|
+
assistant: [Uses the Task tool with subagent_type=Explore to find the files that handle client errors instead of using Glob or Grep directly]
|
|
83
83
|
</example>
|
|
84
84
|
<example>
|
|
85
85
|
user: What is the codebase structure?
|
|
86
|
-
assistant: [Uses the
|
|
86
|
+
assistant: [Uses the Task tool with subagent_type=Explore]
|
|
87
87
|
</example>
|
klaude_code/core/reminders.py
CHANGED
|
@@ -21,20 +21,6 @@ AT_FILE_PATTERN = re.compile(r'(?:(?<!\S)|(?<=\u2192))@("(?P<quoted>[^\"]+)"|(?P
|
|
|
21
21
|
SKILL_PATTERN = re.compile(r"(?:^|\s)[$¥](?P<skill>\S+)")
|
|
22
22
|
|
|
23
23
|
|
|
24
|
-
def get_last_new_user_input(session: Session) -> str | None:
|
|
25
|
-
"""Get last user input & developer message (CLAUDE.md) from conversation history. if there's a tool result after user input, return None"""
|
|
26
|
-
result: list[str] = []
|
|
27
|
-
for item in reversed(session.conversation_history):
|
|
28
|
-
if isinstance(item, message.ToolResultMessage):
|
|
29
|
-
return None
|
|
30
|
-
if isinstance(item, message.UserMessage):
|
|
31
|
-
result.append(message.join_text_parts(item.parts))
|
|
32
|
-
break
|
|
33
|
-
if isinstance(item, message.DeveloperMessage):
|
|
34
|
-
result.append(message.join_text_parts(item.parts))
|
|
35
|
-
return "\n\n".join(result)
|
|
36
|
-
|
|
37
|
-
|
|
38
24
|
@dataclass
|
|
39
25
|
class AtPatternSource:
|
|
40
26
|
"""Represents an @ pattern with its source file (if from a memory file)."""
|
|
@@ -115,6 +101,7 @@ async def _load_at_file_recursive(
|
|
|
115
101
|
at_ops: list[model.AtFileOp],
|
|
116
102
|
formatted_blocks: list[str],
|
|
117
103
|
collected_images: list[message.ImageURLPart],
|
|
104
|
+
collected_image_paths: list[str],
|
|
118
105
|
visited: set[str],
|
|
119
106
|
base_dir: Path | None = None,
|
|
120
107
|
mentioned_in: str | None = None,
|
|
@@ -150,6 +137,7 @@ Result of calling the {tools.READ} tool:
|
|
|
150
137
|
at_ops.append(model.AtFileOp(operation="Read", path=path_str, mentioned_in=mentioned_in))
|
|
151
138
|
if images:
|
|
152
139
|
collected_images.extend(images)
|
|
140
|
+
collected_image_paths.append(path_str)
|
|
153
141
|
|
|
154
142
|
# Recursively parse @ references from ReadTool output
|
|
155
143
|
output = tool_result.output_text
|
|
@@ -163,6 +151,7 @@ Result of calling the {tools.READ} tool:
|
|
|
163
151
|
at_ops,
|
|
164
152
|
formatted_blocks,
|
|
165
153
|
collected_images,
|
|
154
|
+
collected_image_paths,
|
|
166
155
|
visited,
|
|
167
156
|
base_dir=path.parent,
|
|
168
157
|
mentioned_in=path_str,
|
|
@@ -193,6 +182,7 @@ async def at_file_reader_reminder(
|
|
|
193
182
|
at_ops: list[model.AtFileOp] = []
|
|
194
183
|
formatted_blocks: list[str] = []
|
|
195
184
|
collected_images: list[message.ImageURLPart] = []
|
|
185
|
+
collected_image_paths: list[str] = []
|
|
196
186
|
visited: set[str] = set()
|
|
197
187
|
|
|
198
188
|
for source in at_pattern_sources:
|
|
@@ -202,6 +192,7 @@ async def at_file_reader_reminder(
|
|
|
202
192
|
at_ops,
|
|
203
193
|
formatted_blocks,
|
|
204
194
|
collected_images,
|
|
195
|
+
collected_image_paths,
|
|
205
196
|
visited,
|
|
206
197
|
mentioned_in=source.mentioned_in,
|
|
207
198
|
)
|
|
@@ -210,12 +201,15 @@ async def at_file_reader_reminder(
|
|
|
210
201
|
return None
|
|
211
202
|
|
|
212
203
|
at_files_str = "\n\n".join(formatted_blocks)
|
|
204
|
+
ui_items: list[model.DeveloperUIItem] = [model.AtFileOpsUIItem(ops=at_ops)]
|
|
205
|
+
if collected_image_paths:
|
|
206
|
+
ui_items.append(model.AtFileImagesUIItem(paths=collected_image_paths))
|
|
213
207
|
return message.DeveloperMessage(
|
|
214
208
|
parts=message.parts_from_text_and_images(
|
|
215
209
|
f"""<system-reminder>{at_files_str}\n</system-reminder>""",
|
|
216
210
|
collected_images or None,
|
|
217
211
|
),
|
|
218
|
-
ui_extra=model.DeveloperUIExtra(items=
|
|
212
|
+
ui_extra=model.DeveloperUIExtra(items=ui_items),
|
|
219
213
|
)
|
|
220
214
|
|
|
221
215
|
|
|
@@ -410,25 +404,29 @@ class Memory(BaseModel):
|
|
|
410
404
|
content: str
|
|
411
405
|
|
|
412
406
|
|
|
413
|
-
def
|
|
414
|
-
"""Get image
|
|
407
|
+
def get_last_user_message_image_paths(session: Session) -> list[str]:
|
|
408
|
+
"""Get image file paths from the last user message in conversation history."""
|
|
415
409
|
for item in reversed(session.conversation_history):
|
|
416
410
|
if isinstance(item, message.ToolResultMessage):
|
|
417
|
-
return
|
|
411
|
+
return []
|
|
418
412
|
if isinstance(item, message.UserMessage):
|
|
419
|
-
|
|
420
|
-
|
|
413
|
+
paths: list[str] = []
|
|
414
|
+
for part in item.parts:
|
|
415
|
+
if isinstance(part, message.ImageFilePart):
|
|
416
|
+
paths.append(part.file_path)
|
|
417
|
+
return paths
|
|
418
|
+
return []
|
|
421
419
|
|
|
422
420
|
|
|
423
421
|
async def image_reminder(session: Session) -> message.DeveloperMessage | None:
|
|
424
422
|
"""Remind agent about images attached by user in the last message."""
|
|
425
|
-
|
|
426
|
-
if
|
|
423
|
+
image_paths = get_last_user_message_image_paths(session)
|
|
424
|
+
if not image_paths:
|
|
427
425
|
return None
|
|
428
426
|
|
|
429
427
|
return message.DeveloperMessage(
|
|
430
428
|
parts=[],
|
|
431
|
-
ui_extra=model.DeveloperUIExtra(items=[model.UserImagesUIItem(count=
|
|
429
|
+
ui_extra=model.DeveloperUIExtra(items=[model.UserImagesUIItem(count=len(image_paths), paths=image_paths)]),
|
|
432
430
|
)
|
|
433
431
|
|
|
434
432
|
|
klaude_code/core/task.py
CHANGED
|
@@ -179,10 +179,6 @@ class TaskExecutor:
|
|
|
179
179
|
self._started_at: float = 0.0
|
|
180
180
|
self._metadata_accumulator: MetadataAccumulator | None = None
|
|
181
181
|
|
|
182
|
-
@property
|
|
183
|
-
def current_turn(self) -> TurnExecutor | None:
|
|
184
|
-
return self._current_turn
|
|
185
|
-
|
|
186
182
|
def get_partial_metadata(self) -> model.TaskMetadata | None:
|
|
187
183
|
"""Get the currently accumulated metadata without finalizing.
|
|
188
184
|
|
|
@@ -214,7 +210,7 @@ class TaskExecutor:
|
|
|
214
210
|
accumulated = self._metadata_accumulator.get_partial_item(task_duration_s)
|
|
215
211
|
if accumulated is not None:
|
|
216
212
|
session_id = self._context.session_ctx.session_id
|
|
217
|
-
ui_events.append(events.TaskMetadataEvent(metadata=accumulated, session_id=session_id
|
|
213
|
+
ui_events.append(events.TaskMetadataEvent(metadata=accumulated, session_id=session_id))
|
|
218
214
|
self._context.session_ctx.append_history([accumulated])
|
|
219
215
|
|
|
220
216
|
return ui_events
|
|
@@ -7,7 +7,7 @@ from .file.write_tool import WriteTool
|
|
|
7
7
|
from .report_back_tool import ReportBackTool
|
|
8
8
|
from .shell.bash_tool import BashTool
|
|
9
9
|
from .shell.command_safety import SafetyCheckResult, is_safe_command
|
|
10
|
-
from .
|
|
10
|
+
from .sub_agent import ImageGenTool, TaskTool
|
|
11
11
|
from .todo.todo_write_tool import TodoWriteTool
|
|
12
12
|
from .todo.update_plan_tool import UpdatePlanTool
|
|
13
13
|
from .tool_abc import ToolABC
|
|
@@ -23,13 +23,14 @@ __all__ = [
|
|
|
23
23
|
"DiffError",
|
|
24
24
|
"EditTool",
|
|
25
25
|
"FileTracker",
|
|
26
|
+
"ImageGenTool",
|
|
26
27
|
"MermaidTool",
|
|
27
28
|
"ReadTool",
|
|
28
29
|
"ReportBackTool",
|
|
29
30
|
"RunSubtask",
|
|
30
31
|
"SafetyCheckResult",
|
|
31
32
|
"SubAgentResumeClaims",
|
|
32
|
-
"
|
|
33
|
+
"TaskTool",
|
|
33
34
|
"TodoContext",
|
|
34
35
|
"TodoWriteTool",
|
|
35
36
|
"ToolABC",
|
|
@@ -26,33 +26,6 @@ class Commit(BaseModel):
|
|
|
26
26
|
changes: dict[str, FileChange] = Field(default_factory=dict)
|
|
27
27
|
|
|
28
28
|
|
|
29
|
-
def assemble_changes(orig: dict[str, str | None], dest: dict[str, str | None]) -> Commit:
|
|
30
|
-
commit = Commit()
|
|
31
|
-
for path in sorted(set(orig.keys()).union(dest.keys())):
|
|
32
|
-
old_content = orig.get(path)
|
|
33
|
-
new_content = dest.get(path)
|
|
34
|
-
if old_content != new_content:
|
|
35
|
-
if old_content is not None and new_content is not None:
|
|
36
|
-
commit.changes[path] = FileChange(
|
|
37
|
-
type=ActionType.UPDATE,
|
|
38
|
-
old_content=old_content,
|
|
39
|
-
new_content=new_content,
|
|
40
|
-
)
|
|
41
|
-
elif new_content:
|
|
42
|
-
commit.changes[path] = FileChange(
|
|
43
|
-
type=ActionType.ADD,
|
|
44
|
-
new_content=new_content,
|
|
45
|
-
)
|
|
46
|
-
elif old_content:
|
|
47
|
-
commit.changes[path] = FileChange(
|
|
48
|
-
type=ActionType.DELETE,
|
|
49
|
-
old_content=old_content,
|
|
50
|
-
)
|
|
51
|
-
else:
|
|
52
|
-
raise AssertionError()
|
|
53
|
-
return commit
|
|
54
|
-
|
|
55
|
-
|
|
56
29
|
def _new_str_list() -> list[str]:
|
|
57
30
|
# Returns a new list[str] for pydantic Field default_factory
|
|
58
31
|
return []
|
|
@@ -4,10 +4,11 @@ When you need to read an image, use this tool.
|
|
|
4
4
|
|
|
5
5
|
Usage:
|
|
6
6
|
- The file_path parameter must be an absolute path, not a relative path
|
|
7
|
-
- By default, it reads up to
|
|
7
|
+
- By default, it reads up to ${line_cap} lines starting from the beginning of the file
|
|
8
8
|
- This tool allows you to read images (eg PNG, JPG, etc). When reading an image file the contents are presented visually as you are a multimodal LLM.
|
|
9
9
|
- You can optionally specify a line offset and limit (especially handy for long files), but it's recommended to read the whole file by not providing these parameters
|
|
10
|
-
- Any lines longer than
|
|
10
|
+
- Any lines longer than ${char_limit_per_line} characters will be truncated
|
|
11
|
+
- Total output is capped at ${max_chars} characters
|
|
11
12
|
- Results are returned using cat -n format, with line numbers starting at 1
|
|
12
13
|
- This tool can only read files, not directories. To read a directory, use an ls command via the Bash tool.
|
|
13
14
|
- You have the capability to call multiple tools in a single response. It is always better to speculatively read multiple files as a batch that are potentially useful.
|
|
@@ -22,6 +22,7 @@ from klaude_code.core.tool.file._utils import file_exists, is_directory
|
|
|
22
22
|
from klaude_code.core.tool.tool_abc import ToolABC, load_desc
|
|
23
23
|
from klaude_code.core.tool.tool_registry import register
|
|
24
24
|
from klaude_code.protocol import llm_param, message, model, tools
|
|
25
|
+
from klaude_code.protocol.model import ImageUIExtra, ReadPreviewLine, ReadPreviewUIExtra
|
|
25
26
|
|
|
26
27
|
_IMAGE_MIME_TYPES: dict[str, str] = {
|
|
27
28
|
".png": "image/png",
|
|
@@ -164,7 +165,14 @@ class ReadTool(ToolABC):
|
|
|
164
165
|
return llm_param.ToolSchema(
|
|
165
166
|
name=tools.READ,
|
|
166
167
|
type="function",
|
|
167
|
-
description=load_desc(
|
|
168
|
+
description=load_desc(
|
|
169
|
+
Path(__file__).parent / "read_tool.md",
|
|
170
|
+
{
|
|
171
|
+
"line_cap": str(READ_GLOBAL_LINE_CAP),
|
|
172
|
+
"char_limit_per_line": str(READ_CHAR_LIMIT_PER_LINE),
|
|
173
|
+
"max_chars": str(READ_MAX_CHARS),
|
|
174
|
+
},
|
|
175
|
+
),
|
|
168
176
|
parameters={
|
|
169
177
|
"type": "object",
|
|
170
178
|
"properties": {
|
|
@@ -280,7 +288,12 @@ class ReadTool(ToolABC):
|
|
|
280
288
|
size_kb = size_bytes / 1024.0 if size_bytes else 0.0
|
|
281
289
|
output_text = f"[image] {Path(file_path).name} ({size_kb:.1f}KB)"
|
|
282
290
|
image_part = message.ImageURLPart(url=data_url, id=None)
|
|
283
|
-
return message.ToolResultMessage(
|
|
291
|
+
return message.ToolResultMessage(
|
|
292
|
+
status="success",
|
|
293
|
+
output_text=output_text,
|
|
294
|
+
parts=[image_part],
|
|
295
|
+
ui_extra=ImageUIExtra(file_path=file_path),
|
|
296
|
+
)
|
|
284
297
|
|
|
285
298
|
offset = 1 if args.offset is None or args.offset < 1 else int(args.offset)
|
|
286
299
|
limit = None if args.limit is None else int(args.limit)
|
|
@@ -333,4 +346,15 @@ class ReadTool(ToolABC):
|
|
|
333
346
|
read_result_str = "\n".join(lines_out)
|
|
334
347
|
_track_file_access(context.file_tracker, file_path, content_sha256=read_result.content_sha256)
|
|
335
348
|
|
|
336
|
-
|
|
349
|
+
# When offset > 1, show a preview of the first 5 lines in UI
|
|
350
|
+
ui_extra = None
|
|
351
|
+
if args.offset is not None and args.offset > 1:
|
|
352
|
+
preview_count = 5
|
|
353
|
+
preview_lines = [
|
|
354
|
+
ReadPreviewLine(line_no=line_no, content=content)
|
|
355
|
+
for line_no, content in read_result.selected_lines[:preview_count]
|
|
356
|
+
]
|
|
357
|
+
remaining = len(read_result.selected_lines) - len(preview_lines)
|
|
358
|
+
ui_extra = ReadPreviewUIExtra(lines=preview_lines, remaining_lines=remaining)
|
|
359
|
+
|
|
360
|
+
return message.ToolResultMessage(status="success", output_text=read_result_str, ui_extra=ui_extra)
|
klaude_code/core/tool/offload.py
CHANGED
|
@@ -68,13 +68,6 @@ class OffloadPolicy(Enum):
|
|
|
68
68
|
ON_THRESHOLD = auto() # Offload only when exceeding size threshold
|
|
69
69
|
|
|
70
70
|
|
|
71
|
-
class TruncationStyle(Enum):
|
|
72
|
-
"""How to truncate content that exceeds limits."""
|
|
73
|
-
|
|
74
|
-
HEAD_ONLY = auto() # Keep head, discard tail (important content at top)
|
|
75
|
-
HEAD_TAIL = auto() # Keep head and tail, discard middle (errors at end)
|
|
76
|
-
|
|
77
|
-
|
|
78
71
|
@dataclass
|
|
79
72
|
class OffloadResult:
|
|
80
73
|
"""Result of offload/truncation operation."""
|
|
@@ -94,18 +87,6 @@ class OffloadResult:
|
|
|
94
87
|
class OffloadStrategy(ABC):
|
|
95
88
|
"""Base class for tool-specific offload strategies."""
|
|
96
89
|
|
|
97
|
-
@property
|
|
98
|
-
@abstractmethod
|
|
99
|
-
def offload_policy(self) -> OffloadPolicy:
|
|
100
|
-
"""When to offload content to file."""
|
|
101
|
-
...
|
|
102
|
-
|
|
103
|
-
@property
|
|
104
|
-
@abstractmethod
|
|
105
|
-
def truncation_style(self) -> TruncationStyle:
|
|
106
|
-
"""How to truncate content."""
|
|
107
|
-
...
|
|
108
|
-
|
|
109
90
|
@abstractmethod
|
|
110
91
|
def process(self, output: str, tool_call: ToolCallLike | None = None) -> OffloadResult:
|
|
111
92
|
"""Process tool output: truncate and optionally offload."""
|
|
@@ -126,14 +107,6 @@ class ReadToolStrategy(OffloadStrategy):
|
|
|
126
107
|
This strategy is a pass-through since Read tool handles its own truncation.
|
|
127
108
|
"""
|
|
128
109
|
|
|
129
|
-
@property
|
|
130
|
-
def offload_policy(self) -> OffloadPolicy:
|
|
131
|
-
return OffloadPolicy.NEVER
|
|
132
|
-
|
|
133
|
-
@property
|
|
134
|
-
def truncation_style(self) -> TruncationStyle:
|
|
135
|
-
return TruncationStyle.HEAD_ONLY
|
|
136
|
-
|
|
137
110
|
def process(self, output: str, tool_call: ToolCallLike | None = None) -> OffloadResult:
|
|
138
111
|
return OffloadResult(output=output, was_truncated=False, original_length=len(output))
|
|
139
112
|
|
|
@@ -165,14 +138,6 @@ class HeadTailOffloadStrategy(OffloadStrategy):
|
|
|
165
138
|
self.offload_dir = Path(offload_dir or TOOL_OUTPUT_TRUNCATION_DIR)
|
|
166
139
|
self._policy = policy
|
|
167
140
|
|
|
168
|
-
@property
|
|
169
|
-
def offload_policy(self) -> OffloadPolicy:
|
|
170
|
-
return self._policy
|
|
171
|
-
|
|
172
|
-
@property
|
|
173
|
-
def truncation_style(self) -> TruncationStyle:
|
|
174
|
-
return TruncationStyle.HEAD_TAIL
|
|
175
|
-
|
|
176
141
|
def _save_to_file(self, output: str, tool_call: ToolCallLike | None) -> str | None:
|
|
177
142
|
"""Save full output to file. Returns path or None on failure."""
|
|
178
143
|
try:
|
|
@@ -342,7 +342,7 @@ class BashTool(ToolABC):
|
|
|
342
342
|
if not combined:
|
|
343
343
|
combined = f"Command exited with code {rc}"
|
|
344
344
|
return message.ToolResultMessage(
|
|
345
|
-
status="
|
|
345
|
+
status="success",
|
|
346
346
|
# Preserve leading whitespace; only trim trailing newlines.
|
|
347
347
|
output_text=combined.rstrip("\n"),
|
|
348
348
|
)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Generate one or more images from a text prompt.
|
|
2
|
+
|
|
3
|
+
This tool invokes an Image Gen model to generate images. The generated image paths are automatically returned in the response.
|
|
4
|
+
|
|
5
|
+
Inputs:
|
|
6
|
+
- `prompt`: The main instruction describing the desired image.
|
|
7
|
+
- `image_paths` (optional): Local image file paths to use as references for editing or style guidance.
|
|
8
|
+
- `generation` (optional): Per-call image generation settings (aspect ratio / size).
|
|
9
|
+
|
|
10
|
+
Notes:
|
|
11
|
+
- Provide a short textual description of the generated image(s).
|
|
12
|
+
- Do NOT include base64 image data in text output.
|
|
13
|
+
- When providing multiple input images, describe each image's characteristics and purpose in the prompt, not just "image 1, image 2".
|
|
14
|
+
|
|
15
|
+
Multi-turn image editing:
|
|
16
|
+
- Use `resume` to continue editing a previously generated image. The agent preserves its full context including the generated image, so you don't need to pass `image_paths` again.
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""Image generation tool implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, cast
|
|
8
|
+
|
|
9
|
+
from klaude_code.core.tool.context import ToolContext
|
|
10
|
+
from klaude_code.core.tool.tool_abc import ToolABC, ToolConcurrencyPolicy, ToolMetadata, load_desc
|
|
11
|
+
from klaude_code.core.tool.tool_registry import register
|
|
12
|
+
from klaude_code.protocol import llm_param, message, model, tools
|
|
13
|
+
from klaude_code.protocol.sub_agent import get_sub_agent_profile
|
|
14
|
+
from klaude_code.protocol.sub_agent.image_gen import build_image_gen_prompt
|
|
15
|
+
from klaude_code.session.session import Session
|
|
16
|
+
|
|
17
|
+
IMAGE_GEN_PARAMETERS: dict[str, Any] = {
|
|
18
|
+
"type": "object",
|
|
19
|
+
"properties": {
|
|
20
|
+
"resume": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"description": "Optional agent ID to resume from. If provided, the agent will continue from the previous execution transcript.",
|
|
23
|
+
},
|
|
24
|
+
"description": {
|
|
25
|
+
"type": "string",
|
|
26
|
+
"description": "A short (3-5 word) description of the request.",
|
|
27
|
+
},
|
|
28
|
+
"prompt": {
|
|
29
|
+
"type": "string",
|
|
30
|
+
"description": "Text prompt for image generation.",
|
|
31
|
+
},
|
|
32
|
+
"image_paths": {
|
|
33
|
+
"type": "array",
|
|
34
|
+
"items": {"type": "string"},
|
|
35
|
+
"description": "Optional local image file paths used as references.",
|
|
36
|
+
},
|
|
37
|
+
"generation": {
|
|
38
|
+
"type": "object",
|
|
39
|
+
"description": "Optional per-call image generation settings.",
|
|
40
|
+
"properties": {
|
|
41
|
+
"aspect_ratio": {
|
|
42
|
+
"type": "string",
|
|
43
|
+
"description": "Aspect ratio, e.g. '16:9', '1:1', '9:16'.",
|
|
44
|
+
},
|
|
45
|
+
"image_size": {
|
|
46
|
+
"type": "string",
|
|
47
|
+
"enum": ["1K", "2K", "4K"],
|
|
48
|
+
"description": "Output size for Nano Banana Pro (must use uppercase K).",
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
"additionalProperties": False,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
"required": ["prompt"],
|
|
55
|
+
"additionalProperties": False,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@register(tools.IMAGE_GEN)
|
|
60
|
+
class ImageGenTool(ToolABC):
|
|
61
|
+
"""Generate or edit images using the ImageGen sub-agent."""
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
def metadata(cls) -> ToolMetadata:
|
|
65
|
+
return ToolMetadata(concurrency_policy=ToolConcurrencyPolicy.CONCURRENT, has_side_effects=True)
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def schema(cls) -> llm_param.ToolSchema:
|
|
69
|
+
return llm_param.ToolSchema(
|
|
70
|
+
name=tools.IMAGE_GEN,
|
|
71
|
+
type="function",
|
|
72
|
+
description=load_desc(Path(__file__).parent / "image_gen.md"),
|
|
73
|
+
parameters=IMAGE_GEN_PARAMETERS,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
async def call(cls, arguments: str, context: ToolContext) -> message.ToolResultMessage:
|
|
78
|
+
try:
|
|
79
|
+
args = json.loads(arguments)
|
|
80
|
+
except json.JSONDecodeError as exc:
|
|
81
|
+
return message.ToolResultMessage(status="error", output_text=f"Invalid JSON arguments: {exc}")
|
|
82
|
+
|
|
83
|
+
if not isinstance(args, dict):
|
|
84
|
+
return message.ToolResultMessage(status="error", output_text="Invalid arguments: expected object")
|
|
85
|
+
|
|
86
|
+
typed_args = cast(dict[str, Any], args)
|
|
87
|
+
|
|
88
|
+
runner = context.run_subtask
|
|
89
|
+
if runner is None:
|
|
90
|
+
return message.ToolResultMessage(status="error", output_text="No subtask runner available in this context")
|
|
91
|
+
|
|
92
|
+
resume_raw = typed_args.get("resume")
|
|
93
|
+
resume_session_id: str | None = None
|
|
94
|
+
if isinstance(resume_raw, str) and resume_raw.strip():
|
|
95
|
+
try:
|
|
96
|
+
resume_session_id = Session.resolve_sub_agent_session_id(resume_raw)
|
|
97
|
+
except ValueError as exc:
|
|
98
|
+
return message.ToolResultMessage(status="error", output_text=str(exc))
|
|
99
|
+
|
|
100
|
+
claims = context.sub_agent_resume_claims
|
|
101
|
+
if claims is not None:
|
|
102
|
+
ok = await claims.claim(resume_session_id)
|
|
103
|
+
if not ok:
|
|
104
|
+
return message.ToolResultMessage(
|
|
105
|
+
status="error",
|
|
106
|
+
output_text=(
|
|
107
|
+
"Duplicate sub-agent resume in the same response: "
|
|
108
|
+
f"resume='{resume_raw.strip()}' (resolved='{resume_session_id[:7]}…'). "
|
|
109
|
+
"Merge into a single call or resume in a later turn."
|
|
110
|
+
),
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
description = str(typed_args.get("description") or "")
|
|
114
|
+
prompt = build_image_gen_prompt(typed_args)
|
|
115
|
+
generation = typed_args.get("generation")
|
|
116
|
+
generation_dict: dict[str, Any] | None = (
|
|
117
|
+
cast(dict[str, Any], generation) if isinstance(generation, dict) else None
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
profile = get_sub_agent_profile(tools.IMAGE_GEN)
|
|
122
|
+
except KeyError as exc:
|
|
123
|
+
return message.ToolResultMessage(status="error", output_text=str(exc))
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
result = await runner(
|
|
127
|
+
model.SubAgentState(
|
|
128
|
+
sub_agent_type=profile.name,
|
|
129
|
+
sub_agent_desc=description,
|
|
130
|
+
sub_agent_prompt=prompt,
|
|
131
|
+
resume=resume_session_id,
|
|
132
|
+
output_schema=None,
|
|
133
|
+
generation=generation_dict,
|
|
134
|
+
),
|
|
135
|
+
context.record_sub_agent_session_id,
|
|
136
|
+
context.register_sub_agent_metadata_getter,
|
|
137
|
+
)
|
|
138
|
+
except Exception as exc:
|
|
139
|
+
return message.ToolResultMessage(status="error", output_text=f"Failed to run subtask: {exc}")
|
|
140
|
+
|
|
141
|
+
return message.ToolResultMessage(
|
|
142
|
+
status="success" if not result.error else "error",
|
|
143
|
+
output_text=result.task_result,
|
|
144
|
+
ui_extra=model.SessionIdUIExtra(session_id=result.session_id),
|
|
145
|
+
task_metadata=result.task_metadata,
|
|
146
|
+
)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Launch a new agent to handle complex, multi-step tasks autonomously.
|
|
2
|
+
|
|
3
|
+
The Task tool launches specialized agents (subprocesses) that autonomously handle complex tasks. Each agent type has specific capabilities and tools available to it.
|
|
4
|
+
|
|
5
|
+
When using the Task tool, you must specify a subagent_type parameter to select which agent type to use.
|
|
6
|
+
|
|
7
|
+
Available agent types and the tools they have access to:
|
|
8
|
+
|
|
9
|
+
${types_section}
|
|
10
|
+
|
|
11
|
+
Usage notes:
|
|
12
|
+
- Always include a short description (3-5 words) summarizing what the agent will do
|
|
13
|
+
- Launch multiple agents concurrently whenever possible, to maximize performance; to do that, use a single message with multiple tool uses
|
|
14
|
+
- Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, etc.), since it is not aware of the user's intent
|
|
15
|
+
- Provide clear, detailed prompts so the agent can work autonomously and return exactly the information you need.
|
|
16
|
+
- Agents can be resumed using the `resume` parameter by passing the agent ID from a previous invocation. When resumed, the agent continues with its full previous context preserved. When NOT resuming, each invocation starts fresh and you should provide a detailed task description with all necessary context.
|
|
17
|
+
- When the agent is done, it will return a single message back to you along with its agent ID. You can use this ID to resume the agent later if needed for follow-up work.
|
|
18
|
+
- If the agent description mentions that it should be used proactively, then you should try your best to use it without the user having to ask for it first. Use your judgement.
|
|
19
|
+
- If the user specifies that they want you to run agents "in parallel", you MUST send a single message with multiple Task tool use content blocks. For example, if you need to launch both a code-reviewer agent and a test-runner agent in parallel, send a single message with both tool calls.
|
|
20
|
+
- Agents can provide structured output by passing a JSON Schema in `output_schema`.
|