klaude-code 1.9.0__py3-none-any.whl → 2.0.1__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/cost_cmd.py +1 -1
- 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 +20 -0
- klaude_code/config/builtin_config.py +16 -5
- klaude_code/config/config.py +7 -2
- klaude_code/const.py +147 -91
- klaude_code/core/agent.py +3 -12
- klaude_code/core/executor.py +18 -39
- 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 +9 -10
- 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 +95 -285
- klaude_code/protocol/op.py +2 -15
- klaude_code/protocol/op_handler.py +0 -5
- 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 +138 -132
- klaude_code/ui/modes/repl/input_prompt_toolkit.py +1 -1
- klaude_code/ui/modes/repl/key_bindings.py +136 -2
- klaude_code/ui/modes/repl/renderer.py +107 -15
- klaude_code/ui/renderers/assistant.py +2 -2
- klaude_code/ui/renderers/bash_syntax.py +36 -4
- klaude_code/ui/renderers/common.py +70 -10
- 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 +188 -178
- 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 +20 -14
- 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.1.dist-info}/METADATA +4 -2
- klaude_code-2.0.1.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.1.dist-info}/WHEEL +0 -0
- {klaude_code-1.9.0.dist-info → klaude_code-2.0.1.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
|
@@ -2,13 +2,15 @@ from datetime import datetime
|
|
|
2
2
|
from enum import Enum
|
|
3
3
|
from typing import Annotated, Any, Literal
|
|
4
4
|
|
|
5
|
-
from pydantic import BaseModel,
|
|
5
|
+
from pydantic import BaseModel, 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,18 +64,94 @@ 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
|
|
|
64
|
-
class
|
|
65
|
-
|
|
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
|
+
|
|
66
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
|
+
|
|
152
|
+
class TodoItem(BaseModel):
|
|
67
153
|
content: str
|
|
68
154
|
status: TodoStatusType
|
|
69
|
-
active_form: str = Field(default="", alias="activeForm")
|
|
70
155
|
|
|
71
156
|
|
|
72
157
|
class FileStatus(BaseModel):
|
|
@@ -193,7 +278,6 @@ class AtPatternParseResult(BaseModel):
|
|
|
193
278
|
result: str
|
|
194
279
|
tool_args: str
|
|
195
280
|
operation: Literal["Read", "List"]
|
|
196
|
-
images: list["ImageURLPart"] | None = None
|
|
197
281
|
mentioned_in: str | None = None # Parent file that referenced this file
|
|
198
282
|
|
|
199
283
|
|
|
@@ -207,283 +291,9 @@ class SubAgentState(BaseModel):
|
|
|
207
291
|
sub_agent_type: SubAgentType
|
|
208
292
|
sub_agent_desc: str
|
|
209
293
|
sub_agent_prompt: str
|
|
294
|
+
resume: str | None = None
|
|
210
295
|
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
|
-
)
|
|
296
|
+
generation: dict[str, Any] | None = None
|
|
487
297
|
|
|
488
298
|
|
|
489
299
|
def todo_list_str(todos: list[TodoItem]) -> str:
|