klaude-code 2.8.1__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.
Files changed (93) hide show
  1. klaude_code/app/runtime.py +2 -1
  2. klaude_code/auth/antigravity/oauth.py +0 -9
  3. klaude_code/auth/antigravity/token_manager.py +0 -18
  4. klaude_code/auth/base.py +53 -0
  5. klaude_code/auth/codex/exceptions.py +0 -4
  6. klaude_code/auth/codex/oauth.py +32 -28
  7. klaude_code/auth/codex/token_manager.py +0 -18
  8. klaude_code/cli/cost_cmd.py +128 -39
  9. klaude_code/cli/list_model.py +27 -10
  10. klaude_code/cli/main.py +14 -3
  11. klaude_code/config/assets/builtin_config.yaml +8 -24
  12. klaude_code/config/config.py +47 -25
  13. klaude_code/config/sub_agent_model_helper.py +18 -13
  14. klaude_code/config/thinking.py +0 -8
  15. klaude_code/const.py +1 -1
  16. klaude_code/core/agent_profile.py +10 -52
  17. klaude_code/core/compaction/overflow.py +0 -4
  18. klaude_code/core/executor.py +33 -5
  19. klaude_code/core/manager/llm_clients.py +9 -1
  20. klaude_code/core/prompts/prompt-claude-code.md +4 -4
  21. klaude_code/core/reminders.py +21 -23
  22. klaude_code/core/task.py +0 -4
  23. klaude_code/core/tool/__init__.py +3 -2
  24. klaude_code/core/tool/file/apply_patch.py +0 -27
  25. klaude_code/core/tool/file/read_tool.md +3 -2
  26. klaude_code/core/tool/file/read_tool.py +15 -2
  27. klaude_code/core/tool/offload.py +0 -35
  28. klaude_code/core/tool/sub_agent/__init__.py +6 -0
  29. klaude_code/core/tool/sub_agent/image_gen.md +16 -0
  30. klaude_code/core/tool/sub_agent/image_gen.py +146 -0
  31. klaude_code/core/tool/sub_agent/task.md +20 -0
  32. klaude_code/core/tool/sub_agent/task.py +205 -0
  33. klaude_code/core/tool/tool_registry.py +0 -16
  34. klaude_code/core/turn.py +1 -1
  35. klaude_code/llm/anthropic/input.py +6 -5
  36. klaude_code/llm/antigravity/input.py +14 -7
  37. klaude_code/llm/codex/client.py +22 -0
  38. klaude_code/llm/codex/prompt_sync.py +237 -0
  39. klaude_code/llm/google/client.py +8 -6
  40. klaude_code/llm/google/input.py +20 -12
  41. klaude_code/llm/image.py +18 -11
  42. klaude_code/llm/input_common.py +14 -6
  43. klaude_code/llm/json_stable.py +37 -0
  44. klaude_code/llm/openai_compatible/input.py +0 -10
  45. klaude_code/llm/openai_compatible/stream.py +16 -1
  46. klaude_code/llm/registry.py +0 -5
  47. klaude_code/llm/responses/input.py +15 -5
  48. klaude_code/llm/usage.py +0 -8
  49. klaude_code/protocol/events.py +2 -1
  50. klaude_code/protocol/message.py +2 -2
  51. klaude_code/protocol/model.py +20 -1
  52. klaude_code/protocol/op.py +13 -0
  53. klaude_code/protocol/op_handler.py +5 -0
  54. klaude_code/protocol/sub_agent/AGENTS.md +5 -5
  55. klaude_code/protocol/sub_agent/__init__.py +13 -34
  56. klaude_code/protocol/sub_agent/explore.py +7 -34
  57. klaude_code/protocol/sub_agent/image_gen.py +3 -74
  58. klaude_code/protocol/sub_agent/task.py +3 -47
  59. klaude_code/protocol/sub_agent/web.py +8 -52
  60. klaude_code/protocol/tools.py +2 -0
  61. klaude_code/session/session.py +58 -21
  62. klaude_code/session/store.py +0 -4
  63. klaude_code/skill/assets/deslop/SKILL.md +9 -0
  64. klaude_code/skill/system_skills.py +0 -20
  65. klaude_code/tui/command/fork_session_cmd.py +5 -2
  66. klaude_code/tui/command/resume_cmd.py +9 -2
  67. klaude_code/tui/command/sub_agent_model_cmd.py +85 -18
  68. klaude_code/tui/components/assistant.py +0 -26
  69. klaude_code/tui/components/command_output.py +3 -1
  70. klaude_code/tui/components/developer.py +3 -0
  71. klaude_code/tui/components/diffs.py +2 -208
  72. klaude_code/tui/components/errors.py +4 -0
  73. klaude_code/tui/components/mermaid_viewer.py +2 -2
  74. klaude_code/tui/components/rich/markdown.py +0 -54
  75. klaude_code/tui/components/rich/theme.py +2 -0
  76. klaude_code/tui/components/sub_agent.py +2 -46
  77. klaude_code/tui/components/thinking.py +0 -33
  78. klaude_code/tui/components/tools.py +43 -21
  79. klaude_code/tui/input/images.py +21 -18
  80. klaude_code/tui/input/key_bindings.py +2 -2
  81. klaude_code/tui/input/prompt_toolkit.py +49 -49
  82. klaude_code/tui/machine.py +15 -11
  83. klaude_code/tui/renderer.py +11 -20
  84. klaude_code/tui/runner.py +2 -1
  85. klaude_code/tui/terminal/image.py +6 -34
  86. klaude_code/ui/common.py +0 -70
  87. {klaude_code-2.8.1.dist-info → klaude_code-2.9.0.dist-info}/METADATA +3 -6
  88. {klaude_code-2.8.1.dist-info → klaude_code-2.9.0.dist-info}/RECORD +90 -86
  89. klaude_code/core/tool/sub_agent_tool.py +0 -126
  90. klaude_code/llm/openai_compatible/tool_call_accumulator.py +0 -108
  91. klaude_code/tui/components/rich/searchable_text.py +0 -68
  92. {klaude_code-2.8.1.dist-info → klaude_code-2.9.0.dist-info}/WHEEL +0 -0
  93. {klaude_code-2.8.1.dist-info → klaude_code-2.9.0.dist-info}/entry_points.txt +0 -0
@@ -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
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)
@@ -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,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`.
@@ -0,0 +1,205 @@
1
+ """Task tool implementation for running sub-agents by type."""
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, iter_sub_agent_profiles
14
+ from klaude_code.session.session import Session
15
+
16
+ TASK_TYPE_TO_SUB_AGENT: dict[str, str] = {
17
+ "general-purpose": "Task",
18
+ "explore": "Explore",
19
+ "web": "Web",
20
+ }
21
+
22
+
23
+ def _task_description() -> str:
24
+ summaries: dict[str, str] = {}
25
+ for profile in iter_sub_agent_profiles():
26
+ if profile.invoker_type:
27
+ summaries[profile.invoker_type] = profile.invoker_summary.strip()
28
+
29
+ type_lines: list[str] = []
30
+ for invoker_type in TASK_TYPE_TO_SUB_AGENT:
31
+ summary = summaries.get(invoker_type, "")
32
+ if summary:
33
+ type_lines.append(f"- {invoker_type}: {summary}")
34
+ else:
35
+ type_lines.append(f"- {invoker_type}")
36
+
37
+ types_section = "\n".join(type_lines) if type_lines else "- general-purpose"
38
+
39
+ return load_desc(Path(__file__).parent / "task.md", {"types_section": types_section})
40
+
41
+
42
+ TASK_SCHEMA = llm_param.ToolSchema(
43
+ name=tools.TASK,
44
+ type="function",
45
+ description=_task_description(),
46
+ parameters={
47
+ "type": "object",
48
+ "properties": {
49
+ "type": {
50
+ "type": "string",
51
+ "enum": list(TASK_TYPE_TO_SUB_AGENT.keys()),
52
+ "description": "Sub-agent type selector.",
53
+ },
54
+ "description": {
55
+ "type": "string",
56
+ "description": "A short (3-5 word) description of the task.",
57
+ },
58
+ "prompt": {
59
+ "type": "string",
60
+ "description": "The task for the agent to perform.",
61
+ },
62
+ "output_schema": {
63
+ "type": "object",
64
+ "description": "Optional JSON Schema for structured output.",
65
+ },
66
+ "resume": {
67
+ "type": "string",
68
+ "description": "Optional agent ID to resume from.",
69
+ },
70
+ },
71
+ "required": ["description", "prompt"],
72
+ "additionalProperties": False,
73
+ },
74
+ )
75
+
76
+
77
+ @register(tools.TASK)
78
+ class TaskTool(ToolABC):
79
+ """Run a sub-agent based on the requested type."""
80
+
81
+ @classmethod
82
+ def metadata(cls) -> ToolMetadata:
83
+ return ToolMetadata(concurrency_policy=ToolConcurrencyPolicy.CONCURRENT, has_side_effects=True)
84
+
85
+ @classmethod
86
+ def schema(cls) -> llm_param.ToolSchema:
87
+ return TASK_SCHEMA
88
+
89
+ @classmethod
90
+ async def call(cls, arguments: str, context: ToolContext) -> message.ToolResultMessage:
91
+ try:
92
+ args = json.loads(arguments)
93
+ except json.JSONDecodeError as exc:
94
+ return message.ToolResultMessage(status="error", output_text=f"Invalid JSON arguments: {exc}")
95
+
96
+ if not isinstance(args, dict):
97
+ return message.ToolResultMessage(status="error", output_text="Invalid arguments: expected object")
98
+
99
+ typed_args = cast(dict[str, Any], args)
100
+
101
+ runner = context.run_subtask
102
+ if runner is None:
103
+ return message.ToolResultMessage(status="error", output_text="No subtask runner available in this context")
104
+
105
+ description = str(typed_args.get("description") or "")
106
+
107
+ resume_raw = typed_args.get("resume")
108
+ resume_session_id: str | None = None
109
+ resume_sub_agent_type: str | None = None
110
+ if isinstance(resume_raw, str) and resume_raw.strip():
111
+ try:
112
+ resume_session_id = Session.resolve_sub_agent_session_id(resume_raw)
113
+ except ValueError as exc:
114
+ return message.ToolResultMessage(status="error", output_text=str(exc))
115
+
116
+ try:
117
+ resume_session = Session.load(resume_session_id)
118
+ except (OSError, ValueError, json.JSONDecodeError) as exc:
119
+ return message.ToolResultMessage(
120
+ status="error",
121
+ output_text=f"Failed to resume sub-agent session '{resume_session_id}': {exc}",
122
+ )
123
+
124
+ if resume_session.sub_agent_state is None:
125
+ return message.ToolResultMessage(
126
+ status="error",
127
+ output_text=f"Invalid resume id '{resume_session_id}': target session is not a sub-agent session",
128
+ )
129
+
130
+ resume_sub_agent_type = resume_session.sub_agent_state.sub_agent_type
131
+ if resume_sub_agent_type == tools.IMAGE_GEN:
132
+ return message.ToolResultMessage(
133
+ status="error",
134
+ output_text="This resume id belongs to ImageGen; use the ImageGen tool to resume it.",
135
+ )
136
+
137
+ claims = context.sub_agent_resume_claims
138
+ if claims is not None:
139
+ ok = await claims.claim(resume_session_id)
140
+ if not ok:
141
+ return message.ToolResultMessage(
142
+ status="error",
143
+ output_text=(
144
+ "Duplicate sub-agent resume in the same response: "
145
+ f"resume='{resume_raw.strip()}' (resolved='{resume_session_id[:7]}…'). "
146
+ "Merge into a single call or resume in a later turn."
147
+ ),
148
+ )
149
+
150
+ type_raw = typed_args.get("type")
151
+ requested_type = str(type_raw).strip() if isinstance(type_raw, str) else ""
152
+
153
+ if resume_session_id and not requested_type:
154
+ sub_agent_type = resume_sub_agent_type or TASK_TYPE_TO_SUB_AGENT["general-purpose"]
155
+ else:
156
+ if not requested_type:
157
+ requested_type = "general-purpose"
158
+ sub_agent_type = TASK_TYPE_TO_SUB_AGENT.get(requested_type)
159
+ if sub_agent_type is None:
160
+ return message.ToolResultMessage(
161
+ status="error",
162
+ output_text=f"Unknown Task type '{requested_type}'.",
163
+ )
164
+
165
+ if resume_session_id and resume_sub_agent_type and resume_sub_agent_type != sub_agent_type:
166
+ return message.ToolResultMessage(
167
+ status="error",
168
+ output_text=(
169
+ "Invalid resume id: sub-agent type mismatch. "
170
+ f"Expected '{sub_agent_type}', got '{resume_sub_agent_type}'."
171
+ ),
172
+ )
173
+
174
+ try:
175
+ profile = get_sub_agent_profile(sub_agent_type)
176
+ except KeyError as exc:
177
+ return message.ToolResultMessage(status="error", output_text=str(exc))
178
+
179
+ sub_agent_prompt = profile.prompt_builder(typed_args)
180
+
181
+ output_schema_raw = typed_args.get("output_schema")
182
+ output_schema = cast(dict[str, Any], output_schema_raw) if isinstance(output_schema_raw, dict) else None
183
+
184
+ try:
185
+ result = await runner(
186
+ model.SubAgentState(
187
+ sub_agent_type=profile.name,
188
+ sub_agent_desc=description,
189
+ sub_agent_prompt=sub_agent_prompt,
190
+ resume=resume_session_id,
191
+ output_schema=output_schema,
192
+ generation=None,
193
+ ),
194
+ context.record_sub_agent_session_id,
195
+ context.register_sub_agent_metadata_getter,
196
+ )
197
+ except Exception as exc:
198
+ return message.ToolResultMessage(status="error", output_text=f"Failed to run subtask: {exc}")
199
+
200
+ return message.ToolResultMessage(
201
+ status="success" if not result.error else "error",
202
+ output_text=result.task_result,
203
+ ui_extra=model.SessionIdUIExtra(session_id=result.session_id),
204
+ task_metadata=result.task_metadata,
205
+ )
@@ -1,10 +1,8 @@
1
1
  from collections.abc import Callable
2
2
  from typing import TypeVar
3
3
 
4
- from klaude_code.core.tool.sub_agent_tool import SubAgentTool
5
4
  from klaude_code.core.tool.tool_abc import ToolABC
6
5
  from klaude_code.protocol import llm_param
7
- from klaude_code.protocol.sub_agent import iter_sub_agent_profiles
8
6
 
9
7
  _REGISTRY: dict[str, type[ToolABC]] = {}
10
8
 
@@ -19,20 +17,6 @@ def register(name: str) -> Callable[[type[T]], type[T]]:
19
17
  return _decorator
20
18
 
21
19
 
22
- def _register_sub_agent_tools() -> None:
23
- """Automatically register all sub-agent tools based on their profiles."""
24
- for profile in iter_sub_agent_profiles():
25
- tool_cls = SubAgentTool.for_profile(profile)
26
- _REGISTRY[profile.name] = tool_cls
27
-
28
-
29
- _register_sub_agent_tools()
30
-
31
-
32
- def list_tools() -> list[str]:
33
- return list(_REGISTRY.keys())
34
-
35
-
36
20
  def get_tool_schemas(tool_names: list[str]) -> list[llm_param.ToolSchema]:
37
21
  schemas: list[llm_param.ToolSchema] = []
38
22
  for tool_name in tool_names:
klaude_code/core/turn.py CHANGED
@@ -243,7 +243,7 @@ class TurnExecutor:
243
243
  )
244
244
 
245
245
  # ImageGen per-call overrides (tool-level `generation` parameters)
246
- if ctx.sub_agent_state is not None and ctx.sub_agent_state.sub_agent_type == "ImageGen":
246
+ if ctx.sub_agent_state is not None and ctx.sub_agent_state.sub_agent_type == tools.IMAGE_GEN:
247
247
  call_param.modalities = ["image", "text"]
248
248
  generation = ctx.sub_agent_state.generation or {}
249
249
  image_config = llm_param.ImageConfig()
@@ -18,9 +18,10 @@ from anthropic.types.beta.beta_tool_use_block_param import BetaToolUseBlockParam
18
18
  from anthropic.types.beta.beta_url_image_source_param import BetaURLImageSourceParam
19
19
 
20
20
  from klaude_code.const import EMPTY_TOOL_OUTPUT_MESSAGE
21
- from klaude_code.llm.image import parse_data_url
21
+ from klaude_code.llm.image import image_file_to_data_url, parse_data_url
22
22
  from klaude_code.llm.input_common import (
23
23
  DeveloperAttachment,
24
+ ImagePart,
24
25
  attach_developer_messages,
25
26
  merge_reminder_text,
26
27
  split_thinking_parts,
@@ -36,8 +37,8 @@ _INLINE_IMAGE_MEDIA_TYPES: tuple[AllowedMediaType, ...] = (
36
37
  )
37
38
 
38
39
 
39
- def _image_part_to_block(image: message.ImageURLPart) -> BetaImageBlockParam:
40
- url = image.url
40
+ def _image_part_to_block(image: ImagePart) -> BetaImageBlockParam:
41
+ url = image_file_to_data_url(image) if isinstance(image, message.ImageFilePart) else image.url
41
42
  if url.startswith("data:"):
42
43
  media_type, base64_payload, _ = parse_data_url(url)
43
44
  if media_type not in _INLINE_IMAGE_MEDIA_TYPES:
@@ -64,7 +65,7 @@ def _user_message_to_message(
64
65
  for part in msg.parts:
65
66
  if isinstance(part, message.TextPart):
66
67
  blocks.append(cast(BetaTextBlockParam, {"type": "text", "text": part.text}))
67
- elif isinstance(part, message.ImageURLPart):
68
+ elif isinstance(part, (message.ImageURLPart, message.ImageFilePart)):
68
69
  blocks.append(_image_part_to_block(part))
69
70
  if attachment.text:
70
71
  blocks.append(cast(BetaTextBlockParam, {"type": "text", "text": attachment.text}))
@@ -86,7 +87,7 @@ def _tool_message_to_block(
86
87
  attachment.text,
87
88
  )
88
89
  tool_content.append(cast(BetaTextBlockParam, {"type": "text", "text": merged_text}))
89
- for image in [part for part in msg.parts if isinstance(part, message.ImageURLPart)]:
90
+ for image in [part for part in msg.parts if isinstance(part, (message.ImageURLPart, message.ImageFilePart))]:
90
91
  tool_content.append(_image_part_to_block(image))
91
92
  for image in attachment.images:
92
93
  tool_content.append(_image_part_to_block(image))