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.
Files changed (100) hide show
  1. klaude_code/app/runtime.py +2 -1
  2. klaude_code/auth/antigravity/oauth.py +0 -9
  3. klaude_code/auth/antigravity/token_manager.py +0 -18
  4. klaude_code/auth/base.py +53 -0
  5. klaude_code/auth/codex/exceptions.py +0 -4
  6. klaude_code/auth/codex/oauth.py +32 -28
  7. klaude_code/auth/codex/token_manager.py +0 -18
  8. klaude_code/cli/cost_cmd.py +128 -39
  9. klaude_code/cli/list_model.py +27 -10
  10. klaude_code/cli/main.py +15 -4
  11. klaude_code/config/assets/builtin_config.yaml +8 -24
  12. klaude_code/config/config.py +47 -25
  13. klaude_code/config/sub_agent_model_helper.py +18 -13
  14. klaude_code/config/thinking.py +0 -8
  15. klaude_code/const.py +2 -2
  16. klaude_code/core/agent_profile.py +11 -53
  17. klaude_code/core/compaction/compaction.py +4 -6
  18. klaude_code/core/compaction/overflow.py +0 -4
  19. klaude_code/core/executor.py +51 -5
  20. klaude_code/core/manager/llm_clients.py +9 -1
  21. klaude_code/core/prompts/prompt-claude-code.md +4 -4
  22. klaude_code/core/reminders.py +21 -23
  23. klaude_code/core/task.py +0 -4
  24. klaude_code/core/tool/__init__.py +3 -2
  25. klaude_code/core/tool/file/apply_patch.py +0 -27
  26. klaude_code/core/tool/file/edit_tool.py +1 -2
  27. klaude_code/core/tool/file/read_tool.md +3 -2
  28. klaude_code/core/tool/file/read_tool.py +15 -2
  29. klaude_code/core/tool/offload.py +0 -35
  30. klaude_code/core/tool/sub_agent/__init__.py +6 -0
  31. klaude_code/core/tool/sub_agent/image_gen.md +16 -0
  32. klaude_code/core/tool/sub_agent/image_gen.py +146 -0
  33. klaude_code/core/tool/sub_agent/task.md +20 -0
  34. klaude_code/core/tool/sub_agent/task.py +205 -0
  35. klaude_code/core/tool/tool_registry.py +0 -16
  36. klaude_code/core/turn.py +1 -1
  37. klaude_code/llm/anthropic/input.py +6 -5
  38. klaude_code/llm/antigravity/input.py +14 -7
  39. klaude_code/llm/codex/client.py +22 -0
  40. klaude_code/llm/codex/prompt_sync.py +237 -0
  41. klaude_code/llm/google/client.py +8 -6
  42. klaude_code/llm/google/input.py +20 -12
  43. klaude_code/llm/image.py +18 -11
  44. klaude_code/llm/input_common.py +14 -6
  45. klaude_code/llm/json_stable.py +37 -0
  46. klaude_code/llm/openai_compatible/input.py +0 -10
  47. klaude_code/llm/openai_compatible/stream.py +16 -1
  48. klaude_code/llm/registry.py +0 -5
  49. klaude_code/llm/responses/input.py +15 -5
  50. klaude_code/llm/usage.py +0 -8
  51. klaude_code/protocol/commands.py +1 -0
  52. klaude_code/protocol/events.py +2 -1
  53. klaude_code/protocol/message.py +2 -2
  54. klaude_code/protocol/model.py +20 -1
  55. klaude_code/protocol/op.py +27 -0
  56. klaude_code/protocol/op_handler.py +10 -0
  57. klaude_code/protocol/sub_agent/AGENTS.md +5 -5
  58. klaude_code/protocol/sub_agent/__init__.py +13 -34
  59. klaude_code/protocol/sub_agent/explore.py +7 -34
  60. klaude_code/protocol/sub_agent/image_gen.py +3 -74
  61. klaude_code/protocol/sub_agent/task.py +3 -47
  62. klaude_code/protocol/sub_agent/web.py +8 -52
  63. klaude_code/protocol/tools.py +2 -0
  64. klaude_code/session/export.py +308 -299
  65. klaude_code/session/session.py +58 -21
  66. klaude_code/session/store.py +0 -4
  67. klaude_code/session/templates/export_session.html +430 -134
  68. klaude_code/skill/assets/deslop/SKILL.md +9 -0
  69. klaude_code/skill/system_skills.py +0 -20
  70. klaude_code/tui/command/__init__.py +3 -0
  71. klaude_code/tui/command/continue_cmd.py +34 -0
  72. klaude_code/tui/command/fork_session_cmd.py +5 -2
  73. klaude_code/tui/command/resume_cmd.py +9 -2
  74. klaude_code/tui/command/sub_agent_model_cmd.py +85 -18
  75. klaude_code/tui/components/assistant.py +0 -26
  76. klaude_code/tui/components/command_output.py +3 -1
  77. klaude_code/tui/components/developer.py +3 -0
  78. klaude_code/tui/components/diffs.py +2 -208
  79. klaude_code/tui/components/errors.py +4 -0
  80. klaude_code/tui/components/mermaid_viewer.py +2 -2
  81. klaude_code/tui/components/rich/markdown.py +60 -63
  82. klaude_code/tui/components/rich/theme.py +2 -0
  83. klaude_code/tui/components/sub_agent.py +2 -46
  84. klaude_code/tui/components/thinking.py +0 -33
  85. klaude_code/tui/components/tools.py +43 -21
  86. klaude_code/tui/input/images.py +21 -18
  87. klaude_code/tui/input/key_bindings.py +2 -2
  88. klaude_code/tui/input/prompt_toolkit.py +49 -49
  89. klaude_code/tui/machine.py +15 -11
  90. klaude_code/tui/renderer.py +12 -20
  91. klaude_code/tui/runner.py +2 -1
  92. klaude_code/tui/terminal/image.py +6 -34
  93. klaude_code/ui/common.py +0 -70
  94. {klaude_code-2.8.0.dist-info → klaude_code-2.9.0.dist-info}/METADATA +3 -6
  95. {klaude_code-2.8.0.dist-info → klaude_code-2.9.0.dist-info}/RECORD +97 -92
  96. klaude_code/core/tool/sub_agent_tool.py +0 -126
  97. klaude_code/llm/openai_compatible/tool_call_accumulator.py +0 -108
  98. klaude_code/tui/components/rich/searchable_text.py +0 -68
  99. {klaude_code-2.8.0.dist-info → klaude_code-2.9.0.dist-info}/WHEEL +0 -0
  100. {klaude_code-2.8.0.dist-info → klaude_code-2.9.0.dist-info}/entry_points.txt +0 -0
@@ -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: message.ImageURLPart) -> types.Part:
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: message.ImageURLPart) -> types.FunctionResponsePart:
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 = [part for part in msg.parts if isinstance(part, message.ImageURLPart)] + attachment.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
- args = json.loads(part.arguments_json)
163
+ loaded: object = json.loads(part.arguments_json)
159
164
  except json.JSONDecodeError:
160
- args = {"_raw": part.arguments_json}
165
+ loaded = {"_raw": part.arguments_json}
161
166
  else:
162
- args = {}
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 assistant_image_to_data_url(image: message.ImageFilePart) -> str:
103
- """Load an assistant image from disk and encode it as a base64 data URL.
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)
@@ -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
- def _empty_image_parts() -> list[message.ImageURLPart]:
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[message.ImageURLPart] = field(default_factory=_empty_image_parts)
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[message.ImageURLPart]]:
27
+ def _extract_developer_content(msg: message.DeveloperMessage) -> tuple[str, list[ImagePart]]:
25
28
  text_parts: list[str] = []
26
- images: list[message.ImageURLPart] = []
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
- parts.append({"type": "image_url", "image_url": {"url": image.url}})
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
 
@@ -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.url},
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.url},
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 = [part for part in tool.parts if isinstance(part, message.ImageURLPart)] + attachment.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.url},
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()
@@ -27,6 +27,7 @@ class CommandName(str, Enum):
27
27
  FORK_SESSION = "fork-session"
28
28
  RESUME = "resume"
29
29
  COPY = "copy"
30
+ CONTINUE = "continue"
30
31
 
31
32
  def __str__(self) -> str:
32
33
  return self.value
@@ -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: list[message.ImageURLPart] | None = None
62
+ images: Sequence[message.ImageURLPart | message.ImageFilePart] | None = None
62
63
 
63
64
 
64
65
  class DeveloperMessageEvent(Event):
@@ -213,7 +213,7 @@ class UserInputPayload(BaseModel):
213
213
  """
214
214
 
215
215
  text: str
216
- images: list[ImageURLPart] | None = None
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: list[ImageURLPart] | None) -> list[Part]:
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))
@@ -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 | TodoListUIExtra | SessionIdUIExtra | MermaidLinkUIExtra | MarkdownDocUIExtra | SessionStatusUIExtra
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
 
@@ -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 agents invoked by the main agent as tools. This module defines profiles and registration.
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. Auto-resolve via requirement
28
- 3. If neither found, sub-agent is unavailable (no fallback to main agent model)
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