klaude-code 2.1.1__py3-none-any.whl → 2.3.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 (72) hide show
  1. klaude_code/app/__init__.py +1 -2
  2. klaude_code/app/runtime.py +13 -41
  3. klaude_code/cli/list_model.py +27 -10
  4. klaude_code/cli/main.py +42 -159
  5. klaude_code/config/assets/builtin_config.yaml +36 -14
  6. klaude_code/config/config.py +144 -7
  7. klaude_code/config/select_model.py +38 -13
  8. klaude_code/config/sub_agent_model_helper.py +217 -0
  9. klaude_code/const.py +2 -2
  10. klaude_code/core/agent_profile.py +71 -5
  11. klaude_code/core/executor.py +75 -0
  12. klaude_code/core/manager/llm_clients_builder.py +18 -12
  13. klaude_code/core/prompts/prompt-nano-banana.md +1 -0
  14. klaude_code/core/tool/shell/command_safety.py +4 -189
  15. klaude_code/core/tool/sub_agent_tool.py +2 -1
  16. klaude_code/core/turn.py +1 -1
  17. klaude_code/llm/anthropic/client.py +8 -5
  18. klaude_code/llm/anthropic/input.py +54 -29
  19. klaude_code/llm/google/client.py +2 -2
  20. klaude_code/llm/google/input.py +23 -2
  21. klaude_code/llm/openai_compatible/input.py +22 -13
  22. klaude_code/llm/openai_compatible/stream.py +1 -1
  23. klaude_code/llm/openrouter/input.py +37 -25
  24. klaude_code/llm/responses/client.py +1 -1
  25. klaude_code/llm/responses/input.py +96 -57
  26. klaude_code/protocol/commands.py +1 -2
  27. klaude_code/protocol/events/system.py +4 -0
  28. klaude_code/protocol/message.py +2 -2
  29. klaude_code/protocol/op.py +17 -0
  30. klaude_code/protocol/op_handler.py +5 -0
  31. klaude_code/protocol/sub_agent/AGENTS.md +28 -0
  32. klaude_code/protocol/sub_agent/__init__.py +10 -14
  33. klaude_code/protocol/sub_agent/image_gen.py +2 -1
  34. klaude_code/session/codec.py +2 -6
  35. klaude_code/session/session.py +9 -1
  36. klaude_code/skill/assets/create-plan/SKILL.md +3 -5
  37. klaude_code/tui/command/__init__.py +7 -10
  38. klaude_code/tui/command/clear_cmd.py +1 -1
  39. klaude_code/tui/command/command_abc.py +1 -2
  40. klaude_code/tui/command/copy_cmd.py +1 -2
  41. klaude_code/tui/command/fork_session_cmd.py +4 -4
  42. klaude_code/tui/command/model_cmd.py +6 -43
  43. klaude_code/tui/command/model_select.py +75 -15
  44. klaude_code/tui/command/refresh_cmd.py +1 -2
  45. klaude_code/tui/command/resume_cmd.py +3 -4
  46. klaude_code/tui/command/status_cmd.py +1 -1
  47. klaude_code/tui/command/sub_agent_model_cmd.py +190 -0
  48. klaude_code/tui/components/bash_syntax.py +1 -1
  49. klaude_code/tui/components/common.py +1 -1
  50. klaude_code/tui/components/developer.py +10 -15
  51. klaude_code/tui/components/metadata.py +2 -64
  52. klaude_code/tui/components/rich/cjk_wrap.py +3 -2
  53. klaude_code/tui/components/rich/status.py +49 -3
  54. klaude_code/tui/components/rich/theme.py +4 -2
  55. klaude_code/tui/components/sub_agent.py +25 -46
  56. klaude_code/tui/components/user_input.py +9 -21
  57. klaude_code/tui/components/welcome.py +99 -0
  58. klaude_code/tui/input/prompt_toolkit.py +14 -1
  59. klaude_code/tui/renderer.py +2 -3
  60. klaude_code/tui/runner.py +2 -2
  61. klaude_code/tui/terminal/selector.py +8 -18
  62. klaude_code/ui/__init__.py +0 -24
  63. klaude_code/ui/common.py +3 -2
  64. klaude_code/ui/core/display.py +2 -2
  65. {klaude_code-2.1.1.dist-info → klaude_code-2.3.0.dist-info}/METADATA +16 -81
  66. {klaude_code-2.1.1.dist-info → klaude_code-2.3.0.dist-info}/RECORD +68 -67
  67. klaude_code/tui/command/help_cmd.py +0 -51
  68. klaude_code/tui/command/prompt-commit.md +0 -82
  69. klaude_code/tui/command/release_notes_cmd.py +0 -85
  70. klaude_code/ui/exec_mode.py +0 -60
  71. {klaude_code-2.1.1.dist-info → klaude_code-2.3.0.dist-info}/WHEEL +0 -0
  72. {klaude_code-2.1.1.dist-info → klaude_code-2.3.0.dist-info}/entry_points.txt +0 -0
@@ -54,7 +54,7 @@ def _build_config(param: llm_param.LLMCallParameter) -> GenerateContentConfig:
54
54
  system_instruction=param.system,
55
55
  temperature=param.temperature,
56
56
  max_output_tokens=param.max_tokens,
57
- tools=tool_list or None,
57
+ tools=cast(Any, tool_list) if tool_list else None,
58
58
  tool_config=tool_config,
59
59
  thinking_config=thinking_config,
60
60
  )
@@ -242,7 +242,7 @@ async def parse_google_stream(
242
242
 
243
243
  if call_id not in started_tool_items:
244
244
  started_tool_items.add(call_id)
245
- yield message.ToolCallStartItem(response_id=response_id, call_id=call_id, name=name)
245
+ yield message.ToolCallStartDelta(response_id=response_id, call_id=call_id, name=name)
246
246
 
247
247
  args_obj = getattr(function_call, "args", None)
248
248
  if args_obj is not None:
@@ -4,6 +4,8 @@
4
4
  # pyright: reportAttributeAccessIssue=false
5
5
 
6
6
  import json
7
+ from base64 import b64decode
8
+ from binascii import Error as BinasciiError
7
9
  from typing import Any
8
10
 
9
11
  from google.genai import types
@@ -32,6 +34,14 @@ def _image_part_to_part(image: message.ImageURLPart) -> types.Part:
32
34
  return types.Part(file_data=types.FileData(file_uri=url))
33
35
 
34
36
 
37
+ def _image_part_to_function_response_part(image: message.ImageURLPart) -> types.FunctionResponsePart:
38
+ url = image.url
39
+ if url.startswith("data:"):
40
+ media_type, _, decoded = parse_data_url(url)
41
+ return types.FunctionResponsePart.from_bytes(data=decoded, mime_type=media_type)
42
+ return types.FunctionResponsePart.from_uri(file_uri=url)
43
+
44
+
35
45
  def _user_message_to_content(msg: message.UserMessage, attachment: DeveloperAttachment) -> types.Content:
36
46
  parts: list[types.Part] = []
37
47
  for part in msg.parts:
@@ -65,9 +75,12 @@ def _tool_messages_to_contents(
65
75
 
66
76
  images = [part for part in msg.parts if isinstance(part, message.ImageURLPart)] + attachment.images
67
77
  image_parts: list[types.Part] = []
78
+ function_response_parts: list[types.FunctionResponsePart] = []
79
+
68
80
  for image in images:
69
81
  try:
70
82
  image_parts.append(_image_part_to_part(image))
83
+ function_response_parts.append(_image_part_to_function_response_part(image))
71
84
  except ValueError:
72
85
  continue
73
86
 
@@ -79,7 +92,7 @@ def _tool_messages_to_contents(
79
92
  id=msg.call_id,
80
93
  name=msg.tool_name,
81
94
  response=response_payload,
82
- parts=image_parts if (has_images and supports_multimodal_function_response) else None,
95
+ parts=function_response_parts if (has_images and supports_multimodal_function_response) else None,
83
96
  )
84
97
  response_parts.append(types.Part(function_response=function_response))
85
98
 
@@ -106,11 +119,19 @@ def _assistant_message_to_content(msg: message.AssistantMessage, model_name: str
106
119
  nonlocal pending_thought_text, pending_thought_signature
107
120
  if pending_thought_text is None and pending_thought_signature is None:
108
121
  return
122
+
123
+ signature_bytes: bytes | None = None
124
+ if pending_thought_signature:
125
+ try:
126
+ signature_bytes = b64decode(pending_thought_signature)
127
+ except (BinasciiError, ValueError):
128
+ signature_bytes = None
129
+
109
130
  parts.append(
110
131
  types.Part(
111
132
  text=pending_thought_text or "",
112
133
  thought=True,
113
- thought_signature=pending_thought_signature,
134
+ thought_signature=signature_bytes,
114
135
  )
115
136
  )
116
137
  pending_thought_text = None
@@ -3,6 +3,8 @@
3
3
  # pyright: reportUnknownMemberType=false
4
4
  # pyright: reportAttributeAccessIssue=false
5
5
 
6
+ from typing import cast
7
+
6
8
  from openai.types import chat
7
9
  from openai.types.chat import ChatCompletionContentPartParam
8
10
 
@@ -25,14 +27,16 @@ def _assistant_message_to_openai(msg: message.AssistantMessage) -> chat.ChatComp
25
27
  assistant_message["content"] = text_content
26
28
 
27
29
  assistant_message.update(build_assistant_common_fields(msg, image_to_data_url=assistant_image_to_data_url))
28
- return assistant_message
30
+ return cast(chat.ChatCompletionMessageParam, assistant_message)
29
31
 
30
32
 
31
33
  def build_user_content_parts(
32
34
  images: list[message.ImageURLPart],
33
35
  ) -> list[ChatCompletionContentPartParam]:
34
36
  """Build content parts for images only. Used by OpenRouter."""
35
- return [{"type": "image_url", "image_url": {"url": image.url}} for image in images]
37
+ return [
38
+ cast(ChatCompletionContentPartParam, {"type": "image_url", "image_url": {"url": image.url}}) for image in images
39
+ ]
36
40
 
37
41
 
38
42
  def convert_history_to_input(
@@ -42,19 +46,21 @@ def convert_history_to_input(
42
46
  ) -> list[chat.ChatCompletionMessageParam]:
43
47
  """Convert a list of messages to chat completion params."""
44
48
  del model_name
45
- messages: list[chat.ChatCompletionMessageParam] = [{"role": "system", "content": system}] if system else []
49
+ messages: list[chat.ChatCompletionMessageParam] = (
50
+ [cast(chat.ChatCompletionMessageParam, {"role": "system", "content": system})] if system else []
51
+ )
46
52
 
47
53
  for msg, attachment in attach_developer_messages(history):
48
54
  match msg:
49
55
  case message.SystemMessage():
50
56
  system_text = "\n".join(part.text for part in msg.parts)
51
57
  if system_text:
52
- messages.append({"role": "system", "content": system_text})
58
+ messages.append(cast(chat.ChatCompletionMessageParam, {"role": "system", "content": system_text}))
53
59
  case message.UserMessage():
54
60
  parts = build_chat_content_parts(msg, attachment)
55
- messages.append({"role": "user", "content": parts})
61
+ messages.append(cast(chat.ChatCompletionMessageParam, {"role": "user", "content": parts}))
56
62
  case message.ToolResultMessage():
57
- messages.append(build_tool_message(msg, attachment))
63
+ messages.append(cast(chat.ChatCompletionMessageParam, build_tool_message(msg, attachment)))
58
64
  case message.AssistantMessage():
59
65
  messages.append(_assistant_message_to_openai(msg))
60
66
  case _:
@@ -69,13 +75,16 @@ def convert_tool_schema(
69
75
  if tools is None:
70
76
  return []
71
77
  return [
72
- {
73
- "type": "function",
74
- "function": {
75
- "name": tool.name,
76
- "description": tool.description,
77
- "parameters": tool.parameters,
78
+ cast(
79
+ chat.ChatCompletionToolParam,
80
+ {
81
+ "type": "function",
82
+ "function": {
83
+ "name": tool.name,
84
+ "description": tool.description,
85
+ "parameters": tool.parameters,
86
+ },
78
87
  },
79
- }
88
+ )
80
89
  for tool in tools
81
90
  ]
@@ -303,7 +303,7 @@ async def parse_chat_completions_stream(
303
303
  for tc in tool_calls:
304
304
  if tc.index not in state.emitted_tool_start_indices and tc.function and tc.function.name:
305
305
  state.emitted_tool_start_indices.add(tc.index)
306
- yield message.ToolCallStartItem(
306
+ yield message.ToolCallStartDelta(
307
307
  response_id=state.response_id,
308
308
  call_id=tc.id or "",
309
309
  name=tc.function.name,
@@ -6,6 +6,8 @@
6
6
  # pyright: reportUnnecessaryIsInstance=false
7
7
  # pyright: reportGeneralTypeIssues=false
8
8
 
9
+ from typing import cast
10
+
9
11
  from openai.types import chat
10
12
 
11
13
  from klaude_code.llm.image import assistant_image_to_data_url
@@ -71,7 +73,7 @@ def _assistant_message_to_openrouter(
71
73
  if content_parts:
72
74
  assistant_message["content"] = "\n".join(content_parts)
73
75
 
74
- return assistant_message
76
+ return cast(chat.ChatCompletionMessageParam, assistant_message)
75
77
 
76
78
 
77
79
  def _add_cache_control(messages: list[chat.ChatCompletionMessageParam], use_cache_control: bool) -> None:
@@ -98,19 +100,24 @@ def convert_history_to_input(
98
100
 
99
101
  messages: list[chat.ChatCompletionMessageParam] = (
100
102
  [
101
- {
102
- "role": "system",
103
- "content": [
104
- {
105
- "type": "text",
106
- "text": system,
107
- "cache_control": {"type": "ephemeral"},
108
- }
109
- ],
110
- }
103
+ cast(
104
+ chat.ChatCompletionMessageParam,
105
+ {
106
+ "role": "system",
107
+ "content": [
108
+ {
109
+ "type": "text",
110
+ "text": system,
111
+ "cache_control": {"type": "ephemeral"},
112
+ }
113
+ ],
114
+ },
115
+ )
111
116
  ]
112
117
  if system and use_cache_control
113
- else ([{"role": "system", "content": system}] if system else [])
118
+ else (
119
+ [cast(chat.ChatCompletionMessageParam, {"role": "system", "content": system})] if system else []
120
+ )
114
121
  )
115
122
 
116
123
  for msg, attachment in attach_developer_messages(history):
@@ -120,24 +127,29 @@ def convert_history_to_input(
120
127
  if system_text:
121
128
  if use_cache_control:
122
129
  messages.append(
123
- {
124
- "role": "system",
125
- "content": [
126
- {
127
- "type": "text",
128
- "text": system_text,
129
- "cache_control": {"type": "ephemeral"},
130
- }
131
- ],
132
- }
130
+ cast(
131
+ chat.ChatCompletionMessageParam,
132
+ {
133
+ "role": "system",
134
+ "content": [
135
+ {
136
+ "type": "text",
137
+ "text": system_text,
138
+ "cache_control": {"type": "ephemeral"},
139
+ }
140
+ ],
141
+ },
142
+ )
133
143
  )
134
144
  else:
135
- messages.append({"role": "system", "content": system_text})
145
+ messages.append(
146
+ cast(chat.ChatCompletionMessageParam, {"role": "system", "content": system_text})
147
+ )
136
148
  case message.UserMessage():
137
149
  parts = build_chat_content_parts(msg, attachment)
138
- messages.append({"role": "user", "content": parts})
150
+ messages.append(cast(chat.ChatCompletionMessageParam, {"role": "user", "content": parts}))
139
151
  case message.ToolResultMessage():
140
- messages.append(build_tool_message(msg, attachment))
152
+ messages.append(cast(chat.ChatCompletionMessageParam, build_tool_message(msg, attachment)))
141
153
  case message.AssistantMessage():
142
154
  messages.append(_assistant_message_to_openrouter(msg, model_name))
143
155
  case _:
@@ -145,7 +145,7 @@ async def parse_responses_stream(
145
145
  case responses.ResponseOutputItemAddedEvent() as event:
146
146
  if isinstance(event.item, responses.ResponseFunctionToolCall):
147
147
  metadata_tracker.record_token()
148
- yield message.ToolCallStartItem(
148
+ yield message.ToolCallStartDelta(
149
149
  response_id=response_id,
150
150
  call_id=event.item.call_id,
151
151
  name=event.item.name,
@@ -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,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):
@@ -25,7 +25,7 @@ from klaude_code.protocol.model import (
25
25
  # Stream items
26
26
 
27
27
 
28
- class ToolCallStartItem(BaseModel):
28
+ class ToolCallStartDelta(BaseModel):
29
29
  """Transient streaming signal when LLM starts a tool call.
30
30
 
31
31
  This is NOT persisted to conversation history. Used only for
@@ -175,7 +175,7 @@ Message = SystemMessage | DeveloperMessage | UserMessage | AssistantMessage | To
175
175
 
176
176
  HistoryEvent = Message | StreamErrorItem | TaskMetadataItem
177
177
 
178
- StreamItem = AssistantTextDelta | AssistantImageDelta | ThinkingTextDelta | ToolCallStartItem
178
+ StreamItem = AssistantTextDelta | AssistantImageDelta | ThinkingTextDelta | ToolCallStartDelta
179
179
 
180
180
  LLMStreamItem = HistoryEvent | StreamItem
181
181
 
@@ -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"
@@ -97,6 +98,22 @@ class ChangeThinkingOperation(Operation):
97
98
  await handler.handle_change_thinking(self)
98
99
 
99
100
 
101
+ class ChangeSubAgentModelOperation(Operation):
102
+ """Operation for changing the model used by a specific sub-agent."""
103
+
104
+ type: OperationType = OperationType.CHANGE_SUB_AGENT_MODEL
105
+ session_id: str
106
+ sub_agent_type: str
107
+ # When None, clear explicit override and fall back to the sub-agent's default
108
+ # behavior (usually inherit from main agent; some sub-agents auto-resolve a
109
+ # suitable model, e.g. ImageGen).
110
+ model_name: str | None
111
+ save_as_default: bool = False
112
+
113
+ async def execute(self, handler: OperationHandler) -> None:
114
+ await handler.handle_change_sub_agent_model(self)
115
+
116
+
100
117
  class ClearSessionOperation(Operation):
101
118
  """Operation for clearing the active session and starting a new one."""
102
119
 
@@ -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)