klaude-code 2.2.0__py3-none-any.whl → 2.4.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 (82) hide show
  1. klaude_code/app/runtime.py +2 -15
  2. klaude_code/cli/list_model.py +30 -13
  3. klaude_code/cli/main.py +26 -10
  4. klaude_code/config/assets/builtin_config.yaml +177 -310
  5. klaude_code/config/config.py +158 -21
  6. klaude_code/config/{select_model.py → model_matcher.py} +41 -16
  7. klaude_code/config/sub_agent_model_helper.py +217 -0
  8. klaude_code/config/thinking.py +2 -2
  9. klaude_code/const.py +1 -1
  10. klaude_code/core/agent_profile.py +43 -5
  11. klaude_code/core/executor.py +129 -47
  12. klaude_code/core/manager/llm_clients_builder.py +17 -11
  13. klaude_code/core/prompts/prompt-nano-banana.md +1 -1
  14. klaude_code/core/tool/file/diff_builder.py +25 -18
  15. klaude_code/core/tool/sub_agent_tool.py +2 -1
  16. klaude_code/llm/anthropic/client.py +12 -9
  17. klaude_code/llm/anthropic/input.py +54 -29
  18. klaude_code/llm/client.py +1 -1
  19. klaude_code/llm/codex/client.py +2 -2
  20. klaude_code/llm/google/client.py +7 -7
  21. klaude_code/llm/google/input.py +23 -2
  22. klaude_code/llm/input_common.py +2 -2
  23. klaude_code/llm/openai_compatible/client.py +3 -3
  24. klaude_code/llm/openai_compatible/input.py +22 -13
  25. klaude_code/llm/openai_compatible/stream.py +1 -1
  26. klaude_code/llm/openrouter/client.py +4 -4
  27. klaude_code/llm/openrouter/input.py +35 -25
  28. klaude_code/llm/responses/client.py +5 -5
  29. klaude_code/llm/responses/input.py +96 -57
  30. klaude_code/protocol/commands.py +1 -2
  31. klaude_code/protocol/events/__init__.py +7 -1
  32. klaude_code/protocol/events/chat.py +10 -0
  33. klaude_code/protocol/events/system.py +4 -0
  34. klaude_code/protocol/llm_param.py +1 -1
  35. klaude_code/protocol/model.py +0 -26
  36. klaude_code/protocol/op.py +17 -5
  37. klaude_code/protocol/op_handler.py +5 -0
  38. klaude_code/protocol/sub_agent/AGENTS.md +28 -0
  39. klaude_code/protocol/sub_agent/__init__.py +10 -14
  40. klaude_code/protocol/sub_agent/image_gen.py +2 -1
  41. klaude_code/session/codec.py +2 -6
  42. klaude_code/session/session.py +13 -3
  43. klaude_code/skill/assets/create-plan/SKILL.md +3 -5
  44. klaude_code/tui/command/__init__.py +3 -6
  45. klaude_code/tui/command/clear_cmd.py +0 -1
  46. klaude_code/tui/command/command_abc.py +6 -4
  47. klaude_code/tui/command/copy_cmd.py +10 -10
  48. klaude_code/tui/command/debug_cmd.py +11 -10
  49. klaude_code/tui/command/export_online_cmd.py +18 -23
  50. klaude_code/tui/command/fork_session_cmd.py +39 -43
  51. klaude_code/tui/command/model_cmd.py +10 -49
  52. klaude_code/tui/command/model_picker.py +142 -0
  53. klaude_code/tui/command/refresh_cmd.py +0 -1
  54. klaude_code/tui/command/registry.py +15 -21
  55. klaude_code/tui/command/resume_cmd.py +10 -16
  56. klaude_code/tui/command/status_cmd.py +8 -12
  57. klaude_code/tui/command/sub_agent_model_cmd.py +185 -0
  58. klaude_code/tui/command/terminal_setup_cmd.py +8 -11
  59. klaude_code/tui/command/thinking_cmd.py +4 -6
  60. klaude_code/tui/commands.py +5 -0
  61. klaude_code/tui/components/bash_syntax.py +1 -1
  62. klaude_code/tui/components/command_output.py +96 -0
  63. klaude_code/tui/components/common.py +1 -1
  64. klaude_code/tui/components/developer.py +3 -115
  65. klaude_code/tui/components/metadata.py +1 -63
  66. klaude_code/tui/components/rich/cjk_wrap.py +3 -2
  67. klaude_code/tui/components/rich/status.py +49 -3
  68. klaude_code/tui/components/rich/theme.py +2 -0
  69. klaude_code/tui/components/sub_agent.py +25 -46
  70. klaude_code/tui/components/welcome.py +99 -0
  71. klaude_code/tui/input/prompt_toolkit.py +19 -8
  72. klaude_code/tui/machine.py +5 -0
  73. klaude_code/tui/renderer.py +7 -8
  74. klaude_code/tui/runner.py +0 -6
  75. klaude_code/tui/terminal/selector.py +8 -6
  76. {klaude_code-2.2.0.dist-info → klaude_code-2.4.0.dist-info}/METADATA +21 -74
  77. {klaude_code-2.2.0.dist-info → klaude_code-2.4.0.dist-info}/RECORD +79 -76
  78. klaude_code/tui/command/help_cmd.py +0 -51
  79. klaude_code/tui/command/model_select.py +0 -84
  80. klaude_code/tui/command/release_notes_cmd.py +0 -85
  81. {klaude_code-2.2.0.dist-info → klaude_code-2.4.0.dist-info}/WHEEL +0 -0
  82. {klaude_code-2.2.0.dist-info → klaude_code-2.4.0.dist-info}/entry_points.txt +0 -0
@@ -2,7 +2,7 @@
2
2
  # pyright: reportArgumentType=false
3
3
  # pyright: reportAssignmentType=false
4
4
 
5
- from typing import Any
5
+ from typing import Any, cast
6
6
 
7
7
  from openai.types import responses
8
8
 
@@ -23,15 +23,25 @@ def _build_user_content_parts(
23
23
  parts: list[responses.ResponseInputContentParam] = []
24
24
  for part in user.parts:
25
25
  if isinstance(part, message.TextPart):
26
- parts.append({"type": "input_text", "text": part.text})
26
+ parts.append(cast(responses.ResponseInputContentParam, {"type": "input_text", "text": part.text}))
27
27
  elif isinstance(part, message.ImageURLPart):
28
- parts.append({"type": "input_image", "detail": "auto", "image_url": part.url})
28
+ parts.append(
29
+ cast(
30
+ responses.ResponseInputContentParam,
31
+ {"type": "input_image", "detail": "auto", "image_url": part.url},
32
+ )
33
+ )
29
34
  if attachment.text:
30
- parts.append({"type": "input_text", "text": attachment.text})
35
+ parts.append(cast(responses.ResponseInputContentParam, {"type": "input_text", "text": attachment.text}))
31
36
  for image in attachment.images:
32
- parts.append({"type": "input_image", "detail": "auto", "image_url": image.url})
37
+ parts.append(
38
+ cast(
39
+ responses.ResponseInputContentParam,
40
+ {"type": "input_image", "detail": "auto", "image_url": image.url},
41
+ )
42
+ )
33
43
  if not parts:
34
- parts.append({"type": "input_text", "text": ""})
44
+ parts.append(cast(responses.ResponseInputContentParam, {"type": "input_text", "text": ""}))
35
45
  return parts
36
46
 
37
47
 
@@ -45,17 +55,22 @@ def _build_tool_result_item(
45
55
  attachment.text,
46
56
  )
47
57
  if text_output:
48
- content_parts.append({"type": "input_text", "text": text_output})
58
+ content_parts.append(cast(responses.ResponseInputContentParam, {"type": "input_text", "text": text_output}))
49
59
  images = [part for part in tool.parts if isinstance(part, message.ImageURLPart)] + attachment.images
50
60
  for image in images:
51
- content_parts.append({"type": "input_image", "detail": "auto", "image_url": image.url})
61
+ content_parts.append(
62
+ cast(
63
+ responses.ResponseInputContentParam,
64
+ {"type": "input_image", "detail": "auto", "image_url": image.url},
65
+ )
66
+ )
52
67
 
53
68
  item: dict[str, Any] = {
54
69
  "type": "function_call_output",
55
70
  "call_id": tool.call_id,
56
71
  "output": content_parts,
57
72
  }
58
- return item
73
+ return cast(responses.ResponseInputItemParam, item)
59
74
 
60
75
 
61
76
  def convert_history_to_input(
@@ -73,25 +88,30 @@ def convert_history_to_input(
73
88
  system_text = "\n".join(part.text for part in msg.parts)
74
89
  if system_text:
75
90
  items.append(
76
- {
77
- "type": "message",
78
- "role": "system",
79
- "content": [
80
- {
81
- "type": "input_text",
82
- "text": system_text,
83
- }
84
- ],
85
- }
91
+ cast(
92
+ responses.ResponseInputItemParam,
93
+ {
94
+ "type": "message",
95
+ "role": "system",
96
+ "content": [
97
+ cast(
98
+ responses.ResponseInputContentParam,
99
+ {"type": "input_text", "text": system_text},
100
+ )
101
+ ],
102
+ },
103
+ )
86
104
  )
87
105
  case message.UserMessage():
88
106
  items.append(
89
- {
90
- "type": "message",
91
- "role": "user",
92
- "id": msg.id,
93
- "content": _build_user_content_parts(msg, attachment),
94
- }
107
+ cast(
108
+ responses.ResponseInputItemParam,
109
+ {
110
+ "type": "message",
111
+ "role": "user",
112
+ "content": _build_user_content_parts(msg, attachment),
113
+ },
114
+ )
95
115
  )
96
116
  case message.ToolResultMessage():
97
117
  items.append(_build_tool_result_item(msg, attachment))
@@ -103,17 +123,19 @@ def convert_history_to_input(
103
123
  native_thinking_ids = {id(part) for part in native_thinking_parts}
104
124
  degraded_thinking_texts.extend(degraded_for_message)
105
125
 
106
- def flush_text(*, _message_id: str = msg.id) -> None:
126
+ def flush_text() -> None:
107
127
  nonlocal assistant_text_parts
108
128
  if not assistant_text_parts:
109
129
  return
110
130
  items.append(
111
- {
112
- "type": "message",
113
- "role": "assistant",
114
- "id": _message_id,
115
- "content": assistant_text_parts,
116
- }
131
+ cast(
132
+ responses.ResponseInputItemParam,
133
+ {
134
+ "type": "message",
135
+ "role": "assistant",
136
+ "content": assistant_text_parts,
137
+ },
138
+ )
117
139
  )
118
140
  assistant_text_parts = []
119
141
 
@@ -140,17 +162,25 @@ def convert_history_to_input(
140
162
 
141
163
  emit_reasoning()
142
164
  if isinstance(part, message.TextPart):
143
- assistant_text_parts.append({"type": "output_text", "text": part.text})
165
+ assistant_text_parts.append(
166
+ cast(
167
+ responses.ResponseInputContentParam,
168
+ {"type": "input_text", "text": part.text},
169
+ )
170
+ )
144
171
  elif isinstance(part, message.ToolCallPart):
145
172
  flush_text()
146
173
  items.append(
147
- {
148
- "type": "function_call",
149
- "name": part.tool_name,
150
- "arguments": part.arguments_json,
151
- "call_id": part.call_id,
152
- "id": part.id,
153
- }
174
+ cast(
175
+ responses.ResponseInputItemParam,
176
+ {
177
+ "type": "function_call",
178
+ "name": part.tool_name,
179
+ "arguments": part.arguments_json,
180
+ "call_id": part.call_id,
181
+ "id": part.id,
182
+ },
183
+ )
154
184
  )
155
185
 
156
186
  emit_reasoning()
@@ -159,16 +189,22 @@ def convert_history_to_input(
159
189
  continue
160
190
 
161
191
  if degraded_thinking_texts:
162
- degraded_item: responses.ResponseInputItemParam = {
163
- "type": "message",
164
- "role": "assistant",
165
- "content": [
166
- {
167
- "type": "output_text",
168
- "text": "<thinking>\n" + "\n".join(degraded_thinking_texts) + "\n</thinking>",
169
- }
170
- ],
171
- }
192
+ degraded_item = cast(
193
+ responses.ResponseInputItemParam,
194
+ {
195
+ "type": "message",
196
+ "role": "assistant",
197
+ "content": [
198
+ cast(
199
+ responses.ResponseInputContentParam,
200
+ {
201
+ "type": "input_text",
202
+ "text": "<thinking>\n" + "\n".join(degraded_thinking_texts) + "\n</thinking>",
203
+ },
204
+ )
205
+ ],
206
+ },
207
+ )
172
208
  items.insert(0, degraded_item)
173
209
 
174
210
  return items
@@ -184,7 +220,7 @@ def convert_reasoning_inputs(text_content: str | None, signature: str | None) ->
184
220
  ]
185
221
  if signature:
186
222
  result["encrypted_content"] = signature
187
- return result
223
+ return cast(responses.ResponseInputItemParam, result)
188
224
 
189
225
 
190
226
  def convert_tool_schema(
@@ -193,11 +229,14 @@ def convert_tool_schema(
193
229
  if tools is None:
194
230
  return []
195
231
  return [
196
- {
197
- "type": "function",
198
- "name": tool.name,
199
- "description": tool.description,
200
- "parameters": tool.parameters,
201
- }
232
+ cast(
233
+ responses.ToolParam,
234
+ {
235
+ "type": "function",
236
+ "name": tool.name,
237
+ "description": tool.description,
238
+ "parameters": tool.parameters,
239
+ },
240
+ )
202
241
  for tool in tools
203
242
  ]
@@ -15,8 +15,8 @@ class CommandInfo:
15
15
  class CommandName(str, Enum):
16
16
  INIT = "init"
17
17
  DEBUG = "debug"
18
- HELP = "help"
19
18
  MODEL = "model"
19
+ SUB_AGENT_MODEL = "sub-agent-model"
20
20
  COMPACT = "compact"
21
21
  REFRESH_TERMINAL = "refresh-terminal"
22
22
  CLEAR = "clear"
@@ -24,7 +24,6 @@ class CommandName(str, Enum):
24
24
  EXPORT = "export"
25
25
  EXPORT_ONLINE = "export-online"
26
26
  STATUS = "status"
27
- RELEASE_NOTES = "release-notes"
28
27
  THINKING = "thinking"
29
28
  FORK_SESSION = "fork-session"
30
29
  RESUME = "resume"
@@ -1,7 +1,12 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from klaude_code.protocol.events.base import Event, ResponseEvent
4
- from klaude_code.protocol.events.chat import DeveloperMessageEvent, TodoChangeEvent, UserMessageEvent
4
+ from klaude_code.protocol.events.chat import (
5
+ CommandOutputEvent,
6
+ DeveloperMessageEvent,
7
+ TodoChangeEvent,
8
+ UserMessageEvent,
9
+ )
5
10
  from klaude_code.protocol.events.lifecycle import TaskFinishEvent, TaskStartEvent, TurnEndEvent, TurnStartEvent
6
11
  from klaude_code.protocol.events.metadata import TaskMetadataEvent, UsageEvent
7
12
  from klaude_code.protocol.events.streaming import (
@@ -30,6 +35,7 @@ __all__ = [
30
35
  "AssistantTextDeltaEvent",
31
36
  "AssistantTextEndEvent",
32
37
  "AssistantTextStartEvent",
38
+ "CommandOutputEvent",
33
39
  "DeveloperMessageEvent",
34
40
  "EndEvent",
35
41
  "ErrorEvent",
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from klaude_code.protocol import message, model
4
+ from klaude_code.protocol.commands import CommandName
4
5
 
5
6
  from .base import Event
6
7
 
@@ -18,3 +19,12 @@ class DeveloperMessageEvent(Event):
18
19
 
19
20
  class TodoChangeEvent(Event):
20
21
  todos: list[model.TodoItem]
22
+
23
+
24
+ class CommandOutputEvent(Event):
25
+ """Event for command output display. Not persisted to session history."""
26
+
27
+ command_name: CommandName | str
28
+ content: str = ""
29
+ ui_extra: model.ToolResultUIExtra | None = None
30
+ is_error: bool = False
@@ -1,5 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from pydantic import Field
4
+
3
5
  from klaude_code.protocol import llm_param
4
6
  from klaude_code.protocol.events.chat import DeveloperMessageEvent, UserMessageEvent
5
7
  from klaude_code.protocol.events.lifecycle import TaskFinishEvent, TaskStartEvent, TurnStartEvent
@@ -14,6 +16,8 @@ class WelcomeEvent(Event):
14
16
  work_dir: str
15
17
  llm_config: llm_param.LLMConfigParameter
16
18
  show_klaude_code_info: bool = True
19
+ show_sub_agent_models: bool = True
20
+ sub_agent_models: dict[str, llm_param.LLMConfigParameter] = Field(default_factory=dict)
17
21
 
18
22
 
19
23
  class ErrorEvent(Event):
@@ -119,7 +119,7 @@ class LLMConfigProviderParameter(BaseModel):
119
119
 
120
120
 
121
121
  class LLMConfigModelParameter(BaseModel):
122
- model: str | None = None
122
+ model_id: str | None = None
123
123
  temperature: float | None = None
124
124
  max_tokens: int | None = None
125
125
  context_limit: int | None = None
@@ -5,7 +5,6 @@ from typing import Annotated, Any, Literal
5
5
  from pydantic import BaseModel, Field, computed_field
6
6
 
7
7
  from klaude_code.const import DEFAULT_MAX_TOKENS
8
- from klaude_code.protocol.commands import CommandName
9
8
  from klaude_code.protocol.tools import SubAgentType
10
9
 
11
10
  RoleType = Literal["system", "developer", "user", "assistant", "tool"]
@@ -272,12 +271,6 @@ ToolResultUIExtra = Annotated[
272
271
  ]
273
272
 
274
273
 
275
- class CommandOutput(BaseModel):
276
- command_name: CommandName
277
- ui_extra: ToolResultUIExtra | None = None
278
- is_error: bool = False
279
-
280
-
281
274
  class MemoryFileLoaded(BaseModel):
282
275
  path: str
283
276
  mentioned_patterns: list[str] = Field(default_factory=list)
@@ -319,11 +312,6 @@ class SkillActivatedUIItem(BaseModel):
319
312
  name: str
320
313
 
321
314
 
322
- class CommandOutputUIItem(BaseModel):
323
- type: Literal["command_output"] = "command_output"
324
- output: CommandOutput
325
-
326
-
327
315
  type DeveloperUIItem = (
328
316
  MemoryLoadedUIItem
329
317
  | ExternalFileChangesUIItem
@@ -331,7 +319,6 @@ type DeveloperUIItem = (
331
319
  | AtFileOpsUIItem
332
320
  | UserImagesUIItem
333
321
  | SkillActivatedUIItem
334
- | CommandOutputUIItem
335
322
  )
336
323
 
337
324
 
@@ -343,19 +330,6 @@ class DeveloperUIExtra(BaseModel):
343
330
  items: list[DeveloperUIItem] = Field(default_factory=_empty_developer_ui_items)
344
331
 
345
332
 
346
- def build_command_output_extra(
347
- command_name: CommandName,
348
- *,
349
- ui_extra: ToolResultUIExtra | None = None,
350
- is_error: bool = False,
351
- ) -> DeveloperUIExtra:
352
- return DeveloperUIExtra(
353
- items=[
354
- CommandOutputUIItem(output=CommandOutput(command_name=command_name, ui_extra=ui_extra, is_error=is_error))
355
- ]
356
- )
357
-
358
-
359
333
  class SubAgentState(BaseModel):
360
334
  sub_agent_type: SubAgentType
361
335
  sub_agent_desc: str
@@ -25,6 +25,7 @@ class OperationType(Enum):
25
25
 
26
26
  RUN_AGENT = "run_agent"
27
27
  CHANGE_MODEL = "change_model"
28
+ CHANGE_SUB_AGENT_MODEL = "change_sub_agent_model"
28
29
  CHANGE_THINKING = "change_thinking"
29
30
  CLEAR_SESSION = "clear_session"
30
31
  RESUME_SESSION = "resume_session"
@@ -51,11 +52,6 @@ class RunAgentOperation(Operation):
51
52
  type: OperationType = OperationType.RUN_AGENT
52
53
  session_id: str
53
54
  input: UserInputPayload
54
- # Frontends may choose to render the user message themselves (e.g. TUI) to support
55
- # event-only commands; in that case the core should skip emitting the UserMessageEvent.
56
- emit_user_message_event: bool = True
57
- # Frontends may choose to run without persisting input (e.g. some interactive commands).
58
- persist_user_input: bool = True
59
55
 
60
56
  async def execute(self, handler: OperationHandler) -> None:
61
57
  await handler.handle_run_agent(self)
@@ -97,6 +93,22 @@ class ChangeThinkingOperation(Operation):
97
93
  await handler.handle_change_thinking(self)
98
94
 
99
95
 
96
+ class ChangeSubAgentModelOperation(Operation):
97
+ """Operation for changing the model used by a specific sub-agent."""
98
+
99
+ type: OperationType = OperationType.CHANGE_SUB_AGENT_MODEL
100
+ session_id: str
101
+ sub_agent_type: str
102
+ # When None, clear explicit override and fall back to the sub-agent's default
103
+ # behavior (usually inherit from main agent; some sub-agents auto-resolve a
104
+ # suitable model, e.g. ImageGen).
105
+ model_name: str | None
106
+ save_as_default: bool = False
107
+
108
+ async def execute(self, handler: OperationHandler) -> None:
109
+ await handler.handle_change_sub_agent_model(self)
110
+
111
+
100
112
  class ClearSessionOperation(Operation):
101
113
  """Operation for clearing the active session and starting a new one."""
102
114
 
@@ -11,6 +11,7 @@ from typing import TYPE_CHECKING, Protocol
11
11
  if TYPE_CHECKING:
12
12
  from klaude_code.protocol.op import (
13
13
  ChangeModelOperation,
14
+ ChangeSubAgentModelOperation,
14
15
  ChangeThinkingOperation,
15
16
  ClearSessionOperation,
16
17
  ExportSessionOperation,
@@ -36,6 +37,10 @@ class OperationHandler(Protocol):
36
37
  """Handle a change thinking operation."""
37
38
  ...
38
39
 
40
+ async def handle_change_sub_agent_model(self, operation: ChangeSubAgentModelOperation) -> None:
41
+ """Handle a change sub-agent model operation."""
42
+ ...
43
+
39
44
  async def handle_clear_session(self, operation: ClearSessionOperation) -> None:
40
45
  """Handle a clear session operation."""
41
46
  ...
@@ -0,0 +1,28 @@
1
+ # Sub-Agent Protocol
2
+
3
+ Sub-agents are specialized agents invoked by the main agent as tools. This module defines profiles and registration.
4
+
5
+ ## Key Constraint
6
+
7
+ The `protocol` layer cannot import from `config` or `core` (enforced by import-linter). Availability checks are delegated to upper layers via string constants.
8
+
9
+ ## Core Files
10
+
11
+ - `__init__.py` - `SubAgentProfile` dataclass and registration. Defines `AVAILABILITY_*` constants.
12
+ - `image_gen.py`, `task.py`, `explore.py`, `web.py` - Individual sub-agent definitions.
13
+
14
+ ## Availability Requirement Flow
15
+
16
+ Some sub-agents require specific model capabilities (e.g., ImageGen needs an image model). The flow:
17
+
18
+ 1. `SubAgentProfile.availability_requirement` stores a constant (e.g., `AVAILABILITY_IMAGE_MODEL`)
19
+ 2. `config/sub_agent_model_helper.py` checks if the requirement is met based on `config/config.py`
20
+ 3. `config/sub_agent_model_helper.py` resolves the default model when unset (e.g., first available image model)
21
+ 4. Core builders/UI call into the helper to avoid dealing with requirement constants directly
22
+
23
+ ## Model Selection
24
+
25
+ For sub-agents with `availability_requirement`, priority is:
26
+ 1. Explicit config in `sub_agent_models`
27
+ 2. Auto-resolve via requirement
28
+ 3. If neither found, sub-agent is unavailable (no fallback to main agent model)
@@ -9,9 +9,11 @@ from klaude_code.protocol import tools
9
9
  if TYPE_CHECKING:
10
10
  from klaude_code.protocol import model
11
11
 
12
- AvailabilityPredicate = Callable[[str], bool]
13
12
  PromptBuilder = Callable[[dict[str, Any]], str]
14
13
 
14
+ # Availability requirement constants
15
+ AVAILABILITY_IMAGE_MODEL = "image_model"
16
+
15
17
 
16
18
  @dataclass
17
19
  class SubAgentResult:
@@ -58,17 +60,13 @@ class SubAgentProfile:
58
60
  # Availability
59
61
  enabled_by_default: bool = True
60
62
  show_in_main_agent: bool = True
61
- target_model_filter: AvailabilityPredicate | None = None
62
63
 
63
64
  # Structured output support: specifies which parameter in the tool schema contains the output schema
64
65
  output_schema_arg: str | None = None
65
66
 
66
- def enabled_for_model(self, model_name: str | None) -> bool:
67
- if not self.enabled_by_default:
68
- return False
69
- if model_name is None or self.target_model_filter is None:
70
- return True
71
- return self.target_model_filter(model_name)
67
+ # Config-based availability requirement (e.g., "image_model" means requires an image model)
68
+ # The actual check is performed in the core layer to avoid circular imports.
69
+ availability_requirement: str | None = None
72
70
 
73
71
 
74
72
  _PROFILES: dict[str, SubAgentProfile] = {}
@@ -87,11 +85,11 @@ def get_sub_agent_profile(sub_agent_type: tools.SubAgentType) -> SubAgentProfile
87
85
  raise KeyError(f"Unknown sub agent type: {sub_agent_type}") from exc
88
86
 
89
87
 
90
- def iter_sub_agent_profiles(enabled_only: bool = False, model_name: str | None = None) -> list[SubAgentProfile]:
88
+ def iter_sub_agent_profiles(enabled_only: bool = False) -> list[SubAgentProfile]:
91
89
  profiles = list(_PROFILES.values())
92
90
  if not enabled_only:
93
91
  return profiles
94
- return [p for p in profiles if p.enabled_for_model(model_name)]
92
+ return [p for p in profiles if p.enabled_by_default]
95
93
 
96
94
 
97
95
  def get_sub_agent_profile_by_tool(tool_name: str) -> SubAgentProfile | None:
@@ -102,11 +100,9 @@ def is_sub_agent_tool(tool_name: str) -> bool:
102
100
  return tool_name in _PROFILES
103
101
 
104
102
 
105
- def sub_agent_tool_names(enabled_only: bool = False, model_name: str | None = None) -> list[str]:
103
+ def sub_agent_tool_names(enabled_only: bool = False) -> list[str]:
106
104
  return [
107
- profile.name
108
- for profile in iter_sub_agent_profiles(enabled_only=enabled_only, model_name=model_name)
109
- if profile.show_in_main_agent
105
+ profile.name for profile in iter_sub_agent_profiles(enabled_only=enabled_only) if profile.show_in_main_agent
110
106
  ]
111
107
 
112
108
 
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  from typing import Any, cast
4
4
 
5
- from klaude_code.protocol.sub_agent import SubAgentProfile, register_sub_agent
5
+ from klaude_code.protocol.sub_agent import AVAILABILITY_IMAGE_MODEL, SubAgentProfile, register_sub_agent
6
6
 
7
7
  IMAGE_GEN_DESCRIPTION = """\
8
8
  Generate one or more images from a text prompt.
@@ -115,5 +115,6 @@ register_sub_agent(
115
115
  tool_set=(),
116
116
  prompt_builder=_build_image_gen_prompt,
117
117
  active_form="Generating Image",
118
+ availability_requirement=AVAILABILITY_IMAGE_MODEL,
118
119
  )
119
120
  )
@@ -1,17 +1,13 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import json
4
- from typing import Any, TypeGuard, cast, get_args
4
+ from typing import Any, cast, get_args
5
5
 
6
6
  from pydantic import BaseModel
7
7
 
8
8
  from klaude_code.protocol import message
9
9
 
10
10
 
11
- def _is_basemodel_subclass(tp: object) -> TypeGuard[type[BaseModel]]:
12
- return isinstance(tp, type) and issubclass(tp, BaseModel)
13
-
14
-
15
11
  def _flatten_union(tp: object) -> list[object]:
16
12
  args = list(get_args(tp))
17
13
  if not args:
@@ -25,7 +21,7 @@ def _flatten_union(tp: object) -> list[object]:
25
21
  def _build_type_registry() -> dict[str, type[BaseModel]]:
26
22
  registry: dict[str, type[BaseModel]] = {}
27
23
  for tp in _flatten_union(message.HistoryEvent):
28
- if not _is_basemodel_subclass(tp):
24
+ if not isinstance(tp, type) or not issubclass(tp, BaseModel):
29
25
  continue
30
26
  registry[tp.__name__] = tp
31
27
  return registry
@@ -237,7 +237,7 @@ class Session(BaseModel):
237
237
  Args:
238
238
  new_id: Optional ID for the forked session.
239
239
  until_index: If provided, only copy conversation history up to (but not including) this index.
240
- If None, copy all history.
240
+ If -1, copy all history.
241
241
  """
242
242
 
243
243
  forked = Session.create(id=new_id, work_dir=self.work_dir)
@@ -250,7 +250,9 @@ class Session(BaseModel):
250
250
  forked.todos = [todo.model_copy(deep=True) for todo in self.todos]
251
251
 
252
252
  history_to_copy = (
253
- self.conversation_history[:until_index] if until_index is not None else self.conversation_history
253
+ self.conversation_history[:until_index]
254
+ if (until_index is not None and until_index >= 0)
255
+ else self.conversation_history
254
256
  )
255
257
  items = [it.model_copy(deep=True) for it in history_to_copy]
256
258
  if items:
@@ -370,8 +372,16 @@ class Session(BaseModel):
370
372
 
371
373
  has_structured_output = report_back_result is not None
372
374
  task_result = report_back_result if has_structured_output else last_assistant_content
375
+
376
+ if self.sub_agent_state is not None:
377
+ trimmed = (task_result or "").rstrip()
378
+ lines = trimmed.splitlines()
379
+ if not (lines and lines[-1].startswith("agentId:")):
380
+ footer = f"agentId: {self.id} (for resuming to continue this agent's work if needed)"
381
+ task_result = f"{trimmed}\n\n{footer}" if trimmed.strip() else footer
382
+
373
383
  yield events.TaskFinishEvent(
374
- session_id=self.id, task_result=task_result, has_structured_output=has_structured_output
384
+ session_id=self.id, task_result=task_result or "", has_structured_output=has_structured_output
375
385
  )
376
386
 
377
387
  def _iter_sub_agent_history(
@@ -13,6 +13,8 @@ Turn a user prompt into a **single, actionable plan** delivered in the final ass
13
13
 
14
14
  ## Minimal workflow
15
15
 
16
+ Throughout the entire workflow, operate in read-only mode. Do not write or update files.
17
+
16
18
  1. **Scan context quickly**
17
19
  - Read `README.md` and any obvious docs (`docs/`, `CONTRIBUTING.md`, `ARCHITECTURE.md`).
18
20
  - Skim relevant files (the ones most likely touched).
@@ -33,11 +35,7 @@ Turn a user prompt into a **single, actionable plan** delivered in the final ass
33
35
  - Include at least one item for **tests/validation** and one for **edge cases/risk** when applicable.
34
36
  - If there are unknowns, include a tiny **Open questions** section (max 3).
35
37
 
36
- 4. **Write the plan to `plan.md` in the current working directory**
37
- - Use the Write tool to save the plan to `./plan.md`
38
- - If `plan.md` already exists, overwrite it with the new plan
39
-
40
- 5. **Do not preface the plan with meta explanations; output only the plan as per template**
38
+ 4. **Do not preface the plan with meta explanations; output only the plan as per template**
41
39
 
42
40
  ## Plan template (follow exactly)
43
41