klaude-code 1.9.0__py3-none-any.whl → 2.0.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/auth/base.py +2 -6
- klaude_code/cli/auth_cmd.py +4 -4
- klaude_code/cli/list_model.py +1 -1
- klaude_code/cli/main.py +1 -1
- klaude_code/cli/runtime.py +7 -5
- klaude_code/cli/self_update.py +1 -1
- klaude_code/cli/session_cmd.py +1 -1
- klaude_code/command/clear_cmd.py +6 -2
- klaude_code/command/command_abc.py +2 -2
- klaude_code/command/debug_cmd.py +4 -4
- klaude_code/command/export_cmd.py +2 -2
- klaude_code/command/export_online_cmd.py +12 -12
- klaude_code/command/fork_session_cmd.py +29 -23
- klaude_code/command/help_cmd.py +4 -4
- klaude_code/command/model_cmd.py +4 -4
- klaude_code/command/model_select.py +1 -1
- klaude_code/command/prompt-commit.md +11 -2
- klaude_code/command/prompt_command.py +3 -3
- klaude_code/command/refresh_cmd.py +2 -2
- klaude_code/command/registry.py +7 -5
- klaude_code/command/release_notes_cmd.py +4 -4
- klaude_code/command/resume_cmd.py +15 -11
- klaude_code/command/status_cmd.py +4 -4
- klaude_code/command/terminal_setup_cmd.py +8 -8
- klaude_code/command/thinking_cmd.py +4 -4
- klaude_code/config/assets/builtin_config.yaml +16 -0
- klaude_code/config/builtin_config.py +16 -5
- klaude_code/config/config.py +7 -2
- klaude_code/const.py +146 -91
- klaude_code/core/agent.py +3 -12
- klaude_code/core/executor.py +21 -13
- klaude_code/core/manager/sub_agent_manager.py +71 -7
- klaude_code/core/prompts/prompt-sub-agent-image-gen.md +1 -0
- klaude_code/core/prompts/prompt-sub-agent-web.md +27 -1
- klaude_code/core/reminders.py +88 -69
- klaude_code/core/task.py +44 -45
- klaude_code/core/tool/file/apply_patch_tool.py +9 -9
- klaude_code/core/tool/file/diff_builder.py +3 -5
- klaude_code/core/tool/file/edit_tool.py +23 -23
- klaude_code/core/tool/file/move_tool.py +43 -43
- klaude_code/core/tool/file/read_tool.py +44 -39
- klaude_code/core/tool/file/write_tool.py +14 -14
- klaude_code/core/tool/report_back_tool.py +4 -4
- klaude_code/core/tool/shell/bash_tool.py +23 -23
- klaude_code/core/tool/skill/skill_tool.py +7 -7
- klaude_code/core/tool/sub_agent_tool.py +38 -9
- klaude_code/core/tool/todo/todo_write_tool.py +8 -8
- klaude_code/core/tool/todo/update_plan_tool.py +6 -6
- klaude_code/core/tool/tool_abc.py +2 -2
- klaude_code/core/tool/tool_context.py +27 -0
- klaude_code/core/tool/tool_runner.py +88 -42
- klaude_code/core/tool/truncation.py +38 -20
- klaude_code/core/tool/web/mermaid_tool.py +6 -7
- klaude_code/core/tool/web/web_fetch_tool.py +68 -30
- klaude_code/core/tool/web/web_search_tool.py +15 -17
- klaude_code/core/turn.py +120 -73
- klaude_code/llm/anthropic/client.py +79 -44
- klaude_code/llm/anthropic/input.py +116 -108
- klaude_code/llm/bedrock/client.py +8 -5
- klaude_code/llm/claude/client.py +18 -8
- klaude_code/llm/client.py +4 -3
- klaude_code/llm/codex/client.py +15 -9
- klaude_code/llm/google/client.py +122 -60
- klaude_code/llm/google/input.py +94 -108
- klaude_code/llm/image.py +123 -0
- klaude_code/llm/input_common.py +136 -189
- klaude_code/llm/openai_compatible/client.py +17 -7
- klaude_code/llm/openai_compatible/input.py +36 -66
- klaude_code/llm/openai_compatible/stream.py +119 -67
- klaude_code/llm/openai_compatible/tool_call_accumulator.py +23 -11
- klaude_code/llm/openrouter/client.py +34 -9
- klaude_code/llm/openrouter/input.py +63 -64
- klaude_code/llm/openrouter/reasoning.py +22 -24
- klaude_code/llm/registry.py +20 -17
- klaude_code/llm/responses/client.py +107 -45
- klaude_code/llm/responses/input.py +115 -98
- klaude_code/llm/usage.py +52 -25
- klaude_code/protocol/__init__.py +1 -0
- klaude_code/protocol/events.py +16 -12
- klaude_code/protocol/llm_param.py +20 -2
- klaude_code/protocol/message.py +250 -0
- klaude_code/protocol/model.py +94 -281
- klaude_code/protocol/op.py +2 -2
- klaude_code/protocol/sub_agent/__init__.py +1 -0
- klaude_code/protocol/sub_agent/explore.py +10 -0
- klaude_code/protocol/sub_agent/image_gen.py +119 -0
- klaude_code/protocol/sub_agent/task.py +10 -0
- klaude_code/protocol/sub_agent/web.py +10 -0
- klaude_code/session/codec.py +6 -6
- klaude_code/session/export.py +261 -62
- klaude_code/session/selector.py +7 -24
- klaude_code/session/session.py +126 -54
- klaude_code/session/store.py +5 -32
- klaude_code/session/templates/export_session.html +1 -1
- klaude_code/session/templates/mermaid_viewer.html +1 -1
- klaude_code/trace/log.py +11 -6
- klaude_code/ui/core/input.py +1 -1
- klaude_code/ui/core/stage_manager.py +1 -8
- klaude_code/ui/modes/debug/display.py +2 -2
- klaude_code/ui/modes/repl/clipboard.py +2 -2
- klaude_code/ui/modes/repl/completers.py +18 -10
- klaude_code/ui/modes/repl/event_handler.py +136 -127
- klaude_code/ui/modes/repl/input_prompt_toolkit.py +1 -1
- klaude_code/ui/modes/repl/key_bindings.py +1 -1
- klaude_code/ui/modes/repl/renderer.py +107 -15
- klaude_code/ui/renderers/assistant.py +2 -2
- klaude_code/ui/renderers/common.py +65 -7
- klaude_code/ui/renderers/developer.py +7 -6
- klaude_code/ui/renderers/diffs.py +11 -11
- klaude_code/ui/renderers/mermaid_viewer.py +49 -2
- klaude_code/ui/renderers/metadata.py +33 -5
- klaude_code/ui/renderers/sub_agent.py +57 -16
- klaude_code/ui/renderers/thinking.py +37 -2
- klaude_code/ui/renderers/tools.py +180 -165
- klaude_code/ui/rich/live.py +3 -1
- klaude_code/ui/rich/markdown.py +39 -7
- klaude_code/ui/rich/quote.py +76 -1
- klaude_code/ui/rich/status.py +14 -8
- klaude_code/ui/rich/theme.py +8 -2
- klaude_code/ui/terminal/image.py +34 -0
- klaude_code/ui/terminal/notifier.py +2 -1
- klaude_code/ui/terminal/progress_bar.py +4 -4
- klaude_code/ui/terminal/selector.py +22 -4
- klaude_code/ui/utils/common.py +11 -2
- {klaude_code-1.9.0.dist-info → klaude_code-2.0.0.dist-info}/METADATA +4 -2
- klaude_code-2.0.0.dist-info/RECORD +229 -0
- klaude_code-1.9.0.dist-info/RECORD +0 -224
- {klaude_code-1.9.0.dist-info → klaude_code-2.0.0.dist-info}/WHEEL +0 -0
- {klaude_code-1.9.0.dist-info → klaude_code-2.0.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Models for LLM API input and response items.
|
|
3
|
+
|
|
4
|
+
History is persisted as HistoryEvent (messages + error/task metadata).
|
|
5
|
+
Streaming-only items are emitted at runtime but never persisted.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from collections.abc import Sequence
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from typing import Annotated, Literal
|
|
11
|
+
|
|
12
|
+
from pydantic import BaseModel, Field, field_validator
|
|
13
|
+
|
|
14
|
+
from klaude_code.protocol.model import (
|
|
15
|
+
AtPatternParseResult,
|
|
16
|
+
CommandOutput,
|
|
17
|
+
StopReason,
|
|
18
|
+
TaskMetadata,
|
|
19
|
+
TaskMetadataItem,
|
|
20
|
+
ToolResultUIExtra,
|
|
21
|
+
ToolSideEffect,
|
|
22
|
+
ToolStatus,
|
|
23
|
+
Usage,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# Stream items
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ToolCallStartItem(BaseModel):
|
|
30
|
+
"""Transient streaming signal when LLM starts a tool call.
|
|
31
|
+
|
|
32
|
+
This is NOT persisted to conversation history. Used only for
|
|
33
|
+
real-time UI feedback (e.g., "Calling Bash …").
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
response_id: str | None = None
|
|
37
|
+
call_id: str
|
|
38
|
+
name: str
|
|
39
|
+
created_at: datetime = Field(default_factory=datetime.now)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class AssistantTextDelta(BaseModel):
|
|
43
|
+
response_id: str | None = None
|
|
44
|
+
content: str
|
|
45
|
+
created_at: datetime = Field(default_factory=datetime.now)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class AssistantImageDelta(BaseModel):
|
|
49
|
+
"""Streaming signal indicating an image has been saved to disk."""
|
|
50
|
+
|
|
51
|
+
response_id: str | None = None
|
|
52
|
+
file_path: str
|
|
53
|
+
created_at: datetime = Field(default_factory=datetime.now)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class ThinkingTextDelta(BaseModel):
|
|
57
|
+
response_id: str | None = None
|
|
58
|
+
content: str
|
|
59
|
+
created_at: datetime = Field(default_factory=datetime.now)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class StreamErrorItem(BaseModel):
|
|
63
|
+
error: str
|
|
64
|
+
created_at: datetime = Field(default_factory=datetime.now)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# Part types
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class TextPart(BaseModel):
|
|
71
|
+
type: Literal["text"] = "text"
|
|
72
|
+
text: str
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class ImageURLPart(BaseModel):
|
|
76
|
+
type: Literal["image_url"] = "image_url"
|
|
77
|
+
url: str
|
|
78
|
+
id: str | None = None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class ImageFilePart(BaseModel):
|
|
82
|
+
type: Literal["image_file"] = "image_file"
|
|
83
|
+
file_path: str
|
|
84
|
+
mime_type: str | None = None
|
|
85
|
+
byte_size: int | None = None
|
|
86
|
+
sha256: str | None = None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class ThinkingTextPart(BaseModel):
|
|
90
|
+
type: Literal["thinking_text"] = "thinking_text"
|
|
91
|
+
id: str | None = None
|
|
92
|
+
text: str
|
|
93
|
+
model_id: str | None = None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class ThinkingSignaturePart(BaseModel):
|
|
97
|
+
type: Literal["thinking_signature"] = "thinking_signature"
|
|
98
|
+
id: str | None = None
|
|
99
|
+
signature: str
|
|
100
|
+
model_id: str | None = None
|
|
101
|
+
format: str | None = None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class ToolCallPart(BaseModel):
|
|
105
|
+
type: Literal["tool_call"] = "tool_call"
|
|
106
|
+
call_id: str
|
|
107
|
+
id: str | None = None # Responses API: fc_xxx, different from call_id
|
|
108
|
+
tool_name: str
|
|
109
|
+
arguments_json: str
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
Part = Annotated[
|
|
113
|
+
TextPart | ImageURLPart | ImageFilePart | ThinkingTextPart | ThinkingSignaturePart | ToolCallPart,
|
|
114
|
+
Field(discriminator="type"),
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _empty_parts() -> list[Part]:
|
|
119
|
+
return []
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# Message types
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class MessageBase(BaseModel):
|
|
126
|
+
id: str | None = None
|
|
127
|
+
created_at: datetime = Field(default_factory=datetime.now)
|
|
128
|
+
response_id: str | None = None
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class SystemMessage(MessageBase):
|
|
132
|
+
role: Literal["system"] = "system"
|
|
133
|
+
parts: list[TextPart]
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class DeveloperMessage(MessageBase):
|
|
137
|
+
role: Literal["developer"] = "developer"
|
|
138
|
+
parts: list[Part]
|
|
139
|
+
|
|
140
|
+
# Special fields for reminders UI
|
|
141
|
+
memory_paths: list[str] | None = None
|
|
142
|
+
memory_mentioned: dict[str, list[str]] | None = None # memory_path -> list of @ patterns mentioned in it
|
|
143
|
+
external_file_changes: list[str] | None = None
|
|
144
|
+
todo_use: bool | None = None
|
|
145
|
+
at_files: list[AtPatternParseResult] | None = None
|
|
146
|
+
command_output: CommandOutput | None = None
|
|
147
|
+
user_image_count: int | None = None
|
|
148
|
+
skill_name: str | None = None # Skill name activated via $skill syntax
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class UserMessage(MessageBase):
|
|
152
|
+
role: Literal["user"] = "user"
|
|
153
|
+
parts: list[Part]
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class AssistantMessage(MessageBase):
|
|
157
|
+
role: Literal["assistant"] = "assistant"
|
|
158
|
+
parts: list[Part]
|
|
159
|
+
usage: Usage | None = None
|
|
160
|
+
stop_reason: StopReason | None = None
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class ToolResultMessage(MessageBase):
|
|
164
|
+
role: Literal["tool"] = "tool"
|
|
165
|
+
call_id: str = ""
|
|
166
|
+
tool_name: str = ""
|
|
167
|
+
status: ToolStatus
|
|
168
|
+
output_text: str
|
|
169
|
+
parts: list[Part] = Field(default_factory=_empty_parts)
|
|
170
|
+
ui_extra: ToolResultUIExtra | None = None
|
|
171
|
+
side_effects: list[ToolSideEffect] | None = None
|
|
172
|
+
task_metadata: TaskMetadata | None = None # Sub-agent task metadata for propagation to main agent
|
|
173
|
+
|
|
174
|
+
@field_validator("parts")
|
|
175
|
+
@classmethod
|
|
176
|
+
def _ensure_non_text_parts(cls, parts: list[Part]) -> list[Part]:
|
|
177
|
+
if any(isinstance(part, TextPart) for part in parts):
|
|
178
|
+
raise ValueError("ToolResultMessage.parts must not include text parts")
|
|
179
|
+
return parts
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
Message = SystemMessage | DeveloperMessage | UserMessage | AssistantMessage | ToolResultMessage
|
|
183
|
+
|
|
184
|
+
HistoryEvent = Message | StreamErrorItem | TaskMetadataItem
|
|
185
|
+
|
|
186
|
+
StreamItem = AssistantTextDelta | AssistantImageDelta | ThinkingTextDelta | ToolCallStartItem
|
|
187
|
+
|
|
188
|
+
LLMStreamItem = HistoryEvent | StreamItem
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
# User input
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class UserInputPayload(BaseModel):
|
|
195
|
+
"""Structured payload for user input containing text and optional images.
|
|
196
|
+
|
|
197
|
+
This is the unified data structure for user input across the entire
|
|
198
|
+
UI -> CLI -> Executor -> Agent -> Task chain.
|
|
199
|
+
"""
|
|
200
|
+
|
|
201
|
+
text: str
|
|
202
|
+
images: list[ImageURLPart] | None = None
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
# Helper functions
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def text_parts_from_str(text: str | None) -> list[Part]:
|
|
209
|
+
if not text:
|
|
210
|
+
return []
|
|
211
|
+
return [TextPart(text=text)]
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def parts_from_text_and_images(text: str | None, images: list[ImageURLPart] | None) -> list[Part]:
|
|
215
|
+
parts: list[Part] = []
|
|
216
|
+
if text:
|
|
217
|
+
parts.append(TextPart(text=text))
|
|
218
|
+
if images:
|
|
219
|
+
parts.extend(images)
|
|
220
|
+
return parts
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def join_text_parts(parts: Sequence[Part]) -> str:
|
|
224
|
+
return "".join(part.text for part in parts if isinstance(part, TextPart))
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def format_saved_images(images: Sequence[ImageFilePart], text: str = "") -> str:
|
|
228
|
+
"""Format saved image paths with optional text prefix.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
images: List of ImageFilePart with file paths to format.
|
|
232
|
+
text: Optional text content to prepend.
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
Formatted string with image paths. Single image uses "Saved image at xxx",
|
|
236
|
+
multiple images use list format.
|
|
237
|
+
"""
|
|
238
|
+
valid_paths = [img.file_path for img in images if img.file_path]
|
|
239
|
+
if not valid_paths:
|
|
240
|
+
return text
|
|
241
|
+
|
|
242
|
+
if len(valid_paths) == 1:
|
|
243
|
+
image_text = f"Saved image at {valid_paths[0]}"
|
|
244
|
+
else:
|
|
245
|
+
image_lines = "\n".join(f"- {path}" for path in valid_paths)
|
|
246
|
+
image_text = f"Saved images:\n{image_lines}"
|
|
247
|
+
|
|
248
|
+
if text.strip():
|
|
249
|
+
return f"{text}\n\n{image_text}"
|
|
250
|
+
return image_text
|
klaude_code/protocol/model.py
CHANGED
|
@@ -4,11 +4,13 @@ from typing import Annotated, Any, Literal
|
|
|
4
4
|
|
|
5
5
|
from pydantic import BaseModel, ConfigDict, Field, computed_field
|
|
6
6
|
|
|
7
|
-
from klaude_code import
|
|
7
|
+
from klaude_code.const import DEFAULT_MAX_TOKENS
|
|
8
8
|
from klaude_code.protocol.commands import CommandName
|
|
9
9
|
from klaude_code.protocol.tools import SubAgentType
|
|
10
10
|
|
|
11
11
|
RoleType = Literal["system", "developer", "user", "assistant", "tool"]
|
|
12
|
+
StopReason = Literal["stop", "length", "tool_use", "error", "aborted"]
|
|
13
|
+
ToolStatus = Literal["success", "error", "aborted"]
|
|
12
14
|
TodoStatusType = Literal["pending", "in_progress", "completed"]
|
|
13
15
|
|
|
14
16
|
|
|
@@ -18,6 +20,7 @@ class Usage(BaseModel):
|
|
|
18
20
|
cached_tokens: int = 0
|
|
19
21
|
reasoning_tokens: int = 0
|
|
20
22
|
output_tokens: int = 0
|
|
23
|
+
image_tokens: int = 0 # Image generation tokens
|
|
21
24
|
|
|
22
25
|
# Context window tracking
|
|
23
26
|
context_size: int | None = None # Peak total_tokens seen (for context usage display)
|
|
@@ -31,7 +34,13 @@ class Usage(BaseModel):
|
|
|
31
34
|
input_cost: float | None = None # Cost for non-cached input tokens
|
|
32
35
|
output_cost: float | None = None # Cost for output tokens (including reasoning)
|
|
33
36
|
cache_read_cost: float | None = None # Cost for cached tokens
|
|
37
|
+
image_cost: float | None = None # Cost for image generation tokens
|
|
34
38
|
currency: str = "USD" # Currency for cost display (USD or CNY)
|
|
39
|
+
response_id: str | None = None
|
|
40
|
+
model_name: str = ""
|
|
41
|
+
provider: str | None = None # OpenRouter's provider name
|
|
42
|
+
task_duration_s: float | None = None
|
|
43
|
+
created_at: datetime = Field(default_factory=datetime.now)
|
|
35
44
|
|
|
36
45
|
@computed_field
|
|
37
46
|
@property
|
|
@@ -42,8 +51,8 @@ class Usage(BaseModel):
|
|
|
42
51
|
@computed_field
|
|
43
52
|
@property
|
|
44
53
|
def total_cost(self) -> float | None:
|
|
45
|
-
"""Total cost computed from input + output + cache_read costs."""
|
|
46
|
-
costs = [self.input_cost, self.output_cost, self.cache_read_cost]
|
|
54
|
+
"""Total cost computed from input + output + cache_read + image costs."""
|
|
55
|
+
costs = [self.input_cost, self.output_cost, self.cache_read_cost, self.image_cost]
|
|
47
56
|
non_none = [c for c in costs if c is not None]
|
|
48
57
|
return sum(non_none) if non_none else None
|
|
49
58
|
|
|
@@ -55,12 +64,91 @@ class Usage(BaseModel):
|
|
|
55
64
|
return None
|
|
56
65
|
if self.context_size is None:
|
|
57
66
|
return None
|
|
58
|
-
effective_limit = self.context_limit - (self.max_tokens or
|
|
67
|
+
effective_limit = self.context_limit - (self.max_tokens or DEFAULT_MAX_TOKENS)
|
|
59
68
|
if effective_limit <= 0:
|
|
60
69
|
return None
|
|
61
70
|
return (self.context_size / effective_limit) * 100
|
|
62
71
|
|
|
63
72
|
|
|
73
|
+
class TaskMetadata(BaseModel):
|
|
74
|
+
"""Base metadata for a task execution (used by both main and sub-agents)."""
|
|
75
|
+
|
|
76
|
+
usage: Usage | None = None
|
|
77
|
+
model_name: str = ""
|
|
78
|
+
provider: str | None = None
|
|
79
|
+
task_duration_s: float | None = None
|
|
80
|
+
turn_count: int = 0
|
|
81
|
+
|
|
82
|
+
@staticmethod
|
|
83
|
+
def merge_usage(dst: Usage, src: Usage) -> None:
|
|
84
|
+
"""Merge src usage into dst usage (in-place).
|
|
85
|
+
|
|
86
|
+
Accumulates token counts and cost components. Does not handle
|
|
87
|
+
special fields like throughput_tps, first_token_latency_ms,
|
|
88
|
+
context_size, or context_limit - those require custom logic.
|
|
89
|
+
"""
|
|
90
|
+
dst.input_tokens += src.input_tokens
|
|
91
|
+
dst.cached_tokens += src.cached_tokens
|
|
92
|
+
dst.reasoning_tokens += src.reasoning_tokens
|
|
93
|
+
dst.output_tokens += src.output_tokens
|
|
94
|
+
dst.image_tokens += src.image_tokens
|
|
95
|
+
|
|
96
|
+
if src.input_cost is not None:
|
|
97
|
+
dst.input_cost = (dst.input_cost or 0.0) + src.input_cost
|
|
98
|
+
if src.output_cost is not None:
|
|
99
|
+
dst.output_cost = (dst.output_cost or 0.0) + src.output_cost
|
|
100
|
+
if src.cache_read_cost is not None:
|
|
101
|
+
dst.cache_read_cost = (dst.cache_read_cost or 0.0) + src.cache_read_cost
|
|
102
|
+
if src.image_cost is not None:
|
|
103
|
+
dst.image_cost = (dst.image_cost or 0.0) + src.image_cost
|
|
104
|
+
|
|
105
|
+
@staticmethod
|
|
106
|
+
def aggregate_by_model(metadata_list: list["TaskMetadata"]) -> list["TaskMetadata"]:
|
|
107
|
+
"""Aggregate multiple TaskMetadata by (model_name, provider).
|
|
108
|
+
|
|
109
|
+
Returns a list sorted by total_cost descending.
|
|
110
|
+
|
|
111
|
+
Note: total_tokens and total_cost are now computed fields,
|
|
112
|
+
so we only accumulate the primary state fields here.
|
|
113
|
+
"""
|
|
114
|
+
aggregated: dict[tuple[str, str | None], TaskMetadata] = {}
|
|
115
|
+
|
|
116
|
+
for meta in metadata_list:
|
|
117
|
+
if not meta.usage:
|
|
118
|
+
continue
|
|
119
|
+
|
|
120
|
+
key = (meta.model_name, meta.provider)
|
|
121
|
+
usage = meta.usage
|
|
122
|
+
|
|
123
|
+
if key not in aggregated:
|
|
124
|
+
aggregated[key] = TaskMetadata(
|
|
125
|
+
model_name=meta.model_name,
|
|
126
|
+
provider=meta.provider,
|
|
127
|
+
usage=Usage(currency=usage.currency),
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
agg = aggregated[key]
|
|
131
|
+
if agg.usage is None:
|
|
132
|
+
continue
|
|
133
|
+
|
|
134
|
+
TaskMetadata.merge_usage(agg.usage, usage)
|
|
135
|
+
|
|
136
|
+
# Sort by total_cost descending
|
|
137
|
+
return sorted(
|
|
138
|
+
aggregated.values(),
|
|
139
|
+
key=lambda m: m.usage.total_cost if m.usage and m.usage.total_cost else 0.0,
|
|
140
|
+
reverse=True,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class TaskMetadataItem(BaseModel):
|
|
145
|
+
"""Aggregated metadata for a complete task, stored in conversation history."""
|
|
146
|
+
|
|
147
|
+
main_agent: TaskMetadata = Field(default_factory=TaskMetadata) # Main agent metadata
|
|
148
|
+
sub_agent_task_metadata: list[TaskMetadata] = Field(default_factory=lambda: list[TaskMetadata]())
|
|
149
|
+
created_at: datetime = Field(default_factory=datetime.now)
|
|
150
|
+
|
|
151
|
+
|
|
64
152
|
class TodoItem(BaseModel):
|
|
65
153
|
model_config = ConfigDict(populate_by_name=True)
|
|
66
154
|
|
|
@@ -193,7 +281,6 @@ class AtPatternParseResult(BaseModel):
|
|
|
193
281
|
result: str
|
|
194
282
|
tool_args: str
|
|
195
283
|
operation: Literal["Read", "List"]
|
|
196
|
-
images: list["ImageURLPart"] | None = None
|
|
197
284
|
mentioned_in: str | None = None # Parent file that referenced this file
|
|
198
285
|
|
|
199
286
|
|
|
@@ -207,283 +294,9 @@ class SubAgentState(BaseModel):
|
|
|
207
294
|
sub_agent_type: SubAgentType
|
|
208
295
|
sub_agent_desc: str
|
|
209
296
|
sub_agent_prompt: str
|
|
297
|
+
resume: str | None = None
|
|
210
298
|
output_schema: dict[str, Any] | None = None
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
"""
|
|
214
|
-
Models for LLM API input and response items.
|
|
215
|
-
|
|
216
|
-
A typical sequence of response items is:
|
|
217
|
-
- [StartItem]
|
|
218
|
-
- [ReasoningTextItem | ReasoningEncryptedItem]
|
|
219
|
-
- [AssistantMessageDelta] × n
|
|
220
|
-
- [AssistantMessageItem]
|
|
221
|
-
- [ToolCallItem] × n
|
|
222
|
-
- [ResponseMetadataItem]
|
|
223
|
-
- Done
|
|
224
|
-
|
|
225
|
-
A conversation history input contains:
|
|
226
|
-
- [UserMessageItem]
|
|
227
|
-
- [ReasoningTextItem | ReasoningEncryptedItem]
|
|
228
|
-
- [AssistantMessageItem]
|
|
229
|
-
- [ToolCallItem]
|
|
230
|
-
- [ToolResultItem]
|
|
231
|
-
- [InterruptItem]
|
|
232
|
-
- [DeveloperMessageItem]
|
|
233
|
-
|
|
234
|
-
When adding a new item, please also modify the following:
|
|
235
|
-
- session/codec.py (ConversationItem registry derived from ConversationItem union)
|
|
236
|
-
"""
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
class StartItem(BaseModel):
|
|
240
|
-
response_id: str
|
|
241
|
-
created_at: datetime = Field(default_factory=datetime.now)
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
class InterruptItem(BaseModel):
|
|
245
|
-
created_at: datetime = Field(default_factory=datetime.now)
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
class SystemMessageItem(BaseModel):
|
|
249
|
-
id: str | None = None
|
|
250
|
-
role: RoleType = "system"
|
|
251
|
-
content: str | None = None
|
|
252
|
-
created_at: datetime = Field(default_factory=datetime.now)
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
class DeveloperMessageItem(BaseModel):
|
|
256
|
-
id: str | None = None
|
|
257
|
-
role: RoleType = "developer"
|
|
258
|
-
content: str | None = None # For LLM input
|
|
259
|
-
images: list["ImageURLPart"] | None = None
|
|
260
|
-
created_at: datetime = Field(default_factory=datetime.now)
|
|
261
|
-
|
|
262
|
-
# Special fields for reminders UI
|
|
263
|
-
memory_paths: list[str] | None = None
|
|
264
|
-
memory_mentioned: dict[str, list[str]] | None = None # memory_path -> list of @ patterns mentioned in it
|
|
265
|
-
external_file_changes: list[str] | None = None
|
|
266
|
-
todo_use: bool | None = None
|
|
267
|
-
at_files: list[AtPatternParseResult] | None = None
|
|
268
|
-
command_output: CommandOutput | None = None
|
|
269
|
-
user_image_count: int | None = None
|
|
270
|
-
skill_name: str | None = None # Skill name activated via $skill syntax
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
class ImageURLPart(BaseModel):
|
|
274
|
-
class ImageURL(BaseModel):
|
|
275
|
-
url: str
|
|
276
|
-
id: str | None = None
|
|
277
|
-
|
|
278
|
-
image_url: ImageURL
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
class UserInputPayload(BaseModel):
|
|
282
|
-
"""Structured payload for user input containing text and optional images.
|
|
283
|
-
|
|
284
|
-
This is the unified data structure for user input across the entire
|
|
285
|
-
UI -> CLI -> Executor -> Agent -> Task chain.
|
|
286
|
-
"""
|
|
287
|
-
|
|
288
|
-
text: str
|
|
289
|
-
images: list["ImageURLPart"] | None = None
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
class UserMessageItem(BaseModel):
|
|
293
|
-
id: str | None = None
|
|
294
|
-
role: RoleType = "user"
|
|
295
|
-
content: str | None = None
|
|
296
|
-
images: list[ImageURLPart] | None = None
|
|
297
|
-
created_at: datetime = Field(default_factory=datetime.now)
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
class AssistantMessageItem(BaseModel):
|
|
301
|
-
id: str | None = None
|
|
302
|
-
role: RoleType = "assistant"
|
|
303
|
-
content: str | None = None
|
|
304
|
-
response_id: str | None = None
|
|
305
|
-
created_at: datetime = Field(default_factory=datetime.now)
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
class ReasoningTextItem(BaseModel):
|
|
309
|
-
id: str | None = None
|
|
310
|
-
response_id: str | None = None
|
|
311
|
-
content: str
|
|
312
|
-
model: str | None = None
|
|
313
|
-
created_at: datetime = Field(default_factory=datetime.now)
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
class ReasoningEncryptedItem(BaseModel):
|
|
317
|
-
id: str | None = None
|
|
318
|
-
response_id: str | None = None
|
|
319
|
-
encrypted_content: str # OpenAI encrypted content or Anthropic thinking signature
|
|
320
|
-
format: str | None = None
|
|
321
|
-
model: str | None
|
|
322
|
-
created_at: datetime = Field(default_factory=datetime.now)
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
class ToolCallStartItem(BaseModel):
|
|
326
|
-
"""Transient streaming signal when LLM starts a tool call.
|
|
327
|
-
|
|
328
|
-
This is NOT persisted to conversation history. Used only for
|
|
329
|
-
real-time UI feedback (e.g., "Calling Bash ...").
|
|
330
|
-
"""
|
|
331
|
-
|
|
332
|
-
response_id: str | None = None
|
|
333
|
-
call_id: str
|
|
334
|
-
name: str
|
|
335
|
-
created_at: datetime = Field(default_factory=datetime.now)
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
class ToolCallItem(BaseModel):
|
|
339
|
-
id: str | None = None
|
|
340
|
-
response_id: str | None = None
|
|
341
|
-
call_id: str
|
|
342
|
-
name: str
|
|
343
|
-
arguments: str
|
|
344
|
-
created_at: datetime = Field(default_factory=datetime.now)
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
class ToolResultItem(BaseModel):
|
|
348
|
-
call_id: str = "" # This field will auto set by tool registry's run_tool
|
|
349
|
-
output: str | None = None
|
|
350
|
-
status: Literal["success", "error"]
|
|
351
|
-
tool_name: str | None = None # This field will auto set by tool registry's run_tool
|
|
352
|
-
ui_extra: ToolResultUIExtra | None = None # Extra data for UI display, e.g. diff render
|
|
353
|
-
images: list[ImageURLPart] | None = None
|
|
354
|
-
side_effects: list[ToolSideEffect] | None = None
|
|
355
|
-
task_metadata: "TaskMetadata | None" = None # Sub-agent task metadata for propagation to main agent
|
|
356
|
-
created_at: datetime = Field(default_factory=datetime.now)
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
class AssistantMessageDelta(BaseModel):
|
|
360
|
-
response_id: str | None = None
|
|
361
|
-
content: str
|
|
362
|
-
created_at: datetime = Field(default_factory=datetime.now)
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
class ReasoningTextDelta(BaseModel):
|
|
366
|
-
response_id: str | None = None
|
|
367
|
-
content: str
|
|
368
|
-
created_at: datetime = Field(default_factory=datetime.now)
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
class StreamErrorItem(BaseModel):
|
|
372
|
-
error: str
|
|
373
|
-
created_at: datetime = Field(default_factory=datetime.now)
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
class ResponseMetadataItem(BaseModel):
|
|
377
|
-
"""Metadata for a single LLM response (turn-level)."""
|
|
378
|
-
|
|
379
|
-
response_id: str | None = None
|
|
380
|
-
usage: Usage | None = None
|
|
381
|
-
model_name: str = ""
|
|
382
|
-
provider: str | None = None # OpenRouter's provider name
|
|
383
|
-
task_duration_s: float | None = None
|
|
384
|
-
created_at: datetime = Field(default_factory=datetime.now)
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
class TaskMetadata(BaseModel):
|
|
388
|
-
"""Base metadata for a task execution (used by both main and sub-agents)."""
|
|
389
|
-
|
|
390
|
-
usage: Usage | None = None
|
|
391
|
-
model_name: str = ""
|
|
392
|
-
provider: str | None = None
|
|
393
|
-
task_duration_s: float | None = None
|
|
394
|
-
turn_count: int = 0
|
|
395
|
-
|
|
396
|
-
@staticmethod
|
|
397
|
-
def merge_usage(dst: Usage, src: Usage) -> None:
|
|
398
|
-
"""Merge src usage into dst usage (in-place).
|
|
399
|
-
|
|
400
|
-
Accumulates token counts and cost components. Does not handle
|
|
401
|
-
special fields like throughput_tps, first_token_latency_ms,
|
|
402
|
-
context_size, or context_limit - those require custom logic.
|
|
403
|
-
"""
|
|
404
|
-
dst.input_tokens += src.input_tokens
|
|
405
|
-
dst.cached_tokens += src.cached_tokens
|
|
406
|
-
dst.reasoning_tokens += src.reasoning_tokens
|
|
407
|
-
dst.output_tokens += src.output_tokens
|
|
408
|
-
|
|
409
|
-
if src.input_cost is not None:
|
|
410
|
-
dst.input_cost = (dst.input_cost or 0.0) + src.input_cost
|
|
411
|
-
if src.output_cost is not None:
|
|
412
|
-
dst.output_cost = (dst.output_cost or 0.0) + src.output_cost
|
|
413
|
-
if src.cache_read_cost is not None:
|
|
414
|
-
dst.cache_read_cost = (dst.cache_read_cost or 0.0) + src.cache_read_cost
|
|
415
|
-
|
|
416
|
-
@staticmethod
|
|
417
|
-
def aggregate_by_model(metadata_list: list["TaskMetadata"]) -> list["TaskMetadata"]:
|
|
418
|
-
"""Aggregate multiple TaskMetadata by (model_name, provider).
|
|
419
|
-
|
|
420
|
-
Returns a list sorted by total_cost descending.
|
|
421
|
-
|
|
422
|
-
Note: total_tokens and total_cost are now computed fields,
|
|
423
|
-
so we only accumulate the primary state fields here.
|
|
424
|
-
"""
|
|
425
|
-
aggregated: dict[tuple[str, str | None], TaskMetadata] = {}
|
|
426
|
-
|
|
427
|
-
for meta in metadata_list:
|
|
428
|
-
if not meta.usage:
|
|
429
|
-
continue
|
|
430
|
-
|
|
431
|
-
key = (meta.model_name, meta.provider)
|
|
432
|
-
usage = meta.usage
|
|
433
|
-
|
|
434
|
-
if key not in aggregated:
|
|
435
|
-
aggregated[key] = TaskMetadata(
|
|
436
|
-
model_name=meta.model_name,
|
|
437
|
-
provider=meta.provider,
|
|
438
|
-
usage=Usage(currency=usage.currency),
|
|
439
|
-
)
|
|
440
|
-
|
|
441
|
-
agg = aggregated[key]
|
|
442
|
-
if agg.usage is None:
|
|
443
|
-
continue
|
|
444
|
-
|
|
445
|
-
TaskMetadata.merge_usage(agg.usage, usage)
|
|
446
|
-
|
|
447
|
-
# Sort by total_cost descending
|
|
448
|
-
return sorted(
|
|
449
|
-
aggregated.values(),
|
|
450
|
-
key=lambda m: m.usage.total_cost if m.usage and m.usage.total_cost else 0.0,
|
|
451
|
-
reverse=True,
|
|
452
|
-
)
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
class TaskMetadataItem(BaseModel):
|
|
456
|
-
"""Aggregated metadata for a complete task, stored in conversation history."""
|
|
457
|
-
|
|
458
|
-
main_agent: TaskMetadata = Field(default_factory=TaskMetadata) # Main agent metadata
|
|
459
|
-
sub_agent_task_metadata: list[TaskMetadata] = Field(default_factory=lambda: list[TaskMetadata]())
|
|
460
|
-
created_at: datetime = Field(default_factory=datetime.now)
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
MessageItem = (
|
|
464
|
-
UserMessageItem
|
|
465
|
-
| AssistantMessageItem
|
|
466
|
-
| SystemMessageItem
|
|
467
|
-
| DeveloperMessageItem
|
|
468
|
-
| ReasoningTextItem
|
|
469
|
-
| ReasoningEncryptedItem
|
|
470
|
-
| ToolCallItem
|
|
471
|
-
| ToolResultItem
|
|
472
|
-
)
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
StreamItem = AssistantMessageDelta | ReasoningTextDelta
|
|
476
|
-
|
|
477
|
-
ConversationItem = (
|
|
478
|
-
StartItem
|
|
479
|
-
| InterruptItem
|
|
480
|
-
| StreamErrorItem
|
|
481
|
-
| StreamItem
|
|
482
|
-
| MessageItem
|
|
483
|
-
| ResponseMetadataItem
|
|
484
|
-
| TaskMetadataItem
|
|
485
|
-
| ToolCallStartItem
|
|
486
|
-
)
|
|
299
|
+
generation: dict[str, Any] | None = None
|
|
487
300
|
|
|
488
301
|
|
|
489
302
|
def todo_list_str(todos: list[TodoItem]) -> str:
|
klaude_code/protocol/op.py
CHANGED
|
@@ -14,7 +14,7 @@ from uuid import uuid4
|
|
|
14
14
|
from pydantic import BaseModel, Field
|
|
15
15
|
|
|
16
16
|
from klaude_code.protocol.llm_param import Thinking
|
|
17
|
-
from klaude_code.protocol.
|
|
17
|
+
from klaude_code.protocol.message import UserInputPayload
|
|
18
18
|
|
|
19
19
|
if TYPE_CHECKING:
|
|
20
20
|
from klaude_code.protocol.op_handler import OperationHandler
|
|
@@ -84,7 +84,7 @@ class ChangeModelOperation(Operation):
|
|
|
84
84
|
# This is useful for in-prompt model switching where extra output is noisy.
|
|
85
85
|
emit_welcome_event: bool = True
|
|
86
86
|
|
|
87
|
-
# When False, do not emit the "Switched to:
|
|
87
|
+
# When False, do not emit the "Switched to: …" developer message.
|
|
88
88
|
# This is useful for in-prompt model switching where extra output is noisy.
|
|
89
89
|
emit_switch_message: bool = True
|
|
90
90
|
|
|
@@ -112,5 +112,6 @@ def sub_agent_tool_names(enabled_only: bool = False, model_name: str | None = No
|
|
|
112
112
|
|
|
113
113
|
# Import sub-agent modules to trigger registration
|
|
114
114
|
from klaude_code.protocol.sub_agent import explore as explore # noqa: E402
|
|
115
|
+
from klaude_code.protocol.sub_agent import image_gen as image_gen # noqa: E402
|
|
115
116
|
from klaude_code.protocol.sub_agent import task as task # noqa: E402
|
|
116
117
|
from klaude_code.protocol.sub_agent import web as web # noqa: E402
|