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.
Files changed (107) hide show
  1. klaude_code/app/runtime.py +2 -1
  2. klaude_code/auth/antigravity/oauth.py +33 -38
  3. klaude_code/auth/antigravity/token_manager.py +0 -18
  4. klaude_code/auth/base.py +53 -0
  5. klaude_code/auth/claude/oauth.py +34 -49
  6. klaude_code/auth/codex/exceptions.py +0 -4
  7. klaude_code/auth/codex/oauth.py +32 -28
  8. klaude_code/auth/codex/token_manager.py +0 -18
  9. klaude_code/cli/cost_cmd.py +128 -39
  10. klaude_code/cli/list_model.py +27 -10
  11. klaude_code/cli/main.py +14 -3
  12. klaude_code/config/assets/builtin_config.yaml +25 -24
  13. klaude_code/config/config.py +47 -25
  14. klaude_code/config/sub_agent_model_helper.py +18 -13
  15. klaude_code/config/thinking.py +0 -8
  16. klaude_code/const.py +1 -1
  17. klaude_code/core/agent_profile.py +11 -56
  18. klaude_code/core/compaction/overflow.py +0 -4
  19. klaude_code/core/executor.py +33 -5
  20. klaude_code/core/manager/llm_clients.py +9 -1
  21. klaude_code/core/prompts/prompt-claude-code.md +4 -4
  22. klaude_code/core/reminders.py +21 -23
  23. klaude_code/core/task.py +1 -5
  24. klaude_code/core/tool/__init__.py +3 -2
  25. klaude_code/core/tool/file/apply_patch.py +0 -27
  26. klaude_code/core/tool/file/read_tool.md +3 -2
  27. klaude_code/core/tool/file/read_tool.py +27 -3
  28. klaude_code/core/tool/offload.py +0 -35
  29. klaude_code/core/tool/shell/bash_tool.py +1 -1
  30. klaude_code/core/tool/sub_agent/__init__.py +6 -0
  31. klaude_code/core/tool/sub_agent/image_gen.md +16 -0
  32. klaude_code/core/tool/sub_agent/image_gen.py +146 -0
  33. klaude_code/core/tool/sub_agent/task.md +20 -0
  34. klaude_code/core/tool/sub_agent/task.py +205 -0
  35. klaude_code/core/tool/tool_registry.py +0 -16
  36. klaude_code/core/turn.py +1 -1
  37. klaude_code/llm/anthropic/input.py +6 -5
  38. klaude_code/llm/antigravity/input.py +14 -7
  39. klaude_code/llm/bedrock_anthropic/__init__.py +3 -0
  40. klaude_code/llm/google/client.py +8 -6
  41. klaude_code/llm/google/input.py +20 -12
  42. klaude_code/llm/image.py +18 -11
  43. klaude_code/llm/input_common.py +32 -6
  44. klaude_code/llm/json_stable.py +37 -0
  45. klaude_code/llm/{codex → openai_codex}/__init__.py +1 -1
  46. klaude_code/llm/{codex → openai_codex}/client.py +24 -2
  47. klaude_code/llm/openai_codex/prompt_sync.py +237 -0
  48. klaude_code/llm/openai_compatible/client.py +3 -1
  49. klaude_code/llm/openai_compatible/input.py +0 -10
  50. klaude_code/llm/openai_compatible/stream.py +35 -10
  51. klaude_code/llm/{responses → openai_responses}/client.py +1 -1
  52. klaude_code/llm/{responses → openai_responses}/input.py +15 -5
  53. klaude_code/llm/registry.py +3 -8
  54. klaude_code/llm/stream_parts.py +3 -1
  55. klaude_code/llm/usage.py +1 -9
  56. klaude_code/protocol/events.py +2 -2
  57. klaude_code/protocol/message.py +3 -2
  58. klaude_code/protocol/model.py +34 -2
  59. klaude_code/protocol/op.py +13 -0
  60. klaude_code/protocol/op_handler.py +5 -0
  61. klaude_code/protocol/sub_agent/AGENTS.md +5 -5
  62. klaude_code/protocol/sub_agent/__init__.py +13 -34
  63. klaude_code/protocol/sub_agent/explore.py +7 -34
  64. klaude_code/protocol/sub_agent/image_gen.py +3 -74
  65. klaude_code/protocol/sub_agent/task.py +3 -47
  66. klaude_code/protocol/sub_agent/web.py +8 -52
  67. klaude_code/protocol/tools.py +2 -0
  68. klaude_code/session/session.py +80 -22
  69. klaude_code/session/store.py +0 -4
  70. klaude_code/skill/assets/deslop/SKILL.md +9 -0
  71. klaude_code/skill/system_skills.py +0 -20
  72. klaude_code/tui/command/fork_session_cmd.py +5 -2
  73. klaude_code/tui/command/resume_cmd.py +9 -2
  74. klaude_code/tui/command/sub_agent_model_cmd.py +85 -18
  75. klaude_code/tui/components/assistant.py +0 -26
  76. klaude_code/tui/components/bash_syntax.py +4 -0
  77. klaude_code/tui/components/command_output.py +3 -1
  78. klaude_code/tui/components/developer.py +3 -0
  79. klaude_code/tui/components/diffs.py +4 -209
  80. klaude_code/tui/components/errors.py +4 -0
  81. klaude_code/tui/components/mermaid_viewer.py +2 -2
  82. klaude_code/tui/components/metadata.py +0 -3
  83. klaude_code/tui/components/rich/markdown.py +120 -87
  84. klaude_code/tui/components/rich/status.py +2 -2
  85. klaude_code/tui/components/rich/theme.py +11 -6
  86. klaude_code/tui/components/sub_agent.py +2 -46
  87. klaude_code/tui/components/thinking.py +0 -33
  88. klaude_code/tui/components/tools.py +65 -21
  89. klaude_code/tui/components/user_input.py +2 -0
  90. klaude_code/tui/input/images.py +21 -18
  91. klaude_code/tui/input/key_bindings.py +2 -2
  92. klaude_code/tui/input/prompt_toolkit.py +49 -49
  93. klaude_code/tui/machine.py +29 -47
  94. klaude_code/tui/renderer.py +48 -33
  95. klaude_code/tui/runner.py +2 -1
  96. klaude_code/tui/terminal/image.py +27 -34
  97. klaude_code/ui/common.py +0 -70
  98. {klaude_code-2.8.1.dist-info → klaude_code-2.9.1.dist-info}/METADATA +3 -6
  99. {klaude_code-2.8.1.dist-info → klaude_code-2.9.1.dist-info}/RECORD +103 -99
  100. klaude_code/core/tool/sub_agent_tool.py +0 -126
  101. klaude_code/llm/bedrock/__init__.py +0 -3
  102. klaude_code/llm/openai_compatible/tool_call_accumulator.py +0 -108
  103. klaude_code/tui/components/rich/searchable_text.py +0 -68
  104. /klaude_code/llm/{bedrock → bedrock_anthropic}/client.py +0 -0
  105. /klaude_code/llm/{responses → openai_responses}/__init__.py +0 -0
  106. {klaude_code-2.8.1.dist-info → klaude_code-2.9.1.dist-info}/WHEEL +0 -0
  107. {klaude_code-2.8.1.dist-info → klaude_code-2.9.1.dist-info}/entry_points.txt +0 -0
@@ -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
- return self.sub_clients.get(sub_agent_type) or self.main
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 Explore tool in order to reduce context usage.
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 Explore tool instead of running search commands directly.
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 Explore tool to find the files that handle client errors instead of using Glob or Grep directly]
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 Explore tool]
86
+ assistant: [Uses the Task tool with subagent_type=Explore]
87
87
  </example>
@@ -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=[model.AtFileOpsUIItem(ops=at_ops)]),
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 get_last_user_message_image_count(session: Session) -> int:
414
- """Get image count from the last user message in conversation history."""
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 0
411
+ return []
418
412
  if isinstance(item, message.UserMessage):
419
- return len([part for part in item.parts if isinstance(part, message.ImageURLPart)])
420
- return 0
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
- image_count = get_last_user_message_image_count(session)
426
- if image_count == 0:
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=image_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, cancelled=True))
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 .sub_agent_tool import SubAgentTool
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
- "SubAgentTool",
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 2000 lines starting from the beginning of the file
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 2000 characters will be truncated
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(Path(__file__).parent / "read_tool.md"),
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(status="success", output_text=output_text, parts=[image_part])
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
- return message.ToolResultMessage(status="success", output_text=read_result_str)
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)
@@ -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="error",
345
+ status="success",
346
346
  # Preserve leading whitespace; only trim trailing newlines.
347
347
  output_text=combined.rstrip("\n"),
348
348
  )
@@ -0,0 +1,6 @@
1
+ """Sub-agent tool implementations."""
2
+
3
+ from .image_gen import ImageGenTool
4
+ from .task import TaskTool
5
+
6
+ __all__ = ["ImageGenTool", "TaskTool"]
@@ -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`.