klaude-code 2.8.0__py3-none-any.whl → 2.9.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- klaude_code/app/runtime.py +2 -1
- klaude_code/auth/antigravity/oauth.py +0 -9
- klaude_code/auth/antigravity/token_manager.py +0 -18
- klaude_code/auth/base.py +53 -0
- klaude_code/auth/codex/exceptions.py +0 -4
- klaude_code/auth/codex/oauth.py +32 -28
- klaude_code/auth/codex/token_manager.py +0 -18
- klaude_code/cli/cost_cmd.py +128 -39
- klaude_code/cli/list_model.py +27 -10
- klaude_code/cli/main.py +15 -4
- klaude_code/config/assets/builtin_config.yaml +8 -24
- klaude_code/config/config.py +47 -25
- klaude_code/config/sub_agent_model_helper.py +18 -13
- klaude_code/config/thinking.py +0 -8
- klaude_code/const.py +2 -2
- klaude_code/core/agent_profile.py +11 -53
- klaude_code/core/compaction/compaction.py +4 -6
- klaude_code/core/compaction/overflow.py +0 -4
- klaude_code/core/executor.py +51 -5
- klaude_code/core/manager/llm_clients.py +9 -1
- klaude_code/core/prompts/prompt-claude-code.md +4 -4
- klaude_code/core/reminders.py +21 -23
- klaude_code/core/task.py +0 -4
- klaude_code/core/tool/__init__.py +3 -2
- klaude_code/core/tool/file/apply_patch.py +0 -27
- klaude_code/core/tool/file/edit_tool.py +1 -2
- klaude_code/core/tool/file/read_tool.md +3 -2
- klaude_code/core/tool/file/read_tool.py +15 -2
- klaude_code/core/tool/offload.py +0 -35
- klaude_code/core/tool/sub_agent/__init__.py +6 -0
- klaude_code/core/tool/sub_agent/image_gen.md +16 -0
- klaude_code/core/tool/sub_agent/image_gen.py +146 -0
- klaude_code/core/tool/sub_agent/task.md +20 -0
- klaude_code/core/tool/sub_agent/task.py +205 -0
- klaude_code/core/tool/tool_registry.py +0 -16
- klaude_code/core/turn.py +1 -1
- klaude_code/llm/anthropic/input.py +6 -5
- klaude_code/llm/antigravity/input.py +14 -7
- klaude_code/llm/codex/client.py +22 -0
- klaude_code/llm/codex/prompt_sync.py +237 -0
- klaude_code/llm/google/client.py +8 -6
- klaude_code/llm/google/input.py +20 -12
- klaude_code/llm/image.py +18 -11
- klaude_code/llm/input_common.py +14 -6
- klaude_code/llm/json_stable.py +37 -0
- klaude_code/llm/openai_compatible/input.py +0 -10
- klaude_code/llm/openai_compatible/stream.py +16 -1
- klaude_code/llm/registry.py +0 -5
- klaude_code/llm/responses/input.py +15 -5
- klaude_code/llm/usage.py +0 -8
- klaude_code/protocol/commands.py +1 -0
- klaude_code/protocol/events.py +2 -1
- klaude_code/protocol/message.py +2 -2
- klaude_code/protocol/model.py +20 -1
- klaude_code/protocol/op.py +27 -0
- klaude_code/protocol/op_handler.py +10 -0
- klaude_code/protocol/sub_agent/AGENTS.md +5 -5
- klaude_code/protocol/sub_agent/__init__.py +13 -34
- klaude_code/protocol/sub_agent/explore.py +7 -34
- klaude_code/protocol/sub_agent/image_gen.py +3 -74
- klaude_code/protocol/sub_agent/task.py +3 -47
- klaude_code/protocol/sub_agent/web.py +8 -52
- klaude_code/protocol/tools.py +2 -0
- klaude_code/session/export.py +308 -299
- klaude_code/session/session.py +58 -21
- klaude_code/session/store.py +0 -4
- klaude_code/session/templates/export_session.html +430 -134
- klaude_code/skill/assets/deslop/SKILL.md +9 -0
- klaude_code/skill/system_skills.py +0 -20
- klaude_code/tui/command/__init__.py +3 -0
- klaude_code/tui/command/continue_cmd.py +34 -0
- klaude_code/tui/command/fork_session_cmd.py +5 -2
- klaude_code/tui/command/resume_cmd.py +9 -2
- klaude_code/tui/command/sub_agent_model_cmd.py +85 -18
- klaude_code/tui/components/assistant.py +0 -26
- klaude_code/tui/components/command_output.py +3 -1
- klaude_code/tui/components/developer.py +3 -0
- klaude_code/tui/components/diffs.py +2 -208
- klaude_code/tui/components/errors.py +4 -0
- klaude_code/tui/components/mermaid_viewer.py +2 -2
- klaude_code/tui/components/rich/markdown.py +60 -63
- klaude_code/tui/components/rich/theme.py +2 -0
- klaude_code/tui/components/sub_agent.py +2 -46
- klaude_code/tui/components/thinking.py +0 -33
- klaude_code/tui/components/tools.py +43 -21
- klaude_code/tui/input/images.py +21 -18
- klaude_code/tui/input/key_bindings.py +2 -2
- klaude_code/tui/input/prompt_toolkit.py +49 -49
- klaude_code/tui/machine.py +15 -11
- klaude_code/tui/renderer.py +12 -20
- klaude_code/tui/runner.py +2 -1
- klaude_code/tui/terminal/image.py +6 -34
- klaude_code/ui/common.py +0 -70
- {klaude_code-2.8.0.dist-info → klaude_code-2.9.0.dist-info}/METADATA +3 -6
- {klaude_code-2.8.0.dist-info → klaude_code-2.9.0.dist-info}/RECORD +97 -92
- klaude_code/core/tool/sub_agent_tool.py +0 -126
- klaude_code/llm/openai_compatible/tool_call_accumulator.py +0 -108
- klaude_code/tui/components/rich/searchable_text.py +0 -68
- {klaude_code-2.8.0.dist-info → klaude_code-2.9.0.dist-info}/WHEEL +0 -0
- {klaude_code-2.8.0.dist-info → klaude_code-2.9.0.dist-info}/entry_points.txt +0 -0
klaude_code/llm/google/input.py
CHANGED
|
@@ -6,18 +6,20 @@
|
|
|
6
6
|
import json
|
|
7
7
|
from base64 import b64decode
|
|
8
8
|
from binascii import Error as BinasciiError
|
|
9
|
-
from typing import Any
|
|
9
|
+
from typing import Any, cast
|
|
10
10
|
|
|
11
11
|
from google.genai import types
|
|
12
12
|
|
|
13
13
|
from klaude_code.const import EMPTY_TOOL_OUTPUT_MESSAGE
|
|
14
|
-
from klaude_code.llm.image import assistant_image_to_data_url, parse_data_url
|
|
14
|
+
from klaude_code.llm.image import assistant_image_to_data_url, image_file_to_data_url, parse_data_url
|
|
15
15
|
from klaude_code.llm.input_common import (
|
|
16
16
|
DeveloperAttachment,
|
|
17
|
+
ImagePart,
|
|
17
18
|
attach_developer_messages,
|
|
18
19
|
merge_reminder_text,
|
|
19
20
|
split_thinking_parts,
|
|
20
21
|
)
|
|
22
|
+
from klaude_code.llm.json_stable import canonicalize_json
|
|
21
23
|
from klaude_code.protocol import llm_param, message
|
|
22
24
|
|
|
23
25
|
|
|
@@ -26,16 +28,16 @@ def _data_url_to_blob(url: str) -> types.Blob:
|
|
|
26
28
|
return types.Blob(data=decoded, mime_type=media_type)
|
|
27
29
|
|
|
28
30
|
|
|
29
|
-
def _image_part_to_part(image:
|
|
30
|
-
url = image.url
|
|
31
|
+
def _image_part_to_part(image: ImagePart) -> types.Part:
|
|
32
|
+
url = image_file_to_data_url(image) if isinstance(image, message.ImageFilePart) else image.url
|
|
31
33
|
if url.startswith("data:"):
|
|
32
34
|
return types.Part(inline_data=_data_url_to_blob(url))
|
|
33
35
|
# Best-effort: Gemini supports file URIs, and may accept public HTTPS URLs.
|
|
34
36
|
return types.Part(file_data=types.FileData(file_uri=url))
|
|
35
37
|
|
|
36
38
|
|
|
37
|
-
def _image_part_to_function_response_part(image:
|
|
38
|
-
url = image.url
|
|
39
|
+
def _image_part_to_function_response_part(image: ImagePart) -> types.FunctionResponsePart:
|
|
40
|
+
url = image_file_to_data_url(image) if isinstance(image, message.ImageFilePart) else image.url
|
|
39
41
|
if url.startswith("data:"):
|
|
40
42
|
media_type, _, decoded = parse_data_url(url)
|
|
41
43
|
return types.FunctionResponsePart.from_bytes(data=decoded, mime_type=media_type)
|
|
@@ -47,7 +49,7 @@ def _user_message_to_content(msg: message.UserMessage, attachment: DeveloperAtta
|
|
|
47
49
|
for part in msg.parts:
|
|
48
50
|
if isinstance(part, message.TextPart):
|
|
49
51
|
parts.append(types.Part(text=part.text))
|
|
50
|
-
elif isinstance(part, message.ImageURLPart):
|
|
52
|
+
elif isinstance(part, (message.ImageURLPart, message.ImageFilePart)):
|
|
51
53
|
parts.append(_image_part_to_part(part))
|
|
52
54
|
if attachment.text:
|
|
53
55
|
parts.append(types.Part(text=attachment.text))
|
|
@@ -73,7 +75,10 @@ def _tool_messages_to_contents(
|
|
|
73
75
|
)
|
|
74
76
|
has_text = merged_text.strip() != ""
|
|
75
77
|
|
|
76
|
-
images
|
|
78
|
+
images: list[ImagePart] = [
|
|
79
|
+
part for part in msg.parts if isinstance(part, (message.ImageURLPart, message.ImageFilePart))
|
|
80
|
+
]
|
|
81
|
+
images.extend(attachment.images)
|
|
77
82
|
image_parts: list[types.Part] = []
|
|
78
83
|
function_response_parts: list[types.FunctionResponsePart] = []
|
|
79
84
|
|
|
@@ -155,11 +160,14 @@ def _assistant_message_to_content(msg: message.AssistantMessage, model_name: str
|
|
|
155
160
|
args: dict[str, Any]
|
|
156
161
|
if part.arguments_json:
|
|
157
162
|
try:
|
|
158
|
-
|
|
163
|
+
loaded: object = json.loads(part.arguments_json)
|
|
159
164
|
except json.JSONDecodeError:
|
|
160
|
-
|
|
165
|
+
loaded = {"_raw": part.arguments_json}
|
|
161
166
|
else:
|
|
162
|
-
|
|
167
|
+
loaded = {}
|
|
168
|
+
|
|
169
|
+
canonical = canonicalize_json(loaded)
|
|
170
|
+
args = cast(dict[str, Any], canonical) if isinstance(canonical, dict) else {"_value": canonical}
|
|
163
171
|
parts.append(
|
|
164
172
|
types.Part(
|
|
165
173
|
function_call=types.FunctionCall(id=part.call_id, name=part.tool_name, args=args),
|
|
@@ -223,7 +231,7 @@ def convert_tool_schema(tools: list[llm_param.ToolSchema] | None) -> list[types.
|
|
|
223
231
|
types.FunctionDeclaration(
|
|
224
232
|
name=tool.name,
|
|
225
233
|
description=tool.description,
|
|
226
|
-
parameters_json_schema=tool.parameters,
|
|
234
|
+
parameters_json_schema=canonicalize_json(tool.parameters),
|
|
227
235
|
)
|
|
228
236
|
for tool in tools
|
|
229
237
|
]
|
klaude_code/llm/image.py
CHANGED
|
@@ -99,21 +99,12 @@ def save_assistant_image(
|
|
|
99
99
|
)
|
|
100
100
|
|
|
101
101
|
|
|
102
|
-
def
|
|
103
|
-
"""Load an
|
|
104
|
-
|
|
105
|
-
This is primarily used for multi-turn image editing, where providers require
|
|
106
|
-
sending the previous assistant message (including images) back to the model.
|
|
107
|
-
"""
|
|
102
|
+
def image_file_to_data_url(image: message.ImageFilePart) -> str:
|
|
103
|
+
"""Load an image file from disk and encode it as a base64 data URL."""
|
|
108
104
|
|
|
109
105
|
file_path = Path(image.file_path)
|
|
110
106
|
decoded = file_path.read_bytes()
|
|
111
107
|
|
|
112
|
-
if len(decoded) > IMAGE_OUTPUT_MAX_BYTES:
|
|
113
|
-
decoded_mb = len(decoded) / (1024 * 1024)
|
|
114
|
-
limit_mb = IMAGE_OUTPUT_MAX_BYTES / (1024 * 1024)
|
|
115
|
-
raise ValueError(f"Assistant image size ({decoded_mb:.2f}MB) exceeds limit ({limit_mb:.2f}MB)")
|
|
116
|
-
|
|
117
108
|
mime_type = image.mime_type
|
|
118
109
|
if not mime_type:
|
|
119
110
|
guessed, _ = mimetypes.guess_type(str(file_path))
|
|
@@ -121,3 +112,19 @@ def assistant_image_to_data_url(image: message.ImageFilePart) -> str:
|
|
|
121
112
|
|
|
122
113
|
encoded = b64encode(decoded).decode("ascii")
|
|
123
114
|
return f"data:{mime_type};base64,{encoded}"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def assistant_image_to_data_url(image: message.ImageFilePart) -> str:
|
|
118
|
+
"""Load an assistant image from disk and encode it as a base64 data URL.
|
|
119
|
+
|
|
120
|
+
This is primarily used for multi-turn image editing, where providers require
|
|
121
|
+
sending the previous assistant message (including images) back to the model.
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
file_path = Path(image.file_path)
|
|
125
|
+
if file_path.stat().st_size > IMAGE_OUTPUT_MAX_BYTES:
|
|
126
|
+
size_mb = file_path.stat().st_size / (1024 * 1024)
|
|
127
|
+
limit_mb = IMAGE_OUTPUT_MAX_BYTES / (1024 * 1024)
|
|
128
|
+
raise ValueError(f"Assistant image size ({size_mb:.2f}MB) exceeds limit ({limit_mb:.2f}MB)")
|
|
129
|
+
|
|
130
|
+
return image_file_to_data_url(image)
|
klaude_code/llm/input_common.py
CHANGED
|
@@ -8,26 +8,29 @@ if TYPE_CHECKING:
|
|
|
8
8
|
from klaude_code.protocol.llm_param import LLMCallParameter, LLMConfigParameter
|
|
9
9
|
|
|
10
10
|
from klaude_code.const import EMPTY_TOOL_OUTPUT_MESSAGE
|
|
11
|
+
from klaude_code.llm.image import image_file_to_data_url
|
|
11
12
|
from klaude_code.protocol import message
|
|
12
13
|
|
|
14
|
+
ImagePart = message.ImageURLPart | message.ImageFilePart
|
|
13
15
|
|
|
14
|
-
|
|
16
|
+
|
|
17
|
+
def _empty_image_parts() -> list[ImagePart]:
|
|
15
18
|
return []
|
|
16
19
|
|
|
17
20
|
|
|
18
21
|
@dataclass
|
|
19
22
|
class DeveloperAttachment:
|
|
20
23
|
text: str = ""
|
|
21
|
-
images: list[
|
|
24
|
+
images: list[ImagePart] = field(default_factory=_empty_image_parts)
|
|
22
25
|
|
|
23
26
|
|
|
24
|
-
def _extract_developer_content(msg: message.DeveloperMessage) -> tuple[str, list[
|
|
27
|
+
def _extract_developer_content(msg: message.DeveloperMessage) -> tuple[str, list[ImagePart]]:
|
|
25
28
|
text_parts: list[str] = []
|
|
26
|
-
images: list[
|
|
29
|
+
images: list[ImagePart] = []
|
|
27
30
|
for part in msg.parts:
|
|
28
31
|
if isinstance(part, message.TextPart):
|
|
29
32
|
text_parts.append(part.text + "\n")
|
|
30
|
-
elif isinstance(part, message.ImageURLPart):
|
|
33
|
+
elif isinstance(part, (message.ImageURLPart, message.ImageFilePart)):
|
|
31
34
|
images.append(part)
|
|
32
35
|
return "".join(text_parts), images
|
|
33
36
|
|
|
@@ -87,10 +90,15 @@ def build_chat_content_parts(
|
|
|
87
90
|
parts.append({"type": "text", "text": part.text})
|
|
88
91
|
elif isinstance(part, message.ImageURLPart):
|
|
89
92
|
parts.append({"type": "image_url", "image_url": {"url": part.url}})
|
|
93
|
+
elif isinstance(part, message.ImageFilePart):
|
|
94
|
+
parts.append({"type": "image_url", "image_url": {"url": image_file_to_data_url(part)}})
|
|
90
95
|
if attachment.text:
|
|
91
96
|
parts.append({"type": "text", "text": attachment.text})
|
|
92
97
|
for image in attachment.images:
|
|
93
|
-
|
|
98
|
+
if isinstance(image, message.ImageFilePart):
|
|
99
|
+
parts.append({"type": "image_url", "image_url": {"url": image_file_to_data_url(image)}})
|
|
100
|
+
else:
|
|
101
|
+
parts.append({"type": "image_url", "image_url": {"url": image.url}})
|
|
94
102
|
if not parts:
|
|
95
103
|
parts.append({"type": "text", "text": ""})
|
|
96
104
|
return parts
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from collections.abc import Mapping
|
|
5
|
+
from typing import cast
|
|
6
|
+
|
|
7
|
+
type JsonValue = str | int | float | bool | None | list["JsonValue"] | dict[str, "JsonValue"]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def canonicalize_json(value: object) -> JsonValue:
|
|
11
|
+
"""Return a JSON-equivalent value with stable dict key ordering.
|
|
12
|
+
|
|
13
|
+
This is used to make provider payload serialization stable across runs so that
|
|
14
|
+
prefix caching has a better chance to hit.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
if isinstance(value, Mapping):
|
|
18
|
+
items: list[tuple[str, JsonValue]] = []
|
|
19
|
+
for key, item_value in cast(Mapping[object, object], value).items():
|
|
20
|
+
items.append((str(key), canonicalize_json(item_value)))
|
|
21
|
+
items.sort(key=lambda kv: kv[0])
|
|
22
|
+
return {k: v for k, v in items}
|
|
23
|
+
|
|
24
|
+
if isinstance(value, list):
|
|
25
|
+
return [canonicalize_json(v) for v in cast(list[object], value)]
|
|
26
|
+
|
|
27
|
+
if isinstance(value, tuple):
|
|
28
|
+
return [canonicalize_json(v) for v in cast(tuple[object, ...], value)]
|
|
29
|
+
|
|
30
|
+
return cast(JsonValue, value)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def dumps_canonical_json(value: object) -> str:
|
|
34
|
+
"""Dump JSON with stable key order and no insignificant whitespace."""
|
|
35
|
+
|
|
36
|
+
canonical = canonicalize_json(value)
|
|
37
|
+
return json.dumps(canonical, ensure_ascii=False, separators=(",", ":"), sort_keys=False)
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
from typing import cast
|
|
7
7
|
|
|
8
8
|
from openai.types import chat
|
|
9
|
-
from openai.types.chat import ChatCompletionContentPartParam
|
|
10
9
|
|
|
11
10
|
from klaude_code.llm.image import assistant_image_to_data_url
|
|
12
11
|
from klaude_code.llm.input_common import (
|
|
@@ -30,15 +29,6 @@ def _assistant_message_to_openai(msg: message.AssistantMessage) -> chat.ChatComp
|
|
|
30
29
|
return cast(chat.ChatCompletionMessageParam, assistant_message)
|
|
31
30
|
|
|
32
31
|
|
|
33
|
-
def build_user_content_parts(
|
|
34
|
-
images: list[message.ImageURLPart],
|
|
35
|
-
) -> list[ChatCompletionContentPartParam]:
|
|
36
|
-
"""Build content parts for images only. Used by OpenRouter."""
|
|
37
|
-
return [
|
|
38
|
-
cast(ChatCompletionContentPartParam, {"type": "image_url", "image_url": {"url": image.url}}) for image in images
|
|
39
|
-
]
|
|
40
|
-
|
|
41
|
-
|
|
42
32
|
def convert_history_to_input(
|
|
43
33
|
history: list[message.Message],
|
|
44
34
|
system: str | None = None,
|
|
@@ -12,6 +12,7 @@ how reasoning is represented (``reasoning_details`` vs ``reasoning_content``).
|
|
|
12
12
|
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
|
|
15
|
+
import re
|
|
15
16
|
from abc import ABC, abstractmethod
|
|
16
17
|
from collections.abc import AsyncGenerator, Callable
|
|
17
18
|
from dataclasses import dataclass
|
|
@@ -26,7 +27,6 @@ from openai.types.chat.chat_completion_chunk import ChatCompletionChunk
|
|
|
26
27
|
|
|
27
28
|
from klaude_code.llm.client import LLMStreamABC
|
|
28
29
|
from klaude_code.llm.image import save_assistant_image
|
|
29
|
-
from klaude_code.llm.openai_compatible.tool_call_accumulator import normalize_tool_name
|
|
30
30
|
from klaude_code.llm.stream_parts import (
|
|
31
31
|
append_text_part,
|
|
32
32
|
append_thinking_text_part,
|
|
@@ -34,9 +34,24 @@ from klaude_code.llm.stream_parts import (
|
|
|
34
34
|
build_partial_parts,
|
|
35
35
|
)
|
|
36
36
|
from klaude_code.llm.usage import MetadataTracker, convert_usage
|
|
37
|
+
from klaude_code.log import log_debug
|
|
37
38
|
from klaude_code.protocol import llm_param, message, model
|
|
38
39
|
|
|
39
40
|
|
|
41
|
+
def normalize_tool_name(name: str) -> str:
|
|
42
|
+
"""Normalize tool name from Gemini-3 format.
|
|
43
|
+
|
|
44
|
+
Gemini-3 sometimes returns tool names in format like 'tool_Edit_mUoY2p3W3r3z8uO5P2nZ'.
|
|
45
|
+
This function extracts the actual tool name (e.g., 'Edit').
|
|
46
|
+
"""
|
|
47
|
+
match = re.match(r"^tool_([A-Za-z]+)_[A-Za-z0-9]+$", name)
|
|
48
|
+
if match:
|
|
49
|
+
normalized = match.group(1)
|
|
50
|
+
log_debug(f"Gemini-3 tool name normalized: {name} -> {normalized}", style="yellow")
|
|
51
|
+
return normalized
|
|
52
|
+
return name
|
|
53
|
+
|
|
54
|
+
|
|
40
55
|
class StreamStateManager:
|
|
41
56
|
"""Manages streaming state and accumulates parts in stream order.
|
|
42
57
|
|
klaude_code/llm/registry.py
CHANGED
|
@@ -38,11 +38,6 @@ def _load_protocol(protocol: llm_param.LLMClientProtocol) -> None:
|
|
|
38
38
|
_loaded_protocols.add(protocol)
|
|
39
39
|
|
|
40
40
|
|
|
41
|
-
def load_protocol(protocol: llm_param.LLMClientProtocol) -> None:
|
|
42
|
-
"""Load the module for a specific protocol on demand."""
|
|
43
|
-
_load_protocol(protocol)
|
|
44
|
-
|
|
45
|
-
|
|
46
41
|
def register(name: llm_param.LLMClientProtocol) -> Callable[[_T], _T]:
|
|
47
42
|
"""Decorator to register an LLM client class for a protocol."""
|
|
48
43
|
|
|
@@ -7,6 +7,7 @@ from typing import Any, cast
|
|
|
7
7
|
from openai.types import responses
|
|
8
8
|
|
|
9
9
|
from klaude_code.const import EMPTY_TOOL_OUTPUT_MESSAGE
|
|
10
|
+
from klaude_code.llm.image import image_file_to_data_url
|
|
10
11
|
from klaude_code.llm.input_common import (
|
|
11
12
|
DeveloperAttachment,
|
|
12
13
|
attach_developer_messages,
|
|
@@ -16,6 +17,12 @@ from klaude_code.llm.input_common import (
|
|
|
16
17
|
from klaude_code.protocol import llm_param, message
|
|
17
18
|
|
|
18
19
|
|
|
20
|
+
def _image_to_url(image: message.ImageURLPart | message.ImageFilePart) -> str:
|
|
21
|
+
if isinstance(image, message.ImageFilePart):
|
|
22
|
+
return image_file_to_data_url(image)
|
|
23
|
+
return image.url
|
|
24
|
+
|
|
25
|
+
|
|
19
26
|
def _build_user_content_parts(
|
|
20
27
|
user: message.UserMessage,
|
|
21
28
|
attachment: DeveloperAttachment,
|
|
@@ -24,11 +31,11 @@ def _build_user_content_parts(
|
|
|
24
31
|
for part in user.parts:
|
|
25
32
|
if isinstance(part, message.TextPart):
|
|
26
33
|
parts.append(cast(responses.ResponseInputContentParam, {"type": "input_text", "text": part.text}))
|
|
27
|
-
elif isinstance(part, message.ImageURLPart):
|
|
34
|
+
elif isinstance(part, (message.ImageURLPart, message.ImageFilePart)):
|
|
28
35
|
parts.append(
|
|
29
36
|
cast(
|
|
30
37
|
responses.ResponseInputContentParam,
|
|
31
|
-
{"type": "input_image", "detail": "auto", "image_url": part
|
|
38
|
+
{"type": "input_image", "detail": "auto", "image_url": _image_to_url(part)},
|
|
32
39
|
)
|
|
33
40
|
)
|
|
34
41
|
if attachment.text:
|
|
@@ -37,7 +44,7 @@ def _build_user_content_parts(
|
|
|
37
44
|
parts.append(
|
|
38
45
|
cast(
|
|
39
46
|
responses.ResponseInputContentParam,
|
|
40
|
-
{"type": "input_image", "detail": "auto", "image_url": image
|
|
47
|
+
{"type": "input_image", "detail": "auto", "image_url": _image_to_url(image)},
|
|
41
48
|
)
|
|
42
49
|
)
|
|
43
50
|
if not parts:
|
|
@@ -56,12 +63,15 @@ def _build_tool_result_item(
|
|
|
56
63
|
)
|
|
57
64
|
if text_output:
|
|
58
65
|
content_parts.append(cast(responses.ResponseInputContentParam, {"type": "input_text", "text": text_output}))
|
|
59
|
-
images
|
|
66
|
+
images: list[message.ImageURLPart | message.ImageFilePart] = [
|
|
67
|
+
part for part in tool.parts if isinstance(part, (message.ImageURLPart, message.ImageFilePart))
|
|
68
|
+
]
|
|
69
|
+
images.extend(attachment.images)
|
|
60
70
|
for image in images:
|
|
61
71
|
content_parts.append(
|
|
62
72
|
cast(
|
|
63
73
|
responses.ResponseInputContentParam,
|
|
64
|
-
{"type": "input_image", "detail": "auto", "image_url": image
|
|
74
|
+
{"type": "input_image", "detail": "auto", "image_url": _image_to_url(image)},
|
|
65
75
|
)
|
|
66
76
|
)
|
|
67
77
|
|
klaude_code/llm/usage.py
CHANGED
|
@@ -44,14 +44,6 @@ class MetadataTracker:
|
|
|
44
44
|
self._usage = model.Usage()
|
|
45
45
|
self._cost_config = cost_config
|
|
46
46
|
|
|
47
|
-
@property
|
|
48
|
-
def first_token_time(self) -> float | None:
|
|
49
|
-
return self._first_token_time
|
|
50
|
-
|
|
51
|
-
@property
|
|
52
|
-
def last_token_time(self) -> float | None:
|
|
53
|
-
return self._last_token_time
|
|
54
|
-
|
|
55
47
|
def record_token(self) -> None:
|
|
56
48
|
"""Record a token arrival, updating first/last token times."""
|
|
57
49
|
now = time.time()
|
klaude_code/protocol/commands.py
CHANGED
klaude_code/protocol/events.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import time
|
|
4
|
+
from collections.abc import Sequence
|
|
4
5
|
from typing import Literal
|
|
5
6
|
|
|
6
7
|
from pydantic import BaseModel, Field
|
|
@@ -58,7 +59,7 @@ class ResponseEvent(Event):
|
|
|
58
59
|
|
|
59
60
|
class UserMessageEvent(Event):
|
|
60
61
|
content: str
|
|
61
|
-
images:
|
|
62
|
+
images: Sequence[message.ImageURLPart | message.ImageFilePart] | None = None
|
|
62
63
|
|
|
63
64
|
|
|
64
65
|
class DeveloperMessageEvent(Event):
|
klaude_code/protocol/message.py
CHANGED
|
@@ -213,7 +213,7 @@ class UserInputPayload(BaseModel):
|
|
|
213
213
|
"""
|
|
214
214
|
|
|
215
215
|
text: str
|
|
216
|
-
images:
|
|
216
|
+
images: Sequence[ImageURLPart | ImageFilePart] | None = None
|
|
217
217
|
|
|
218
218
|
|
|
219
219
|
# Helper functions
|
|
@@ -225,7 +225,7 @@ def text_parts_from_str(text: str | None) -> list[Part]:
|
|
|
225
225
|
return [TextPart(text=text)]
|
|
226
226
|
|
|
227
227
|
|
|
228
|
-
def parts_from_text_and_images(text: str | None, images:
|
|
228
|
+
def parts_from_text_and_images(text: str | None, images: Sequence[ImageURLPart | ImageFilePart] | None) -> list[Part]:
|
|
229
229
|
parts: list[Part] = []
|
|
230
230
|
if text:
|
|
231
231
|
parts.append(TextPart(text=text))
|
klaude_code/protocol/model.py
CHANGED
|
@@ -217,6 +217,11 @@ class MermaidLinkUIExtra(BaseModel):
|
|
|
217
217
|
line_count: int
|
|
218
218
|
|
|
219
219
|
|
|
220
|
+
class ImageUIExtra(BaseModel):
|
|
221
|
+
type: Literal["image"] = "image"
|
|
222
|
+
file_path: str
|
|
223
|
+
|
|
224
|
+
|
|
220
225
|
class MarkdownDocUIExtra(BaseModel):
|
|
221
226
|
type: Literal["markdown_doc"] = "markdown_doc"
|
|
222
227
|
file_path: str
|
|
@@ -231,7 +236,13 @@ class SessionStatusUIExtra(BaseModel):
|
|
|
231
236
|
|
|
232
237
|
|
|
233
238
|
MultiUIExtraItem = (
|
|
234
|
-
DiffUIExtra
|
|
239
|
+
DiffUIExtra
|
|
240
|
+
| TodoListUIExtra
|
|
241
|
+
| SessionIdUIExtra
|
|
242
|
+
| MermaidLinkUIExtra
|
|
243
|
+
| ImageUIExtra
|
|
244
|
+
| MarkdownDocUIExtra
|
|
245
|
+
| SessionStatusUIExtra
|
|
235
246
|
)
|
|
236
247
|
|
|
237
248
|
|
|
@@ -251,6 +262,7 @@ ToolResultUIExtra = Annotated[
|
|
|
251
262
|
| TodoListUIExtra
|
|
252
263
|
| SessionIdUIExtra
|
|
253
264
|
| MermaidLinkUIExtra
|
|
265
|
+
| ImageUIExtra
|
|
254
266
|
| MarkdownDocUIExtra
|
|
255
267
|
| SessionStatusUIExtra
|
|
256
268
|
| MultiUIExtra,
|
|
@@ -292,6 +304,7 @@ class AtFileOpsUIItem(BaseModel):
|
|
|
292
304
|
class UserImagesUIItem(BaseModel):
|
|
293
305
|
type: Literal["user_images"] = "user_images"
|
|
294
306
|
count: int
|
|
307
|
+
paths: list[str] = []
|
|
295
308
|
|
|
296
309
|
|
|
297
310
|
class SkillActivatedUIItem(BaseModel):
|
|
@@ -299,6 +312,11 @@ class SkillActivatedUIItem(BaseModel):
|
|
|
299
312
|
name: str
|
|
300
313
|
|
|
301
314
|
|
|
315
|
+
class AtFileImagesUIItem(BaseModel):
|
|
316
|
+
type: Literal["at_file_images"] = "at_file_images"
|
|
317
|
+
paths: list[str]
|
|
318
|
+
|
|
319
|
+
|
|
302
320
|
type DeveloperUIItem = (
|
|
303
321
|
MemoryLoadedUIItem
|
|
304
322
|
| ExternalFileChangesUIItem
|
|
@@ -306,6 +324,7 @@ type DeveloperUIItem = (
|
|
|
306
324
|
| AtFileOpsUIItem
|
|
307
325
|
| UserImagesUIItem
|
|
308
326
|
| SkillActivatedUIItem
|
|
327
|
+
| AtFileImagesUIItem
|
|
309
328
|
)
|
|
310
329
|
|
|
311
330
|
|
klaude_code/protocol/op.py
CHANGED
|
@@ -24,8 +24,10 @@ class OperationType(Enum):
|
|
|
24
24
|
"""Enumeration of supported operation types."""
|
|
25
25
|
|
|
26
26
|
RUN_AGENT = "run_agent"
|
|
27
|
+
CONTINUE_AGENT = "continue_agent"
|
|
27
28
|
COMPACT_SESSION = "compact_session"
|
|
28
29
|
CHANGE_MODEL = "change_model"
|
|
30
|
+
CHANGE_COMPACT_MODEL = "change_compact_model"
|
|
29
31
|
CHANGE_SUB_AGENT_MODEL = "change_sub_agent_model"
|
|
30
32
|
CHANGE_THINKING = "change_thinking"
|
|
31
33
|
CLEAR_SESSION = "clear_session"
|
|
@@ -58,6 +60,19 @@ class RunAgentOperation(Operation):
|
|
|
58
60
|
await handler.handle_run_agent(self)
|
|
59
61
|
|
|
60
62
|
|
|
63
|
+
class ContinueAgentOperation(Operation):
|
|
64
|
+
"""Operation for continuing an agent task without adding a new user message.
|
|
65
|
+
|
|
66
|
+
Used for recovery after interruptions (network errors, API failures, etc.).
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
type: OperationType = OperationType.CONTINUE_AGENT
|
|
70
|
+
session_id: str
|
|
71
|
+
|
|
72
|
+
async def execute(self, handler: OperationHandler) -> None:
|
|
73
|
+
await handler.handle_continue_agent(self)
|
|
74
|
+
|
|
75
|
+
|
|
61
76
|
class CompactSessionOperation(Operation):
|
|
62
77
|
"""Operation for compacting a session's conversation history."""
|
|
63
78
|
|
|
@@ -94,6 +109,18 @@ class ChangeModelOperation(Operation):
|
|
|
94
109
|
await handler.handle_change_model(self)
|
|
95
110
|
|
|
96
111
|
|
|
112
|
+
class ChangeCompactModelOperation(Operation):
|
|
113
|
+
"""Operation for changing the compact model (used for session compaction)."""
|
|
114
|
+
|
|
115
|
+
type: OperationType = OperationType.CHANGE_COMPACT_MODEL
|
|
116
|
+
session_id: str
|
|
117
|
+
model_name: str | None
|
|
118
|
+
save_as_default: bool = False
|
|
119
|
+
|
|
120
|
+
async def execute(self, handler: OperationHandler) -> None:
|
|
121
|
+
await handler.handle_change_compact_model(self)
|
|
122
|
+
|
|
123
|
+
|
|
97
124
|
class ChangeThinkingOperation(Operation):
|
|
98
125
|
"""Operation for changing the thinking/reasoning configuration."""
|
|
99
126
|
|
|
@@ -10,11 +10,13 @@ from typing import TYPE_CHECKING, Protocol
|
|
|
10
10
|
|
|
11
11
|
if TYPE_CHECKING:
|
|
12
12
|
from klaude_code.protocol.op import (
|
|
13
|
+
ChangeCompactModelOperation,
|
|
13
14
|
ChangeModelOperation,
|
|
14
15
|
ChangeSubAgentModelOperation,
|
|
15
16
|
ChangeThinkingOperation,
|
|
16
17
|
ClearSessionOperation,
|
|
17
18
|
CompactSessionOperation,
|
|
19
|
+
ContinueAgentOperation,
|
|
18
20
|
ExportSessionOperation,
|
|
19
21
|
InitAgentOperation,
|
|
20
22
|
InterruptOperation,
|
|
@@ -30,6 +32,10 @@ class OperationHandler(Protocol):
|
|
|
30
32
|
"""Handle a run agent operation."""
|
|
31
33
|
...
|
|
32
34
|
|
|
35
|
+
async def handle_continue_agent(self, operation: ContinueAgentOperation) -> None:
|
|
36
|
+
"""Handle a continue agent operation (resume without adding user message)."""
|
|
37
|
+
...
|
|
38
|
+
|
|
33
39
|
async def handle_compact_session(self, operation: CompactSessionOperation) -> None:
|
|
34
40
|
"""Handle a compact session operation."""
|
|
35
41
|
...
|
|
@@ -38,6 +44,10 @@ class OperationHandler(Protocol):
|
|
|
38
44
|
"""Handle a change model operation."""
|
|
39
45
|
...
|
|
40
46
|
|
|
47
|
+
async def handle_change_compact_model(self, operation: ChangeCompactModelOperation) -> None:
|
|
48
|
+
"""Handle a change compact model operation."""
|
|
49
|
+
...
|
|
50
|
+
|
|
41
51
|
async def handle_change_thinking(self, operation: ChangeThinkingOperation) -> None:
|
|
42
52
|
"""Handle a change thinking operation."""
|
|
43
53
|
...
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Sub-Agent Protocol
|
|
2
2
|
|
|
3
|
-
Sub-agents are specialized
|
|
3
|
+
Sub-agents are specialized agent types invoked by tools like Task and ImageGen. This module defines profiles and registration.
|
|
4
4
|
|
|
5
5
|
## Key Constraint
|
|
6
6
|
|
|
@@ -9,7 +9,7 @@ The `protocol` layer cannot import from `config` or `core` (enforced by import-l
|
|
|
9
9
|
## Core Files
|
|
10
10
|
|
|
11
11
|
- `__init__.py` - `SubAgentProfile` dataclass and registration. Defines `AVAILABILITY_*` constants.
|
|
12
|
-
- `image_gen.py`, `task.py`, `explore.py`, `web.py` - Individual sub-agent definitions.
|
|
12
|
+
- `image_gen.py`, `task.py`, `explore.py`, `web.py` - Individual sub-agent type definitions.
|
|
13
13
|
|
|
14
14
|
## Availability Requirement Flow
|
|
15
15
|
|
|
@@ -23,6 +23,6 @@ Some sub-agents require specific model capabilities (e.g., ImageGen needs an ima
|
|
|
23
23
|
## Model Selection
|
|
24
24
|
|
|
25
25
|
For sub-agents with `availability_requirement`, priority is:
|
|
26
|
-
1. Explicit config in `sub_agent_models`
|
|
27
|
-
2.
|
|
28
|
-
3.
|
|
26
|
+
1. Explicit config in `sub_agent_models` for the specific type
|
|
27
|
+
2. Fallback to the Task model config (if present)
|
|
28
|
+
3. Otherwise inherit the main agent model
|