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.
- klaude_code/app/runtime.py +2 -15
- klaude_code/cli/list_model.py +30 -13
- klaude_code/cli/main.py +26 -10
- klaude_code/config/assets/builtin_config.yaml +177 -310
- klaude_code/config/config.py +158 -21
- klaude_code/config/{select_model.py → model_matcher.py} +41 -16
- klaude_code/config/sub_agent_model_helper.py +217 -0
- klaude_code/config/thinking.py +2 -2
- klaude_code/const.py +1 -1
- klaude_code/core/agent_profile.py +43 -5
- klaude_code/core/executor.py +129 -47
- klaude_code/core/manager/llm_clients_builder.py +17 -11
- klaude_code/core/prompts/prompt-nano-banana.md +1 -1
- klaude_code/core/tool/file/diff_builder.py +25 -18
- klaude_code/core/tool/sub_agent_tool.py +2 -1
- klaude_code/llm/anthropic/client.py +12 -9
- klaude_code/llm/anthropic/input.py +54 -29
- klaude_code/llm/client.py +1 -1
- klaude_code/llm/codex/client.py +2 -2
- klaude_code/llm/google/client.py +7 -7
- klaude_code/llm/google/input.py +23 -2
- klaude_code/llm/input_common.py +2 -2
- klaude_code/llm/openai_compatible/client.py +3 -3
- klaude_code/llm/openai_compatible/input.py +22 -13
- klaude_code/llm/openai_compatible/stream.py +1 -1
- klaude_code/llm/openrouter/client.py +4 -4
- klaude_code/llm/openrouter/input.py +35 -25
- klaude_code/llm/responses/client.py +5 -5
- klaude_code/llm/responses/input.py +96 -57
- klaude_code/protocol/commands.py +1 -2
- klaude_code/protocol/events/__init__.py +7 -1
- klaude_code/protocol/events/chat.py +10 -0
- klaude_code/protocol/events/system.py +4 -0
- klaude_code/protocol/llm_param.py +1 -1
- klaude_code/protocol/model.py +0 -26
- klaude_code/protocol/op.py +17 -5
- 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 +13 -3
- klaude_code/skill/assets/create-plan/SKILL.md +3 -5
- klaude_code/tui/command/__init__.py +3 -6
- klaude_code/tui/command/clear_cmd.py +0 -1
- klaude_code/tui/command/command_abc.py +6 -4
- klaude_code/tui/command/copy_cmd.py +10 -10
- klaude_code/tui/command/debug_cmd.py +11 -10
- klaude_code/tui/command/export_online_cmd.py +18 -23
- klaude_code/tui/command/fork_session_cmd.py +39 -43
- klaude_code/tui/command/model_cmd.py +10 -49
- klaude_code/tui/command/model_picker.py +142 -0
- klaude_code/tui/command/refresh_cmd.py +0 -1
- klaude_code/tui/command/registry.py +15 -21
- klaude_code/tui/command/resume_cmd.py +10 -16
- klaude_code/tui/command/status_cmd.py +8 -12
- klaude_code/tui/command/sub_agent_model_cmd.py +185 -0
- klaude_code/tui/command/terminal_setup_cmd.py +8 -11
- klaude_code/tui/command/thinking_cmd.py +4 -6
- klaude_code/tui/commands.py +5 -0
- klaude_code/tui/components/bash_syntax.py +1 -1
- klaude_code/tui/components/command_output.py +96 -0
- klaude_code/tui/components/common.py +1 -1
- klaude_code/tui/components/developer.py +3 -115
- klaude_code/tui/components/metadata.py +1 -63
- 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 +2 -0
- klaude_code/tui/components/sub_agent.py +25 -46
- klaude_code/tui/components/welcome.py +99 -0
- klaude_code/tui/input/prompt_toolkit.py +19 -8
- klaude_code/tui/machine.py +5 -0
- klaude_code/tui/renderer.py +7 -8
- klaude_code/tui/runner.py +0 -6
- klaude_code/tui/terminal/selector.py +8 -6
- {klaude_code-2.2.0.dist-info → klaude_code-2.4.0.dist-info}/METADATA +21 -74
- {klaude_code-2.2.0.dist-info → klaude_code-2.4.0.dist-info}/RECORD +79 -76
- klaude_code/tui/command/help_cmd.py +0 -51
- klaude_code/tui/command/model_select.py +0 -84
- klaude_code/tui/command/release_notes_cmd.py +0 -85
- {klaude_code-2.2.0.dist-info → klaude_code-2.4.0.dist-info}/WHEEL +0 -0
- {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(
|
|
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,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
|
|
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
|
-
|
|
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
|
klaude_code/protocol/model.py
CHANGED
|
@@ -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
|
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"
|
|
@@ -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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
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.
|
|
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
|
|
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
|
)
|
klaude_code/session/codec.py
CHANGED
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
-
from typing import Any,
|
|
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
|
|
24
|
+
if not isinstance(tp, type) or not issubclass(tp, BaseModel):
|
|
29
25
|
continue
|
|
30
26
|
registry[tp.__name__] = tp
|
|
31
27
|
return registry
|
klaude_code/session/session.py
CHANGED
|
@@ -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
|
|
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]
|
|
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. **
|
|
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
|
|