klaude-code 2.8.0__py3-none-any.whl → 2.9.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- klaude_code/app/runtime.py +2 -1
- klaude_code/auth/antigravity/oauth.py +0 -9
- klaude_code/auth/antigravity/token_manager.py +0 -18
- klaude_code/auth/base.py +53 -0
- 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 +15 -4
- klaude_code/config/assets/builtin_config.yaml +8 -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 +2 -2
- klaude_code/core/agent_profile.py +11 -53
- klaude_code/core/compaction/compaction.py +4 -6
- klaude_code/core/compaction/overflow.py +0 -4
- klaude_code/core/executor.py +51 -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 +0 -4
- klaude_code/core/tool/__init__.py +3 -2
- klaude_code/core/tool/file/apply_patch.py +0 -27
- klaude_code/core/tool/file/edit_tool.py +1 -2
- klaude_code/core/tool/file/read_tool.md +3 -2
- klaude_code/core/tool/file/read_tool.py +15 -2
- klaude_code/core/tool/offload.py +0 -35
- 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/codex/client.py +22 -0
- klaude_code/llm/codex/prompt_sync.py +237 -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 +14 -6
- klaude_code/llm/json_stable.py +37 -0
- klaude_code/llm/openai_compatible/input.py +0 -10
- klaude_code/llm/openai_compatible/stream.py +16 -1
- klaude_code/llm/registry.py +0 -5
- klaude_code/llm/responses/input.py +15 -5
- klaude_code/llm/usage.py +0 -8
- klaude_code/protocol/commands.py +1 -0
- klaude_code/protocol/events.py +2 -1
- klaude_code/protocol/message.py +2 -2
- klaude_code/protocol/model.py +20 -1
- klaude_code/protocol/op.py +27 -0
- klaude_code/protocol/op_handler.py +10 -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/export.py +308 -299
- klaude_code/session/session.py +58 -21
- klaude_code/session/store.py +0 -4
- klaude_code/session/templates/export_session.html +430 -134
- klaude_code/skill/assets/deslop/SKILL.md +9 -0
- klaude_code/skill/system_skills.py +0 -20
- klaude_code/tui/command/__init__.py +3 -0
- klaude_code/tui/command/continue_cmd.py +34 -0
- 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/command_output.py +3 -1
- klaude_code/tui/components/developer.py +3 -0
- klaude_code/tui/components/diffs.py +2 -208
- klaude_code/tui/components/errors.py +4 -0
- klaude_code/tui/components/mermaid_viewer.py +2 -2
- klaude_code/tui/components/rich/markdown.py +60 -63
- klaude_code/tui/components/rich/theme.py +2 -0
- klaude_code/tui/components/sub_agent.py +2 -46
- klaude_code/tui/components/thinking.py +0 -33
- klaude_code/tui/components/tools.py +43 -21
- 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 +15 -11
- klaude_code/tui/renderer.py +12 -20
- klaude_code/tui/runner.py +2 -1
- klaude_code/tui/terminal/image.py +6 -34
- klaude_code/ui/common.py +0 -70
- {klaude_code-2.8.0.dist-info → klaude_code-2.9.0.dist-info}/METADATA +3 -6
- {klaude_code-2.8.0.dist-info → klaude_code-2.9.0.dist-info}/RECORD +97 -92
- klaude_code/core/tool/sub_agent_tool.py +0 -126
- klaude_code/llm/openai_compatible/tool_call_accumulator.py +0 -108
- klaude_code/tui/components/rich/searchable_text.py +0 -68
- {klaude_code-2.8.0.dist-info → klaude_code-2.9.0.dist-info}/WHEEL +0 -0
- {klaude_code-2.8.0.dist-info → klaude_code-2.9.0.dist-info}/entry_points.txt +0 -0
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
|
|
|
@@ -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 []
|
|
@@ -114,10 +114,9 @@ class EditTool(ToolABC):
|
|
|
114
114
|
file_tracker = context.file_tracker
|
|
115
115
|
tracked_status: model.FileStatus | None = None
|
|
116
116
|
if not file_exists(file_path):
|
|
117
|
-
# We require reading before editing
|
|
118
117
|
return message.ToolResultMessage(
|
|
119
118
|
status="error",
|
|
120
|
-
output_text=("File
|
|
119
|
+
output_text=("File does not exist. If you want to create a file, use the Write tool instead."),
|
|
121
120
|
)
|
|
122
121
|
tracked_status = file_tracker.get(file_path)
|
|
123
122
|
if tracked_status is None:
|
|
@@ -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
|
|
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)
|
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:
|
|
@@ -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`.
|