klaude-code 1.2.7__py3-none-any.whl → 1.2.9__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/codex/__init__.py +1 -1
- klaude_code/command/__init__.py +2 -0
- klaude_code/command/prompt-deslop.md +14 -0
- klaude_code/command/release_notes_cmd.py +86 -0
- klaude_code/command/status_cmd.py +92 -54
- klaude_code/core/agent.py +13 -19
- klaude_code/core/manager/sub_agent_manager.py +5 -1
- klaude_code/core/prompt.py +38 -28
- klaude_code/core/reminders.py +4 -4
- klaude_code/core/task.py +60 -45
- klaude_code/core/tool/__init__.py +2 -0
- klaude_code/core/tool/file/apply_patch_tool.py +1 -1
- klaude_code/core/tool/file/edit_tool.py +1 -1
- klaude_code/core/tool/file/multi_edit_tool.py +1 -1
- klaude_code/core/tool/file/write_tool.py +1 -1
- klaude_code/core/tool/memory/memory_tool.py +2 -2
- klaude_code/core/tool/sub_agent_tool.py +2 -1
- klaude_code/core/tool/todo/todo_write_tool.py +1 -1
- klaude_code/core/tool/todo/update_plan_tool.py +1 -1
- klaude_code/core/tool/tool_context.py +21 -4
- klaude_code/core/tool/tool_runner.py +5 -8
- klaude_code/core/tool/web/mermaid_tool.py +1 -4
- klaude_code/core/turn.py +90 -62
- klaude_code/llm/anthropic/client.py +15 -46
- klaude_code/llm/client.py +1 -1
- klaude_code/llm/codex/client.py +44 -30
- klaude_code/llm/input_common.py +0 -6
- klaude_code/llm/openai_compatible/client.py +29 -73
- klaude_code/llm/openai_compatible/input.py +6 -4
- klaude_code/llm/openai_compatible/stream_processor.py +82 -0
- klaude_code/llm/openrouter/client.py +29 -59
- klaude_code/llm/openrouter/input.py +4 -27
- klaude_code/llm/responses/client.py +49 -79
- klaude_code/llm/usage.py +51 -10
- klaude_code/protocol/commands.py +1 -0
- klaude_code/protocol/events.py +12 -2
- klaude_code/protocol/model.py +142 -26
- klaude_code/protocol/sub_agent.py +5 -1
- klaude_code/session/export.py +51 -27
- klaude_code/session/session.py +33 -16
- klaude_code/session/templates/export_session.html +4 -1
- klaude_code/ui/modes/repl/__init__.py +1 -5
- klaude_code/ui/modes/repl/event_handler.py +153 -54
- klaude_code/ui/modes/repl/renderer.py +6 -4
- klaude_code/ui/renderers/developer.py +35 -25
- klaude_code/ui/renderers/metadata.py +68 -30
- klaude_code/ui/renderers/tools.py +53 -87
- klaude_code/ui/rich/markdown.py +5 -5
- {klaude_code-1.2.7.dist-info → klaude_code-1.2.9.dist-info}/METADATA +1 -1
- {klaude_code-1.2.7.dist-info → klaude_code-1.2.9.dist-info}/RECORD +52 -49
- {klaude_code-1.2.7.dist-info → klaude_code-1.2.9.dist-info}/WHEEL +0 -0
- {klaude_code-1.2.7.dist-info → klaude_code-1.2.9.dist-info}/entry_points.txt +0 -0
klaude_code/protocol/model.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
from datetime import datetime
|
|
2
2
|
from enum import Enum
|
|
3
|
-
from typing import Literal
|
|
3
|
+
from typing import Annotated, Literal
|
|
4
4
|
|
|
5
|
-
from pydantic import BaseModel, Field
|
|
5
|
+
from pydantic import BaseModel, ConfigDict, Field, computed_field
|
|
6
6
|
|
|
7
7
|
from klaude_code.protocol.commands import CommandName
|
|
8
8
|
from klaude_code.protocol.tools import SubAgentType
|
|
@@ -12,12 +12,16 @@ TodoStatusType = Literal["pending", "in_progress", "completed"]
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class Usage(BaseModel):
|
|
15
|
+
# Token Usage (primary state)
|
|
15
16
|
input_tokens: int = 0
|
|
16
17
|
cached_tokens: int = 0
|
|
17
18
|
reasoning_tokens: int = 0
|
|
18
19
|
output_tokens: int = 0
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
|
|
21
|
+
# Context window tracking
|
|
22
|
+
context_window_size: int | None = None # Peak total_tokens seen (for context usage display)
|
|
23
|
+
context_limit: int | None = None # Model's context limit
|
|
24
|
+
|
|
21
25
|
throughput_tps: float | None = None
|
|
22
26
|
first_token_latency_ms: float | None = None
|
|
23
27
|
|
|
@@ -25,14 +29,39 @@ class Usage(BaseModel):
|
|
|
25
29
|
input_cost: float | None = None # Cost for non-cached input tokens
|
|
26
30
|
output_cost: float | None = None # Cost for output tokens (including reasoning)
|
|
27
31
|
cache_read_cost: float | None = None # Cost for cached tokens
|
|
28
|
-
total_cost: float | None = None # Total cost (input + output + cache_read)
|
|
29
32
|
currency: str = "USD" # Currency for cost display (USD or CNY)
|
|
30
33
|
|
|
34
|
+
@computed_field # type: ignore[prop-decorator]
|
|
35
|
+
@property
|
|
36
|
+
def total_tokens(self) -> int:
|
|
37
|
+
"""Total tokens computed from input + output tokens."""
|
|
38
|
+
return self.input_tokens + self.output_tokens
|
|
39
|
+
|
|
40
|
+
@computed_field # type: ignore[prop-decorator]
|
|
41
|
+
@property
|
|
42
|
+
def total_cost(self) -> float | None:
|
|
43
|
+
"""Total cost computed from input + output + cache_read costs."""
|
|
44
|
+
costs = [self.input_cost, self.output_cost, self.cache_read_cost]
|
|
45
|
+
non_none = [c for c in costs if c is not None]
|
|
46
|
+
return sum(non_none) if non_none else None
|
|
47
|
+
|
|
48
|
+
@computed_field # type: ignore[prop-decorator]
|
|
49
|
+
@property
|
|
50
|
+
def context_usage_percent(self) -> float | None:
|
|
51
|
+
"""Context usage percentage computed from context_window_size / context_limit."""
|
|
52
|
+
if self.context_limit is None or self.context_limit <= 0:
|
|
53
|
+
return None
|
|
54
|
+
if self.context_window_size is None:
|
|
55
|
+
return None
|
|
56
|
+
return (self.context_window_size / self.context_limit) * 100
|
|
57
|
+
|
|
31
58
|
|
|
32
59
|
class TodoItem(BaseModel):
|
|
60
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
61
|
+
|
|
33
62
|
content: str
|
|
34
63
|
status: TodoStatusType
|
|
35
|
-
|
|
64
|
+
active_form: str = Field(default="", alias="activeForm")
|
|
36
65
|
|
|
37
66
|
|
|
38
67
|
class TodoUIExtra(BaseModel):
|
|
@@ -40,43 +69,55 @@ class TodoUIExtra(BaseModel):
|
|
|
40
69
|
new_completed: list[str]
|
|
41
70
|
|
|
42
71
|
|
|
43
|
-
class ToolResultUIExtraType(str, Enum):
|
|
44
|
-
DIFF_TEXT = "diff_text"
|
|
45
|
-
TODO_LIST = "todo_list"
|
|
46
|
-
SESSION_ID = "session_id"
|
|
47
|
-
MERMAID_LINK = "mermaid_link"
|
|
48
|
-
TRUNCATION = "truncation"
|
|
49
|
-
SESSION_STATUS = "session_status"
|
|
50
|
-
|
|
51
|
-
|
|
52
72
|
class ToolSideEffect(str, Enum):
|
|
53
73
|
TODO_CHANGE = "todo_change"
|
|
54
74
|
|
|
55
75
|
|
|
76
|
+
# Discriminated union types for ToolResultUIExtra
|
|
77
|
+
class DiffTextUIExtra(BaseModel):
|
|
78
|
+
type: Literal["diff_text"] = "diff_text"
|
|
79
|
+
diff_text: str
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class TodoListUIExtra(BaseModel):
|
|
83
|
+
type: Literal["todo_list"] = "todo_list"
|
|
84
|
+
todo_list: TodoUIExtra
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class SessionIdUIExtra(BaseModel):
|
|
88
|
+
type: Literal["session_id"] = "session_id"
|
|
89
|
+
session_id: str
|
|
90
|
+
|
|
91
|
+
|
|
56
92
|
class MermaidLinkUIExtra(BaseModel):
|
|
93
|
+
type: Literal["mermaid_link"] = "mermaid_link"
|
|
57
94
|
link: str
|
|
58
95
|
line_count: int
|
|
59
96
|
|
|
60
97
|
|
|
61
98
|
class TruncationUIExtra(BaseModel):
|
|
99
|
+
type: Literal["truncation"] = "truncation"
|
|
62
100
|
saved_file_path: str
|
|
63
101
|
original_length: int
|
|
64
102
|
truncated_length: int
|
|
65
103
|
|
|
66
104
|
|
|
67
105
|
class SessionStatusUIExtra(BaseModel):
|
|
106
|
+
type: Literal["session_status"] = "session_status"
|
|
68
107
|
usage: "Usage"
|
|
69
108
|
task_count: int
|
|
109
|
+
by_model: list["TaskMetadata"] = []
|
|
70
110
|
|
|
71
111
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
112
|
+
ToolResultUIExtra = Annotated[
|
|
113
|
+
DiffTextUIExtra
|
|
114
|
+
| TodoListUIExtra
|
|
115
|
+
| SessionIdUIExtra
|
|
116
|
+
| MermaidLinkUIExtra
|
|
117
|
+
| TruncationUIExtra
|
|
118
|
+
| SessionStatusUIExtra,
|
|
119
|
+
Field(discriminator="type"),
|
|
120
|
+
]
|
|
80
121
|
|
|
81
122
|
|
|
82
123
|
class AtPatternParseResult(BaseModel):
|
|
@@ -240,6 +281,7 @@ class ToolResultItem(BaseModel):
|
|
|
240
281
|
ui_extra: ToolResultUIExtra | None = None # Extra data for UI display, e.g. diff render
|
|
241
282
|
images: list[ImageURLPart] | None = None
|
|
242
283
|
side_effects: list[ToolSideEffect] | None = None
|
|
284
|
+
task_metadata: "TaskMetadata | None" = None # Sub-agent task metadata for propagation to main agent
|
|
243
285
|
created_at: datetime = Field(default_factory=datetime.now)
|
|
244
286
|
|
|
245
287
|
|
|
@@ -255,13 +297,80 @@ class StreamErrorItem(BaseModel):
|
|
|
255
297
|
|
|
256
298
|
|
|
257
299
|
class ResponseMetadataItem(BaseModel):
|
|
300
|
+
"""Metadata for a single LLM response (turn-level)."""
|
|
301
|
+
|
|
258
302
|
response_id: str | None = None
|
|
259
303
|
usage: Usage | None = None
|
|
260
304
|
model_name: str = ""
|
|
261
305
|
provider: str | None = None # OpenRouter's provider name
|
|
262
306
|
task_duration_s: float | None = None
|
|
263
|
-
|
|
264
|
-
|
|
307
|
+
created_at: datetime = Field(default_factory=datetime.now)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
class TaskMetadata(BaseModel):
|
|
311
|
+
"""Base metadata for a task execution (used by both main and sub-agents)."""
|
|
312
|
+
|
|
313
|
+
usage: Usage | None = None
|
|
314
|
+
model_name: str = ""
|
|
315
|
+
provider: str | None = None
|
|
316
|
+
task_duration_s: float | None = None
|
|
317
|
+
|
|
318
|
+
@staticmethod
|
|
319
|
+
def aggregate_by_model(metadata_list: list["TaskMetadata"]) -> list["TaskMetadata"]:
|
|
320
|
+
"""Aggregate multiple TaskMetadata by (model_name, provider).
|
|
321
|
+
|
|
322
|
+
Returns a list sorted by total_cost descending.
|
|
323
|
+
|
|
324
|
+
Note: total_tokens and total_cost are now computed fields,
|
|
325
|
+
so we only accumulate the primary state fields here.
|
|
326
|
+
"""
|
|
327
|
+
aggregated: dict[tuple[str, str | None], TaskMetadata] = {}
|
|
328
|
+
|
|
329
|
+
for meta in metadata_list:
|
|
330
|
+
if not meta.usage:
|
|
331
|
+
continue
|
|
332
|
+
|
|
333
|
+
key = (meta.model_name, meta.provider)
|
|
334
|
+
usage = meta.usage
|
|
335
|
+
|
|
336
|
+
if key not in aggregated:
|
|
337
|
+
aggregated[key] = TaskMetadata(
|
|
338
|
+
model_name=meta.model_name,
|
|
339
|
+
provider=meta.provider,
|
|
340
|
+
usage=Usage(currency=usage.currency),
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
agg = aggregated[key]
|
|
344
|
+
if agg.usage is None:
|
|
345
|
+
continue
|
|
346
|
+
|
|
347
|
+
# Accumulate primary token fields (total_tokens is computed)
|
|
348
|
+
agg.usage.input_tokens += usage.input_tokens
|
|
349
|
+
agg.usage.cached_tokens += usage.cached_tokens
|
|
350
|
+
agg.usage.reasoning_tokens += usage.reasoning_tokens
|
|
351
|
+
agg.usage.output_tokens += usage.output_tokens
|
|
352
|
+
|
|
353
|
+
# Accumulate cost components (total_cost is computed)
|
|
354
|
+
if usage.input_cost is not None:
|
|
355
|
+
agg.usage.input_cost = (agg.usage.input_cost or 0.0) + usage.input_cost
|
|
356
|
+
if usage.output_cost is not None:
|
|
357
|
+
agg.usage.output_cost = (agg.usage.output_cost or 0.0) + usage.output_cost
|
|
358
|
+
if usage.cache_read_cost is not None:
|
|
359
|
+
agg.usage.cache_read_cost = (agg.usage.cache_read_cost or 0.0) + usage.cache_read_cost
|
|
360
|
+
|
|
361
|
+
# Sort by total_cost descending
|
|
362
|
+
return sorted(
|
|
363
|
+
aggregated.values(),
|
|
364
|
+
key=lambda m: m.usage.total_cost if m.usage and m.usage.total_cost else 0.0,
|
|
365
|
+
reverse=True,
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
class TaskMetadataItem(BaseModel):
|
|
370
|
+
"""Aggregated metadata for a complete task, stored in conversation history."""
|
|
371
|
+
|
|
372
|
+
main: TaskMetadata = Field(default_factory=TaskMetadata)
|
|
373
|
+
sub_agent_task_metadata: list[TaskMetadata] = Field(default_factory=lambda: list[TaskMetadata]())
|
|
265
374
|
created_at: datetime = Field(default_factory=datetime.now)
|
|
266
375
|
|
|
267
376
|
|
|
@@ -280,7 +389,14 @@ MessageItem = (
|
|
|
280
389
|
StreamItem = AssistantMessageDelta
|
|
281
390
|
|
|
282
391
|
ConversationItem = (
|
|
283
|
-
StartItem
|
|
392
|
+
StartItem
|
|
393
|
+
| InterruptItem
|
|
394
|
+
| StreamErrorItem
|
|
395
|
+
| StreamItem
|
|
396
|
+
| MessageItem
|
|
397
|
+
| ResponseMetadataItem
|
|
398
|
+
| TaskMetadataItem
|
|
399
|
+
| ToolCallStartItem
|
|
284
400
|
)
|
|
285
401
|
|
|
286
402
|
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass, field
|
|
4
|
-
from typing import Any, Callable
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Callable
|
|
5
5
|
|
|
6
6
|
from klaude_code.protocol import tools
|
|
7
7
|
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from klaude_code.protocol import model
|
|
10
|
+
|
|
8
11
|
AvailabilityPredicate = Callable[[str], bool]
|
|
9
12
|
PromptBuilder = Callable[[dict[str, Any]], str]
|
|
10
13
|
|
|
@@ -14,6 +17,7 @@ class SubAgentResult:
|
|
|
14
17
|
task_result: str
|
|
15
18
|
session_id: str
|
|
16
19
|
error: bool = False
|
|
20
|
+
task_metadata: model.TaskMetadata | None = None
|
|
17
21
|
|
|
18
22
|
|
|
19
23
|
def _default_prompt_builder(args: dict[str, Any]) -> str:
|
klaude_code/session/export.py
CHANGED
|
@@ -159,20 +159,35 @@ def _format_cost(cost: float, currency: str = "USD") -> str:
|
|
|
159
159
|
return f"{symbol}{cost:.4f}"
|
|
160
160
|
|
|
161
161
|
|
|
162
|
-
def
|
|
163
|
-
|
|
162
|
+
def _render_single_metadata(
|
|
163
|
+
metadata: model.TaskMetadata,
|
|
164
|
+
*,
|
|
165
|
+
indent: int = 0,
|
|
166
|
+
show_context: bool = True,
|
|
167
|
+
) -> str:
|
|
168
|
+
"""Render a single TaskMetadata block as HTML.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
metadata: The TaskMetadata to render.
|
|
172
|
+
indent: Number of spaces to indent (0 for main, 2 for sub-agents).
|
|
173
|
+
show_context: Whether to show context usage percent.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
HTML string for this metadata block.
|
|
177
|
+
"""
|
|
164
178
|
parts: list[str] = []
|
|
165
179
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
180
|
+
# Model Name [@ Provider]
|
|
181
|
+
model_parts = [f'<span class="metadata-model">{_escape_html(metadata.model_name)}</span>']
|
|
182
|
+
if metadata.provider:
|
|
183
|
+
provider = _escape_html(metadata.provider.lower().replace(" ", "-"))
|
|
169
184
|
model_parts.append(f'<span class="metadata-provider">@{provider}</span>')
|
|
170
185
|
|
|
171
186
|
parts.append("".join(model_parts))
|
|
172
187
|
|
|
173
188
|
# Stats
|
|
174
|
-
if
|
|
175
|
-
u =
|
|
189
|
+
if metadata.usage:
|
|
190
|
+
u = metadata.usage
|
|
176
191
|
# Input with cost
|
|
177
192
|
input_stat = f"input: {_format_token_count(u.input_tokens)}"
|
|
178
193
|
if u.input_cost is not None:
|
|
@@ -194,22 +209,39 @@ def _render_metadata_item(item: model.ResponseMetadataItem) -> str:
|
|
|
194
209
|
|
|
195
210
|
if u.reasoning_tokens > 0:
|
|
196
211
|
parts.append(f'<span class="metadata-stat">thinking: {_format_token_count(u.reasoning_tokens)}</span>')
|
|
197
|
-
if u.context_usage_percent is not None:
|
|
212
|
+
if show_context and u.context_usage_percent is not None:
|
|
198
213
|
parts.append(f'<span class="metadata-stat">context: {u.context_usage_percent:.1f}%</span>')
|
|
199
214
|
if u.throughput_tps is not None:
|
|
200
215
|
parts.append(f'<span class="metadata-stat">tps: {u.throughput_tps:.1f}</span>')
|
|
201
216
|
|
|
202
|
-
if
|
|
203
|
-
parts.append(f'<span class="metadata-stat">time: {
|
|
217
|
+
if metadata.task_duration_s is not None:
|
|
218
|
+
parts.append(f'<span class="metadata-stat">time: {metadata.task_duration_s:.1f}s</span>')
|
|
204
219
|
|
|
205
220
|
# Total cost
|
|
206
|
-
if
|
|
207
|
-
parts.append(
|
|
221
|
+
if metadata.usage is not None and metadata.usage.total_cost is not None:
|
|
222
|
+
parts.append(
|
|
223
|
+
f'<span class="metadata-stat">cost: {_format_cost(metadata.usage.total_cost, metadata.usage.currency)}</span>'
|
|
224
|
+
)
|
|
208
225
|
|
|
209
226
|
divider = '<span class="metadata-divider">/</span>'
|
|
210
227
|
joined_html = divider.join(parts)
|
|
211
228
|
|
|
212
|
-
|
|
229
|
+
indent_style = f' style="padding-left: {indent}em;"' if indent > 0 else ""
|
|
230
|
+
return f'<div class="metadata-line"{indent_style}>{joined_html}</div>'
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _render_metadata_item(item: model.TaskMetadataItem) -> str:
|
|
234
|
+
"""Render TaskMetadataItem including main agent and sub-agents."""
|
|
235
|
+
lines: list[str] = []
|
|
236
|
+
|
|
237
|
+
# Main agent metadata
|
|
238
|
+
lines.append(_render_single_metadata(item.main, indent=0, show_context=True))
|
|
239
|
+
|
|
240
|
+
# Sub-agent metadata with indent
|
|
241
|
+
for sub in item.sub_agent_task_metadata:
|
|
242
|
+
lines.append(_render_single_metadata(sub, indent=1, show_context=False))
|
|
243
|
+
|
|
244
|
+
return f'<div class="response-metadata">{"".join(lines)}</div>'
|
|
213
245
|
|
|
214
246
|
|
|
215
247
|
def _render_assistant_message(index: int, content: str, timestamp: datetime) -> str:
|
|
@@ -336,11 +368,9 @@ def _render_diff_block(diff: str) -> str:
|
|
|
336
368
|
|
|
337
369
|
|
|
338
370
|
def _get_diff_text(ui_extra: model.ToolResultUIExtra | None) -> str | None:
|
|
339
|
-
if ui_extra
|
|
340
|
-
return
|
|
341
|
-
|
|
342
|
-
return None
|
|
343
|
-
return ui_extra.diff_text
|
|
371
|
+
if isinstance(ui_extra, model.DiffTextUIExtra):
|
|
372
|
+
return ui_extra.diff_text
|
|
373
|
+
return None
|
|
344
374
|
|
|
345
375
|
|
|
346
376
|
def _get_mermaid_link_html(
|
|
@@ -355,9 +385,7 @@ def _get_mermaid_link_html(
|
|
|
355
385
|
else:
|
|
356
386
|
code = ""
|
|
357
387
|
|
|
358
|
-
if not code and (
|
|
359
|
-
ui_extra is None or ui_extra.type != model.ToolResultUIExtraType.MERMAID_LINK or not ui_extra.mermaid_link
|
|
360
|
-
):
|
|
388
|
+
if not code and not isinstance(ui_extra, model.MermaidLinkUIExtra):
|
|
361
389
|
return None
|
|
362
390
|
|
|
363
391
|
# Prepare code for rendering and copy
|
|
@@ -376,11 +404,7 @@ def _get_mermaid_link_html(
|
|
|
376
404
|
f'<button type="button" class="copy-mermaid-btn" data-code="{escaped_code}" title="Copy Mermaid Code">Copy Code</button>'
|
|
377
405
|
)
|
|
378
406
|
|
|
379
|
-
link = (
|
|
380
|
-
ui_extra.mermaid_link.link
|
|
381
|
-
if (ui_extra and ui_extra.type == model.ToolResultUIExtraType.MERMAID_LINK and ui_extra.mermaid_link)
|
|
382
|
-
else None
|
|
383
|
-
)
|
|
407
|
+
link = ui_extra.link if isinstance(ui_extra, model.MermaidLinkUIExtra) else None
|
|
384
408
|
|
|
385
409
|
if link:
|
|
386
410
|
link_url = _escape_html(link)
|
|
@@ -544,7 +568,7 @@ def _build_messages_html(
|
|
|
544
568
|
elif isinstance(item, model.AssistantMessageItem):
|
|
545
569
|
assistant_counter += 1
|
|
546
570
|
blocks.append(_render_assistant_message(assistant_counter, item.content or "", item.created_at))
|
|
547
|
-
elif isinstance(item, model.
|
|
571
|
+
elif isinstance(item, model.TaskMetadataItem):
|
|
548
572
|
blocks.append(_render_metadata_item(item))
|
|
549
573
|
elif isinstance(item, model.DeveloperMessageItem):
|
|
550
574
|
content = _escape_html(item.content or "")
|
klaude_code/session/session.py
CHANGED
|
@@ -5,7 +5,7 @@ from collections.abc import Iterable, Sequence
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from typing import ClassVar
|
|
7
7
|
|
|
8
|
-
from pydantic import BaseModel, Field
|
|
8
|
+
from pydantic import BaseModel, Field, PrivateAttr
|
|
9
9
|
|
|
10
10
|
from klaude_code.protocol import events, model
|
|
11
11
|
|
|
@@ -19,8 +19,6 @@ class Session(BaseModel):
|
|
|
19
19
|
file_tracker: dict[str, float] = Field(default_factory=dict)
|
|
20
20
|
# Todo list for the session
|
|
21
21
|
todos: list[model.TodoItem] = Field(default_factory=list) # pyright: ignore[reportUnknownVariableType]
|
|
22
|
-
# Messages count, redundant state for performance optimization to avoid reading entire jsonl file
|
|
23
|
-
messages_count: int = Field(default=0)
|
|
24
22
|
# Model name used for this session
|
|
25
23
|
# Used in list method SessionMetaBrief
|
|
26
24
|
model_name: str | None = None
|
|
@@ -33,6 +31,27 @@ class Session(BaseModel):
|
|
|
33
31
|
need_todo_empty_cooldown_counter: int = Field(exclude=True, default=0)
|
|
34
32
|
need_todo_not_used_cooldown_counter: int = Field(exclude=True, default=0)
|
|
35
33
|
|
|
34
|
+
# Cached messages count (computed property)
|
|
35
|
+
_messages_count_cache: int | None = PrivateAttr(default=None)
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def messages_count(self) -> int:
|
|
39
|
+
"""Count of user and assistant messages in conversation history.
|
|
40
|
+
|
|
41
|
+
This is a cached property that is invalidated when append_history is called.
|
|
42
|
+
"""
|
|
43
|
+
if self._messages_count_cache is None:
|
|
44
|
+
self._messages_count_cache = sum(
|
|
45
|
+
1
|
|
46
|
+
for it in self.conversation_history
|
|
47
|
+
if isinstance(it, (model.UserMessageItem, model.AssistantMessageItem))
|
|
48
|
+
)
|
|
49
|
+
return self._messages_count_cache
|
|
50
|
+
|
|
51
|
+
def _invalidate_messages_count_cache(self) -> None:
|
|
52
|
+
"""Invalidate the cached messages count."""
|
|
53
|
+
self._messages_count_cache = None
|
|
54
|
+
|
|
36
55
|
# Internal: mapping for (de)serialization of conversation items
|
|
37
56
|
_TypeMap: ClassVar[dict[str, type[BaseModel]]] = {
|
|
38
57
|
# Messages
|
|
@@ -50,7 +69,7 @@ class Session(BaseModel):
|
|
|
50
69
|
"AssistantMessageDelta": model.AssistantMessageDelta,
|
|
51
70
|
"StartItem": model.StartItem,
|
|
52
71
|
"StreamErrorItem": model.StreamErrorItem,
|
|
53
|
-
"
|
|
72
|
+
"TaskMetadataItem": model.TaskMetadataItem,
|
|
54
73
|
"InterruptItem": model.InterruptItem,
|
|
55
74
|
}
|
|
56
75
|
|
|
@@ -109,7 +128,6 @@ class Session(BaseModel):
|
|
|
109
128
|
loaded_memory = list(raw.get("loaded_memory", []))
|
|
110
129
|
created_at = float(raw.get("created_at", time.time()))
|
|
111
130
|
updated_at = float(raw.get("updated_at", created_at))
|
|
112
|
-
messages_count = int(raw.get("messages_count", 0))
|
|
113
131
|
model_name = raw.get("model_name")
|
|
114
132
|
|
|
115
133
|
sess = Session(
|
|
@@ -121,7 +139,6 @@ class Session(BaseModel):
|
|
|
121
139
|
loaded_memory=loaded_memory,
|
|
122
140
|
created_at=created_at,
|
|
123
141
|
updated_at=updated_at,
|
|
124
|
-
messages_count=messages_count,
|
|
125
142
|
model_name=model_name,
|
|
126
143
|
)
|
|
127
144
|
|
|
@@ -154,10 +171,7 @@ class Session(BaseModel):
|
|
|
154
171
|
# Best-effort load; skip malformed lines
|
|
155
172
|
continue
|
|
156
173
|
sess.conversation_history = history
|
|
157
|
-
#
|
|
158
|
-
sess.messages_count = sum(
|
|
159
|
-
1 for it in history if isinstance(it, (model.UserMessageItem, model.AssistantMessageItem))
|
|
160
|
-
)
|
|
174
|
+
# messages_count is now a computed property, no need to set it
|
|
161
175
|
|
|
162
176
|
return sess
|
|
163
177
|
|
|
@@ -190,10 +204,8 @@ class Session(BaseModel):
|
|
|
190
204
|
def append_history(self, items: Sequence[model.ConversationItem]):
|
|
191
205
|
# Append to in-memory history
|
|
192
206
|
self.conversation_history.extend(items)
|
|
193
|
-
#
|
|
194
|
-
self.
|
|
195
|
-
1 for it in items if isinstance(it, (model.UserMessageItem, model.AssistantMessageItem))
|
|
196
|
-
)
|
|
207
|
+
# Invalidate messages count cache
|
|
208
|
+
self._invalidate_messages_count_cache()
|
|
197
209
|
|
|
198
210
|
# Incrementally persist to JSONL under messages directory
|
|
199
211
|
messages_dir = self._messages_dir()
|
|
@@ -295,8 +307,8 @@ class Session(BaseModel):
|
|
|
295
307
|
content=ri.content,
|
|
296
308
|
session_id=self.id,
|
|
297
309
|
)
|
|
298
|
-
case model.
|
|
299
|
-
yield events.
|
|
310
|
+
case model.TaskMetadataItem() as mt:
|
|
311
|
+
yield events.TaskMetadataEvent(
|
|
300
312
|
session_id=self.id,
|
|
301
313
|
metadata=mt,
|
|
302
314
|
)
|
|
@@ -309,6 +321,11 @@ class Session(BaseModel):
|
|
|
309
321
|
session_id=self.id,
|
|
310
322
|
item=dm,
|
|
311
323
|
)
|
|
324
|
+
case model.StreamErrorItem() as se:
|
|
325
|
+
yield events.ErrorEvent(
|
|
326
|
+
error_message=se.error,
|
|
327
|
+
can_retry=False,
|
|
328
|
+
)
|
|
312
329
|
case _:
|
|
313
330
|
continue
|
|
314
331
|
prev_item = it
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
rel="stylesheet"
|
|
22
22
|
/>
|
|
23
23
|
<link
|
|
24
|
-
href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;700&family=IBM+Plex+Sans:wght@400;500;700&display=swap"
|
|
24
|
+
href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,400;0,500;0,700;1,400;1,500;1,700&family=IBM+Plex+Sans:ital,wght@0,400;0,500;0,700;1,400;1,500;1,700&display=swap"
|
|
25
25
|
rel="stylesheet"
|
|
26
26
|
/>
|
|
27
27
|
<style>
|
|
@@ -411,6 +411,9 @@
|
|
|
411
411
|
font-size: var(--font-size-xs);
|
|
412
412
|
color: var(--text-dim);
|
|
413
413
|
border-left: 2px solid transparent;
|
|
414
|
+
display: flex;
|
|
415
|
+
flex-direction: column;
|
|
416
|
+
gap: 8px;
|
|
414
417
|
}
|
|
415
418
|
.metadata-line {
|
|
416
419
|
display: flex;
|
|
@@ -22,11 +22,7 @@ def build_repl_status_snapshot(agent: "Agent | None", update_message: str | None
|
|
|
22
22
|
tool_calls = 0
|
|
23
23
|
|
|
24
24
|
if agent is not None:
|
|
25
|
-
|
|
26
|
-
if profile is not None:
|
|
27
|
-
model_name = profile.llm_client.model_name or ""
|
|
28
|
-
else:
|
|
29
|
-
model_name = "N/A"
|
|
25
|
+
model_name = agent.profile.llm_client.model_name or ""
|
|
30
26
|
|
|
31
27
|
history = agent.session.conversation_history
|
|
32
28
|
for item in history:
|