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.
- klaude_code/app/__init__.py +1 -2
- klaude_code/app/runtime.py +13 -41
- klaude_code/cli/list_model.py +27 -10
- klaude_code/cli/main.py +42 -159
- klaude_code/config/assets/builtin_config.yaml +36 -14
- klaude_code/config/config.py +144 -7
- klaude_code/config/select_model.py +38 -13
- klaude_code/config/sub_agent_model_helper.py +217 -0
- klaude_code/const.py +2 -2
- klaude_code/core/agent_profile.py +71 -5
- klaude_code/core/executor.py +75 -0
- klaude_code/core/manager/llm_clients_builder.py +18 -12
- klaude_code/core/prompts/prompt-nano-banana.md +1 -0
- klaude_code/core/tool/shell/command_safety.py +4 -189
- klaude_code/core/tool/sub_agent_tool.py +2 -1
- klaude_code/core/turn.py +1 -1
- klaude_code/llm/anthropic/client.py +8 -5
- klaude_code/llm/anthropic/input.py +54 -29
- klaude_code/llm/google/client.py +2 -2
- klaude_code/llm/google/input.py +23 -2
- klaude_code/llm/openai_compatible/input.py +22 -13
- klaude_code/llm/openai_compatible/stream.py +1 -1
- klaude_code/llm/openrouter/input.py +37 -25
- klaude_code/llm/responses/client.py +1 -1
- klaude_code/llm/responses/input.py +96 -57
- klaude_code/protocol/commands.py +1 -2
- klaude_code/protocol/events/system.py +4 -0
- klaude_code/protocol/message.py +2 -2
- klaude_code/protocol/op.py +17 -0
- klaude_code/protocol/op_handler.py +5 -0
- klaude_code/protocol/sub_agent/AGENTS.md +28 -0
- klaude_code/protocol/sub_agent/__init__.py +10 -14
- klaude_code/protocol/sub_agent/image_gen.py +2 -1
- klaude_code/session/codec.py +2 -6
- klaude_code/session/session.py +9 -1
- klaude_code/skill/assets/create-plan/SKILL.md +3 -5
- klaude_code/tui/command/__init__.py +7 -10
- klaude_code/tui/command/clear_cmd.py +1 -1
- klaude_code/tui/command/command_abc.py +1 -2
- klaude_code/tui/command/copy_cmd.py +1 -2
- klaude_code/tui/command/fork_session_cmd.py +4 -4
- klaude_code/tui/command/model_cmd.py +6 -43
- klaude_code/tui/command/model_select.py +75 -15
- klaude_code/tui/command/refresh_cmd.py +1 -2
- klaude_code/tui/command/resume_cmd.py +3 -4
- klaude_code/tui/command/status_cmd.py +1 -1
- klaude_code/tui/command/sub_agent_model_cmd.py +190 -0
- klaude_code/tui/components/bash_syntax.py +1 -1
- klaude_code/tui/components/common.py +1 -1
- klaude_code/tui/components/developer.py +10 -15
- klaude_code/tui/components/metadata.py +2 -64
- klaude_code/tui/components/rich/cjk_wrap.py +3 -2
- klaude_code/tui/components/rich/status.py +49 -3
- klaude_code/tui/components/rich/theme.py +4 -2
- klaude_code/tui/components/sub_agent.py +25 -46
- klaude_code/tui/components/user_input.py +9 -21
- klaude_code/tui/components/welcome.py +99 -0
- klaude_code/tui/input/prompt_toolkit.py +14 -1
- klaude_code/tui/renderer.py +2 -3
- klaude_code/tui/runner.py +2 -2
- klaude_code/tui/terminal/selector.py +8 -18
- klaude_code/ui/__init__.py +0 -24
- klaude_code/ui/common.py +3 -2
- klaude_code/ui/core/display.py +2 -2
- {klaude_code-2.1.1.dist-info → klaude_code-2.3.0.dist-info}/METADATA +16 -81
- {klaude_code-2.1.1.dist-info → klaude_code-2.3.0.dist-info}/RECORD +68 -67
- klaude_code/tui/command/help_cmd.py +0 -51
- klaude_code/tui/command/prompt-commit.md +0 -82
- klaude_code/tui/command/release_notes_cmd.py +0 -85
- klaude_code/ui/exec_mode.py +0 -60
- {klaude_code-2.1.1.dist-info → klaude_code-2.3.0.dist-info}/WHEEL +0 -0
- {klaude_code-2.1.1.dist-info → klaude_code-2.3.0.dist-info}/entry_points.txt +0 -0
klaude_code/llm/google/client.py
CHANGED
|
@@ -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
|
|
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.
|
|
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:
|
klaude_code/llm/google/input.py
CHANGED
|
@@ -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=
|
|
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=
|
|
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 [
|
|
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] =
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
"
|
|
76
|
-
"
|
|
77
|
-
|
|
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.
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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 (
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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(
|
|
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.
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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(
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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(
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
]
|
klaude_code/protocol/commands.py
CHANGED
|
@@ -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):
|
klaude_code/protocol/message.py
CHANGED
|
@@ -25,7 +25,7 @@ from klaude_code.protocol.model import (
|
|
|
25
25
|
# Stream items
|
|
26
26
|
|
|
27
27
|
|
|
28
|
-
class
|
|
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 |
|
|
178
|
+
StreamItem = AssistantTextDelta | AssistantImageDelta | ThinkingTextDelta | ToolCallStartDelta
|
|
179
179
|
|
|
180
180
|
LLMStreamItem = HistoryEvent | StreamItem
|
|
181
181
|
|
klaude_code/protocol/op.py
CHANGED
|
@@ -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)
|