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.
Files changed (132) hide show
  1. klaude_code/auth/base.py +2 -6
  2. klaude_code/cli/auth_cmd.py +4 -4
  3. klaude_code/cli/cost_cmd.py +1 -1
  4. klaude_code/cli/list_model.py +1 -1
  5. klaude_code/cli/main.py +1 -1
  6. klaude_code/cli/runtime.py +7 -5
  7. klaude_code/cli/self_update.py +1 -1
  8. klaude_code/cli/session_cmd.py +1 -1
  9. klaude_code/command/clear_cmd.py +6 -2
  10. klaude_code/command/command_abc.py +2 -2
  11. klaude_code/command/debug_cmd.py +4 -4
  12. klaude_code/command/export_cmd.py +2 -2
  13. klaude_code/command/export_online_cmd.py +12 -12
  14. klaude_code/command/fork_session_cmd.py +29 -23
  15. klaude_code/command/help_cmd.py +4 -4
  16. klaude_code/command/model_cmd.py +4 -4
  17. klaude_code/command/model_select.py +1 -1
  18. klaude_code/command/prompt-commit.md +11 -2
  19. klaude_code/command/prompt_command.py +3 -3
  20. klaude_code/command/refresh_cmd.py +2 -2
  21. klaude_code/command/registry.py +7 -5
  22. klaude_code/command/release_notes_cmd.py +4 -4
  23. klaude_code/command/resume_cmd.py +15 -11
  24. klaude_code/command/status_cmd.py +4 -4
  25. klaude_code/command/terminal_setup_cmd.py +8 -8
  26. klaude_code/command/thinking_cmd.py +4 -4
  27. klaude_code/config/assets/builtin_config.yaml +20 -0
  28. klaude_code/config/builtin_config.py +16 -5
  29. klaude_code/config/config.py +7 -2
  30. klaude_code/const.py +147 -91
  31. klaude_code/core/agent.py +3 -12
  32. klaude_code/core/executor.py +18 -39
  33. klaude_code/core/manager/sub_agent_manager.py +71 -7
  34. klaude_code/core/prompts/prompt-sub-agent-image-gen.md +1 -0
  35. klaude_code/core/prompts/prompt-sub-agent-web.md +27 -1
  36. klaude_code/core/reminders.py +88 -69
  37. klaude_code/core/task.py +44 -45
  38. klaude_code/core/tool/file/apply_patch_tool.py +9 -9
  39. klaude_code/core/tool/file/diff_builder.py +3 -5
  40. klaude_code/core/tool/file/edit_tool.py +23 -23
  41. klaude_code/core/tool/file/move_tool.py +43 -43
  42. klaude_code/core/tool/file/read_tool.py +44 -39
  43. klaude_code/core/tool/file/write_tool.py +14 -14
  44. klaude_code/core/tool/report_back_tool.py +4 -4
  45. klaude_code/core/tool/shell/bash_tool.py +23 -23
  46. klaude_code/core/tool/skill/skill_tool.py +7 -7
  47. klaude_code/core/tool/sub_agent_tool.py +38 -9
  48. klaude_code/core/tool/todo/todo_write_tool.py +9 -10
  49. klaude_code/core/tool/todo/update_plan_tool.py +6 -6
  50. klaude_code/core/tool/tool_abc.py +2 -2
  51. klaude_code/core/tool/tool_context.py +27 -0
  52. klaude_code/core/tool/tool_runner.py +88 -42
  53. klaude_code/core/tool/truncation.py +38 -20
  54. klaude_code/core/tool/web/mermaid_tool.py +6 -7
  55. klaude_code/core/tool/web/web_fetch_tool.py +68 -30
  56. klaude_code/core/tool/web/web_search_tool.py +15 -17
  57. klaude_code/core/turn.py +120 -73
  58. klaude_code/llm/anthropic/client.py +79 -44
  59. klaude_code/llm/anthropic/input.py +116 -108
  60. klaude_code/llm/bedrock/client.py +8 -5
  61. klaude_code/llm/claude/client.py +18 -8
  62. klaude_code/llm/client.py +4 -3
  63. klaude_code/llm/codex/client.py +15 -9
  64. klaude_code/llm/google/client.py +122 -60
  65. klaude_code/llm/google/input.py +94 -108
  66. klaude_code/llm/image.py +123 -0
  67. klaude_code/llm/input_common.py +136 -189
  68. klaude_code/llm/openai_compatible/client.py +17 -7
  69. klaude_code/llm/openai_compatible/input.py +36 -66
  70. klaude_code/llm/openai_compatible/stream.py +119 -67
  71. klaude_code/llm/openai_compatible/tool_call_accumulator.py +23 -11
  72. klaude_code/llm/openrouter/client.py +34 -9
  73. klaude_code/llm/openrouter/input.py +63 -64
  74. klaude_code/llm/openrouter/reasoning.py +22 -24
  75. klaude_code/llm/registry.py +20 -17
  76. klaude_code/llm/responses/client.py +107 -45
  77. klaude_code/llm/responses/input.py +115 -98
  78. klaude_code/llm/usage.py +52 -25
  79. klaude_code/protocol/__init__.py +1 -0
  80. klaude_code/protocol/events.py +16 -12
  81. klaude_code/protocol/llm_param.py +20 -2
  82. klaude_code/protocol/message.py +250 -0
  83. klaude_code/protocol/model.py +95 -285
  84. klaude_code/protocol/op.py +2 -15
  85. klaude_code/protocol/op_handler.py +0 -5
  86. klaude_code/protocol/sub_agent/__init__.py +1 -0
  87. klaude_code/protocol/sub_agent/explore.py +10 -0
  88. klaude_code/protocol/sub_agent/image_gen.py +119 -0
  89. klaude_code/protocol/sub_agent/task.py +10 -0
  90. klaude_code/protocol/sub_agent/web.py +10 -0
  91. klaude_code/session/codec.py +6 -6
  92. klaude_code/session/export.py +261 -62
  93. klaude_code/session/selector.py +7 -24
  94. klaude_code/session/session.py +126 -54
  95. klaude_code/session/store.py +5 -32
  96. klaude_code/session/templates/export_session.html +1 -1
  97. klaude_code/session/templates/mermaid_viewer.html +1 -1
  98. klaude_code/trace/log.py +11 -6
  99. klaude_code/ui/core/input.py +1 -1
  100. klaude_code/ui/core/stage_manager.py +1 -8
  101. klaude_code/ui/modes/debug/display.py +2 -2
  102. klaude_code/ui/modes/repl/clipboard.py +2 -2
  103. klaude_code/ui/modes/repl/completers.py +18 -10
  104. klaude_code/ui/modes/repl/event_handler.py +138 -132
  105. klaude_code/ui/modes/repl/input_prompt_toolkit.py +1 -1
  106. klaude_code/ui/modes/repl/key_bindings.py +136 -2
  107. klaude_code/ui/modes/repl/renderer.py +107 -15
  108. klaude_code/ui/renderers/assistant.py +2 -2
  109. klaude_code/ui/renderers/bash_syntax.py +36 -4
  110. klaude_code/ui/renderers/common.py +70 -10
  111. klaude_code/ui/renderers/developer.py +7 -6
  112. klaude_code/ui/renderers/diffs.py +11 -11
  113. klaude_code/ui/renderers/mermaid_viewer.py +49 -2
  114. klaude_code/ui/renderers/metadata.py +33 -5
  115. klaude_code/ui/renderers/sub_agent.py +57 -16
  116. klaude_code/ui/renderers/thinking.py +37 -2
  117. klaude_code/ui/renderers/tools.py +188 -178
  118. klaude_code/ui/rich/live.py +3 -1
  119. klaude_code/ui/rich/markdown.py +39 -7
  120. klaude_code/ui/rich/quote.py +76 -1
  121. klaude_code/ui/rich/status.py +14 -8
  122. klaude_code/ui/rich/theme.py +20 -14
  123. klaude_code/ui/terminal/image.py +34 -0
  124. klaude_code/ui/terminal/notifier.py +2 -1
  125. klaude_code/ui/terminal/progress_bar.py +4 -4
  126. klaude_code/ui/terminal/selector.py +22 -4
  127. klaude_code/ui/utils/common.py +11 -2
  128. {klaude_code-1.9.0.dist-info → klaude_code-2.0.1.dist-info}/METADATA +4 -2
  129. klaude_code-2.0.1.dist-info/RECORD +229 -0
  130. klaude_code-1.9.0.dist-info/RECORD +0 -224
  131. {klaude_code-1.9.0.dist-info → klaude_code-2.0.1.dist-info}/WHEEL +0 -0
  132. {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
@@ -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, ConfigDict, Field, computed_field
5
+ from pydantic import BaseModel, Field, computed_field
6
6
 
7
- from klaude_code import const
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 const.DEFAULT_MAX_TOKENS)
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 TodoItem(BaseModel):
65
- model_config = ConfigDict(populate_by_name=True)
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: