klaude-code 2.5.1__py3-none-any.whl → 2.5.3__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/.DS_Store +0 -0
- klaude_code/cli/auth_cmd.py +2 -13
- klaude_code/cli/cost_cmd.py +10 -10
- klaude_code/cli/list_model.py +8 -0
- klaude_code/cli/main.py +41 -8
- klaude_code/cli/session_cmd.py +2 -11
- klaude_code/config/assets/builtin_config.yaml +45 -26
- klaude_code/config/config.py +30 -7
- klaude_code/config/model_matcher.py +3 -3
- klaude_code/config/sub_agent_model_helper.py +1 -1
- klaude_code/const.py +2 -1
- klaude_code/core/agent_profile.py +1 -0
- klaude_code/core/executor.py +4 -0
- klaude_code/core/loaded_skills.py +36 -0
- klaude_code/core/tool/context.py +1 -3
- klaude_code/core/tool/file/edit_tool.py +1 -1
- klaude_code/core/tool/file/read_tool.py +2 -2
- klaude_code/core/tool/file/write_tool.py +1 -1
- klaude_code/core/turn.py +19 -7
- klaude_code/llm/anthropic/client.py +97 -60
- klaude_code/llm/anthropic/input.py +20 -9
- klaude_code/llm/google/client.py +223 -148
- klaude_code/llm/google/input.py +44 -36
- klaude_code/llm/openai_compatible/stream.py +109 -99
- klaude_code/llm/openrouter/reasoning.py +4 -29
- klaude_code/llm/partial_message.py +2 -32
- klaude_code/llm/responses/client.py +99 -81
- klaude_code/llm/responses/input.py +11 -25
- klaude_code/llm/stream_parts.py +94 -0
- klaude_code/log.py +57 -0
- klaude_code/protocol/events/system.py +3 -0
- klaude_code/protocol/llm_param.py +1 -0
- klaude_code/session/export.py +259 -91
- klaude_code/session/templates/export_session.html +141 -59
- klaude_code/skill/.DS_Store +0 -0
- klaude_code/skill/assets/.DS_Store +0 -0
- klaude_code/skill/loader.py +1 -0
- klaude_code/tui/command/fork_session_cmd.py +14 -23
- klaude_code/tui/command/model_picker.py +2 -17
- klaude_code/tui/command/refresh_cmd.py +2 -0
- klaude_code/tui/command/resume_cmd.py +2 -18
- klaude_code/tui/command/sub_agent_model_cmd.py +5 -19
- klaude_code/tui/command/thinking_cmd.py +2 -14
- klaude_code/tui/components/common.py +1 -1
- klaude_code/tui/components/metadata.py +22 -21
- klaude_code/tui/components/rich/markdown.py +8 -0
- klaude_code/tui/components/rich/quote.py +36 -8
- klaude_code/tui/components/rich/theme.py +2 -0
- klaude_code/tui/components/welcome.py +32 -0
- klaude_code/tui/input/prompt_toolkit.py +3 -1
- klaude_code/tui/machine.py +19 -1
- klaude_code/tui/renderer.py +3 -4
- klaude_code/tui/terminal/selector.py +174 -31
- {klaude_code-2.5.1.dist-info → klaude_code-2.5.3.dist-info}/METADATA +1 -1
- {klaude_code-2.5.1.dist-info → klaude_code-2.5.3.dist-info}/RECORD +57 -53
- klaude_code/skill/assets/jj-workspace/SKILL.md +0 -20
- {klaude_code-2.5.1.dist-info → klaude_code-2.5.3.dist-info}/WHEEL +0 -0
- {klaude_code-2.5.1.dist-info → klaude_code-2.5.3.dist-info}/entry_points.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import json
|
|
2
2
|
from collections.abc import AsyncGenerator
|
|
3
|
-
from typing import TYPE_CHECKING,
|
|
3
|
+
from typing import TYPE_CHECKING, override
|
|
4
4
|
|
|
5
5
|
import httpx
|
|
6
6
|
import openai
|
|
@@ -11,9 +11,14 @@ from openai.types.responses.response_create_params import ResponseCreateParamsSt
|
|
|
11
11
|
from klaude_code.const import LLM_HTTP_TIMEOUT_CONNECT, LLM_HTTP_TIMEOUT_READ, LLM_HTTP_TIMEOUT_TOTAL
|
|
12
12
|
from klaude_code.llm.client import LLMClientABC, LLMStreamABC
|
|
13
13
|
from klaude_code.llm.input_common import apply_config_defaults
|
|
14
|
-
from klaude_code.llm.partial_message import degrade_thinking_to_text
|
|
15
14
|
from klaude_code.llm.registry import register
|
|
16
15
|
from klaude_code.llm.responses.input import convert_history_to_input, convert_tool_schema
|
|
16
|
+
from klaude_code.llm.stream_parts import (
|
|
17
|
+
append_text_part,
|
|
18
|
+
append_thinking_text_part,
|
|
19
|
+
build_partial_message,
|
|
20
|
+
build_partial_parts,
|
|
21
|
+
)
|
|
17
22
|
from klaude_code.llm.usage import MetadataTracker, error_llm_stream
|
|
18
23
|
from klaude_code.log import DebugType, log_debug
|
|
19
24
|
from klaude_code.protocol import llm_param, message, model
|
|
@@ -58,68 +63,82 @@ def build_payload(param: llm_param.LLMCallParameter) -> ResponseCreateParamsStre
|
|
|
58
63
|
|
|
59
64
|
|
|
60
65
|
class ResponsesStreamStateManager:
|
|
61
|
-
"""Manages streaming state for Responses API and provides partial message access.
|
|
66
|
+
"""Manages streaming state for Responses API and provides partial message access.
|
|
67
|
+
|
|
68
|
+
Accumulates parts directly during streaming to support get_partial_message()
|
|
69
|
+
for cancellation scenarios. Merges consecutive text parts of the same type.
|
|
70
|
+
Each reasoning summary is kept as a separate ThinkingTextPart.
|
|
71
|
+
"""
|
|
62
72
|
|
|
63
73
|
def __init__(self, model_id: str) -> None:
|
|
64
74
|
self.model_id = model_id
|
|
65
75
|
self.response_id: str | None = None
|
|
66
|
-
self.stage: Literal["waiting", "thinking", "assistant", "tool"] = "waiting"
|
|
67
|
-
self.accumulated_thinking: list[str] = []
|
|
68
|
-
self.accumulated_text: list[str] = []
|
|
69
|
-
self.pending_signature: str | None = None
|
|
70
76
|
self.assistant_parts: list[message.Part] = []
|
|
71
77
|
self.stop_reason: model.StopReason | None = None
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
self._new_thinking_part: bool = True # Start fresh for first thinking part
|
|
79
|
+
self._summary_count: int = 0 # Track number of summary parts seen
|
|
80
|
+
|
|
81
|
+
def start_new_thinking_part(self) -> bool:
|
|
82
|
+
"""Mark that the next thinking text should create a new ThinkingTextPart.
|
|
83
|
+
|
|
84
|
+
Returns True if this is not the first summary part (needs separator).
|
|
85
|
+
"""
|
|
86
|
+
self._new_thinking_part = True
|
|
87
|
+
needs_separator = self._summary_count > 0
|
|
88
|
+
self._summary_count += 1
|
|
89
|
+
return needs_separator
|
|
90
|
+
|
|
91
|
+
def append_thinking_text(self, text: str) -> None:
|
|
92
|
+
"""Append thinking text, merging with previous ThinkingTextPart if in same summary."""
|
|
93
|
+
if (
|
|
94
|
+
append_thinking_text_part(
|
|
95
|
+
self.assistant_parts,
|
|
96
|
+
text,
|
|
97
|
+
model_id=self.model_id,
|
|
98
|
+
force_new=self._new_thinking_part,
|
|
81
99
|
)
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
self.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
100
|
+
is not None
|
|
101
|
+
):
|
|
102
|
+
self._new_thinking_part = False
|
|
103
|
+
|
|
104
|
+
def append_text(self, text: str) -> None:
|
|
105
|
+
"""Append text, merging with previous TextPart if possible."""
|
|
106
|
+
append_text_part(self.assistant_parts, text)
|
|
107
|
+
|
|
108
|
+
def append_thinking_signature(self, signature: str) -> None:
|
|
109
|
+
"""Append a ThinkingSignaturePart after the current part."""
|
|
110
|
+
self.assistant_parts.append(
|
|
111
|
+
message.ThinkingSignaturePart(
|
|
112
|
+
signature=signature,
|
|
113
|
+
model_id=self.model_id,
|
|
114
|
+
format="openai-responses",
|
|
90
115
|
)
|
|
91
|
-
|
|
116
|
+
)
|
|
92
117
|
|
|
93
|
-
def
|
|
94
|
-
"""
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
118
|
+
def append_tool_call(self, call_id: str, item_id: str | None, name: str, arguments_json: str) -> None:
|
|
119
|
+
"""Append a ToolCallPart."""
|
|
120
|
+
self.assistant_parts.append(
|
|
121
|
+
message.ToolCallPart(
|
|
122
|
+
call_id=call_id,
|
|
123
|
+
id=item_id,
|
|
124
|
+
tool_name=name,
|
|
125
|
+
arguments_json=arguments_json,
|
|
126
|
+
)
|
|
127
|
+
)
|
|
99
128
|
|
|
100
|
-
def
|
|
101
|
-
"""
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
129
|
+
def get_partial_parts(self) -> list[message.Part]:
|
|
130
|
+
"""Get accumulated parts excluding tool calls, with thinking degraded.
|
|
131
|
+
|
|
132
|
+
Filters out ToolCallPart and applies degrade_thinking_to_text.
|
|
133
|
+
"""
|
|
134
|
+
return build_partial_parts(self.assistant_parts)
|
|
105
135
|
|
|
106
136
|
def get_partial_message(self) -> message.AssistantMessage | None:
|
|
107
|
-
"""Build a partial AssistantMessage from accumulated state.
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
continue
|
|
113
|
-
filtered_parts.append(part)
|
|
114
|
-
|
|
115
|
-
filtered_parts = degrade_thinking_to_text(filtered_parts)
|
|
116
|
-
if not filtered_parts:
|
|
117
|
-
return None
|
|
118
|
-
return message.AssistantMessage(
|
|
119
|
-
parts=filtered_parts,
|
|
120
|
-
response_id=self.response_id,
|
|
121
|
-
stop_reason="aborted",
|
|
122
|
-
)
|
|
137
|
+
"""Build a partial AssistantMessage from accumulated state.
|
|
138
|
+
|
|
139
|
+
Returns None if no content has been accumulated yet.
|
|
140
|
+
"""
|
|
141
|
+
return build_partial_message(self.assistant_parts, response_id=self.response_id)
|
|
123
142
|
|
|
124
143
|
|
|
125
144
|
async def parse_responses_stream(
|
|
@@ -157,24 +176,28 @@ async def parse_responses_stream(
|
|
|
157
176
|
match event:
|
|
158
177
|
case responses.ResponseCreatedEvent() as event:
|
|
159
178
|
state.response_id = event.response.id
|
|
179
|
+
case responses.ResponseReasoningSummaryPartAddedEvent():
|
|
180
|
+
# New reasoning summary part started, ensure it becomes a new ThinkingTextPart
|
|
181
|
+
needs_separator = state.start_new_thinking_part()
|
|
182
|
+
if needs_separator:
|
|
183
|
+
# Add blank lines between summary parts for visual separation
|
|
184
|
+
yield message.ThinkingTextDelta(content=" \n \n", response_id=state.response_id)
|
|
160
185
|
case responses.ResponseReasoningSummaryTextDeltaEvent() as event:
|
|
161
186
|
if event.delta:
|
|
162
187
|
metadata_tracker.record_token()
|
|
163
|
-
|
|
164
|
-
state.flush_text()
|
|
165
|
-
state.stage = "thinking"
|
|
166
|
-
state.accumulated_thinking.append(event.delta)
|
|
188
|
+
state.append_thinking_text(event.delta)
|
|
167
189
|
yield message.ThinkingTextDelta(content=event.delta, response_id=state.response_id)
|
|
168
190
|
case responses.ResponseReasoningSummaryTextDoneEvent() as event:
|
|
169
|
-
if
|
|
170
|
-
|
|
191
|
+
# Fallback: if no delta was received but done has full text, use it
|
|
192
|
+
if event.text:
|
|
193
|
+
# Check if we already have content for this summary by seeing if last part matches
|
|
194
|
+
last_part = state.assistant_parts[-1] if state.assistant_parts else None
|
|
195
|
+
if not isinstance(last_part, message.ThinkingTextPart) or not last_part.text:
|
|
196
|
+
state.append_thinking_text(event.text)
|
|
171
197
|
case responses.ResponseTextDeltaEvent() as event:
|
|
172
198
|
if event.delta:
|
|
173
199
|
metadata_tracker.record_token()
|
|
174
|
-
|
|
175
|
-
state.flush_thinking()
|
|
176
|
-
state.stage = "assistant"
|
|
177
|
-
state.accumulated_text.append(event.delta)
|
|
200
|
+
state.append_text(event.delta)
|
|
178
201
|
yield message.AssistantTextDelta(content=event.delta, response_id=state.response_id)
|
|
179
202
|
case responses.ResponseOutputItemAddedEvent() as event:
|
|
180
203
|
if isinstance(event.item, responses.ResponseFunctionToolCall):
|
|
@@ -188,30 +211,23 @@ async def parse_responses_stream(
|
|
|
188
211
|
match event.item:
|
|
189
212
|
case responses.ResponseReasoningItem() as item:
|
|
190
213
|
if item.encrypted_content:
|
|
191
|
-
state.
|
|
214
|
+
state.append_thinking_signature(item.encrypted_content)
|
|
192
215
|
case responses.ResponseOutputMessage() as item:
|
|
193
|
-
if
|
|
216
|
+
# Fallback: if no text delta was received, extract from final message
|
|
217
|
+
has_text = any(isinstance(p, message.TextPart) for p in state.assistant_parts)
|
|
218
|
+
if not has_text:
|
|
194
219
|
text_content = "\n".join(
|
|
195
|
-
|
|
196
|
-
part.text
|
|
197
|
-
for part in item.content
|
|
198
|
-
if isinstance(part, responses.ResponseOutputText)
|
|
199
|
-
]
|
|
220
|
+
part.text for part in item.content if isinstance(part, responses.ResponseOutputText)
|
|
200
221
|
)
|
|
201
222
|
if text_content:
|
|
202
|
-
state.
|
|
223
|
+
state.append_text(text_content)
|
|
203
224
|
case responses.ResponseFunctionToolCall() as item:
|
|
204
225
|
metadata_tracker.record_token()
|
|
205
|
-
state.
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
call_id=item.call_id,
|
|
211
|
-
id=item.id,
|
|
212
|
-
tool_name=item.name,
|
|
213
|
-
arguments_json=item.arguments.strip(),
|
|
214
|
-
)
|
|
226
|
+
state.append_tool_call(
|
|
227
|
+
call_id=item.call_id,
|
|
228
|
+
item_id=item.id,
|
|
229
|
+
name=item.name,
|
|
230
|
+
arguments_json=item.arguments.strip(),
|
|
215
231
|
)
|
|
216
232
|
case _:
|
|
217
233
|
pass
|
|
@@ -254,10 +270,12 @@ async def parse_responses_stream(
|
|
|
254
270
|
)
|
|
255
271
|
except (openai.OpenAIError, httpx.HTTPError) as e:
|
|
256
272
|
yield message.StreamErrorItem(error=f"{e.__class__.__name__} {e!s}")
|
|
273
|
+
state.stop_reason = "error"
|
|
257
274
|
|
|
258
|
-
parts = state.flush_all()
|
|
259
275
|
metadata_tracker.set_response_id(state.response_id)
|
|
260
276
|
metadata = metadata_tracker.finalize()
|
|
277
|
+
# On error, use partial parts (excluding incomplete tool calls) for potential prefill on retry
|
|
278
|
+
parts = state.get_partial_parts() if state.stop_reason == "error" else list(state.assistant_parts)
|
|
261
279
|
yield message.AssistantMessage(
|
|
262
280
|
parts=parts,
|
|
263
281
|
response_id=state.response_id,
|
|
@@ -80,8 +80,6 @@ def convert_history_to_input(
|
|
|
80
80
|
"""Convert a list of messages to response input params."""
|
|
81
81
|
items: list[responses.ResponseInputItemParam] = []
|
|
82
82
|
|
|
83
|
-
degraded_thinking_texts: list[str] = []
|
|
84
|
-
|
|
85
83
|
for msg, attachment in attach_developer_messages(history):
|
|
86
84
|
match msg:
|
|
87
85
|
case message.SystemMessage():
|
|
@@ -116,12 +114,19 @@ def convert_history_to_input(
|
|
|
116
114
|
case message.ToolResultMessage():
|
|
117
115
|
items.append(_build_tool_result_item(msg, attachment))
|
|
118
116
|
case message.AssistantMessage():
|
|
119
|
-
assistant_text_parts: list[responses.
|
|
117
|
+
assistant_text_parts: list[responses.ResponseOutputTextParam] = []
|
|
120
118
|
pending_thinking_text: str | None = None
|
|
121
119
|
pending_signature: str | None = None
|
|
122
120
|
native_thinking_parts, degraded_for_message = split_thinking_parts(msg, model_name)
|
|
123
121
|
native_thinking_ids = {id(part) for part in native_thinking_parts}
|
|
124
|
-
|
|
122
|
+
if degraded_for_message:
|
|
123
|
+
degraded_text = "<thinking>\n" + "\n".join(degraded_for_message) + "\n</thinking>"
|
|
124
|
+
assistant_text_parts.append(
|
|
125
|
+
cast(
|
|
126
|
+
responses.ResponseOutputTextParam,
|
|
127
|
+
{"type": "output_text", "text": degraded_text},
|
|
128
|
+
)
|
|
129
|
+
)
|
|
125
130
|
|
|
126
131
|
def flush_text() -> None:
|
|
127
132
|
nonlocal assistant_text_parts
|
|
@@ -164,8 +169,8 @@ def convert_history_to_input(
|
|
|
164
169
|
if isinstance(part, message.TextPart):
|
|
165
170
|
assistant_text_parts.append(
|
|
166
171
|
cast(
|
|
167
|
-
responses.
|
|
168
|
-
{"type": "
|
|
172
|
+
responses.ResponseOutputTextParam,
|
|
173
|
+
{"type": "output_text", "text": part.text},
|
|
169
174
|
)
|
|
170
175
|
)
|
|
171
176
|
elif isinstance(part, message.ToolCallPart):
|
|
@@ -188,25 +193,6 @@ def convert_history_to_input(
|
|
|
188
193
|
case _:
|
|
189
194
|
continue
|
|
190
195
|
|
|
191
|
-
if degraded_thinking_texts:
|
|
192
|
-
degraded_item = cast(
|
|
193
|
-
responses.ResponseInputItemParam,
|
|
194
|
-
{
|
|
195
|
-
"type": "message",
|
|
196
|
-
"role": "assistant",
|
|
197
|
-
"content": [
|
|
198
|
-
cast(
|
|
199
|
-
responses.ResponseInputContentParam,
|
|
200
|
-
{
|
|
201
|
-
"type": "input_text",
|
|
202
|
-
"text": "<thinking>\n" + "\n".join(degraded_thinking_texts) + "\n</thinking>",
|
|
203
|
-
},
|
|
204
|
-
)
|
|
205
|
-
],
|
|
206
|
-
},
|
|
207
|
-
)
|
|
208
|
-
items.insert(0, degraded_item)
|
|
209
|
-
|
|
210
196
|
return items
|
|
211
197
|
|
|
212
198
|
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import MutableSequence
|
|
4
|
+
|
|
5
|
+
from klaude_code.protocol import message
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def append_text_part(parts: MutableSequence[message.Part], text: str) -> int | None:
|
|
9
|
+
if not text:
|
|
10
|
+
return None
|
|
11
|
+
|
|
12
|
+
if parts:
|
|
13
|
+
last = parts[-1]
|
|
14
|
+
if isinstance(last, message.TextPart):
|
|
15
|
+
parts[-1] = message.TextPart(text=last.text + text)
|
|
16
|
+
return len(parts) - 1
|
|
17
|
+
|
|
18
|
+
parts.append(message.TextPart(text=text))
|
|
19
|
+
return len(parts) - 1
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def append_thinking_text_part(
|
|
23
|
+
parts: MutableSequence[message.Part],
|
|
24
|
+
text: str,
|
|
25
|
+
*,
|
|
26
|
+
model_id: str,
|
|
27
|
+
force_new: bool = False,
|
|
28
|
+
) -> int | None:
|
|
29
|
+
if not text:
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
if not force_new and parts:
|
|
33
|
+
last = parts[-1]
|
|
34
|
+
if isinstance(last, message.ThinkingTextPart):
|
|
35
|
+
parts[-1] = message.ThinkingTextPart(
|
|
36
|
+
text=last.text + text,
|
|
37
|
+
model_id=model_id,
|
|
38
|
+
)
|
|
39
|
+
return len(parts) - 1
|
|
40
|
+
|
|
41
|
+
parts.append(message.ThinkingTextPart(text=text, model_id=model_id))
|
|
42
|
+
return len(parts) - 1
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def degrade_thinking_to_text(parts: list[message.Part]) -> list[message.Part]:
|
|
46
|
+
"""Degrade thinking parts into a regular TextPart.
|
|
47
|
+
|
|
48
|
+
Some providers require thinking signatures/encrypted content to be echoed back
|
|
49
|
+
for subsequent calls. During interruption we cannot reliably determine whether
|
|
50
|
+
we have a complete signature, so we persist thinking as plain text instead.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
thinking_texts: list[str] = []
|
|
54
|
+
non_thinking_parts: list[message.Part] = []
|
|
55
|
+
|
|
56
|
+
for part in parts:
|
|
57
|
+
if isinstance(part, message.ThinkingTextPart):
|
|
58
|
+
text = part.text
|
|
59
|
+
if text and text.strip():
|
|
60
|
+
thinking_texts.append(text)
|
|
61
|
+
continue
|
|
62
|
+
if isinstance(part, message.ThinkingSignaturePart):
|
|
63
|
+
continue
|
|
64
|
+
non_thinking_parts.append(part)
|
|
65
|
+
|
|
66
|
+
if not thinking_texts:
|
|
67
|
+
return non_thinking_parts
|
|
68
|
+
|
|
69
|
+
joined = "\n".join(thinking_texts).strip()
|
|
70
|
+
thinking_block = f"<thinking>\n{joined}\n</thinking>"
|
|
71
|
+
if non_thinking_parts:
|
|
72
|
+
thinking_block += "\n\n"
|
|
73
|
+
|
|
74
|
+
return [message.TextPart(text=thinking_block), *non_thinking_parts]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def build_partial_parts(parts: list[message.Part]) -> list[message.Part]:
|
|
78
|
+
filtered_parts: list[message.Part] = [p for p in parts if not isinstance(p, message.ToolCallPart)]
|
|
79
|
+
return degrade_thinking_to_text(filtered_parts)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def build_partial_message(
|
|
83
|
+
parts: list[message.Part],
|
|
84
|
+
*,
|
|
85
|
+
response_id: str | None,
|
|
86
|
+
) -> message.AssistantMessage | None:
|
|
87
|
+
partial_parts = build_partial_parts(parts)
|
|
88
|
+
if not partial_parts:
|
|
89
|
+
return None
|
|
90
|
+
return message.AssistantMessage(
|
|
91
|
+
parts=partial_parts,
|
|
92
|
+
response_id=response_id,
|
|
93
|
+
stop_reason="aborted",
|
|
94
|
+
)
|
klaude_code/log.py
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import gzip
|
|
2
|
+
import json
|
|
2
3
|
import logging
|
|
3
4
|
import os
|
|
4
5
|
import shutil
|
|
5
6
|
import subprocess
|
|
7
|
+
from base64 import b64encode
|
|
6
8
|
from collections.abc import Iterable
|
|
7
9
|
from datetime import datetime, timedelta
|
|
8
10
|
from enum import Enum
|
|
9
11
|
from logging.handlers import RotatingFileHandler
|
|
10
12
|
from pathlib import Path
|
|
13
|
+
from typing import cast
|
|
11
14
|
|
|
12
15
|
from rich.console import Console
|
|
13
16
|
from rich.logging import RichHandler
|
|
@@ -316,3 +319,57 @@ def _trash_path(path: Path) -> None:
|
|
|
316
319
|
)
|
|
317
320
|
except FileNotFoundError:
|
|
318
321
|
path.unlink(missing_ok=True)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
# Debug JSON serialization utilities
|
|
325
|
+
_DEBUG_TRUNCATE_PREFIX_CHARS = 96
|
|
326
|
+
|
|
327
|
+
# Keys whose values should be truncated (e.g., signatures, large payloads)
|
|
328
|
+
_TRUNCATE_KEYS = {"thought_signature", "thoughtSignature"}
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _truncate_debug_str(value: str, *, prefix_chars: int = _DEBUG_TRUNCATE_PREFIX_CHARS) -> str:
|
|
332
|
+
if len(value) <= prefix_chars:
|
|
333
|
+
return value
|
|
334
|
+
return f"{value[:prefix_chars]}...(truncated,len={len(value)})"
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _sanitize_debug_value(value: object) -> object:
|
|
338
|
+
if isinstance(value, (bytes, bytearray)):
|
|
339
|
+
encoded = b64encode(bytes(value)).decode("ascii")
|
|
340
|
+
return _truncate_debug_str(encoded)
|
|
341
|
+
if isinstance(value, str):
|
|
342
|
+
return value
|
|
343
|
+
if isinstance(value, list):
|
|
344
|
+
return [_sanitize_debug_value(v) for v in cast(list[object], value)]
|
|
345
|
+
if isinstance(value, dict):
|
|
346
|
+
return _sanitize_debug_dict(value) # type: ignore[arg-type]
|
|
347
|
+
return value
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _sanitize_debug_dict(obj: dict[object, object]) -> dict[object, object]:
|
|
351
|
+
sanitized: dict[object, object] = {}
|
|
352
|
+
for k, v in obj.items():
|
|
353
|
+
if k in _TRUNCATE_KEYS:
|
|
354
|
+
if isinstance(v, str):
|
|
355
|
+
sanitized[k] = _truncate_debug_str(v)
|
|
356
|
+
else:
|
|
357
|
+
sanitized[k] = _sanitize_debug_value(v)
|
|
358
|
+
continue
|
|
359
|
+
sanitized[k] = _sanitize_debug_value(v)
|
|
360
|
+
|
|
361
|
+
# Truncate inline image payloads (data field with mime_type indicates image blob)
|
|
362
|
+
if "data" in sanitized and ("mime_type" in sanitized or "mimeType" in sanitized):
|
|
363
|
+
data = sanitized.get("data")
|
|
364
|
+
if isinstance(data, str):
|
|
365
|
+
sanitized["data"] = _truncate_debug_str(data)
|
|
366
|
+
elif isinstance(data, (bytes, bytearray)):
|
|
367
|
+
encoded = b64encode(bytes(data)).decode("ascii")
|
|
368
|
+
sanitized["data"] = _truncate_debug_str(encoded)
|
|
369
|
+
|
|
370
|
+
return sanitized
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def debug_json(value: object) -> str:
|
|
374
|
+
"""Serialize a value to JSON for debug logging, truncating large payloads."""
|
|
375
|
+
return json.dumps(_sanitize_debug_value(value), ensure_ascii=False)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from pydantic import Field
|
|
4
|
+
|
|
3
5
|
from klaude_code.protocol import llm_param
|
|
4
6
|
from klaude_code.protocol.events.chat import DeveloperMessageEvent, UserMessageEvent
|
|
5
7
|
from klaude_code.protocol.events.lifecycle import TaskFinishEvent, TaskStartEvent, TurnStartEvent
|
|
@@ -14,6 +16,7 @@ class WelcomeEvent(Event):
|
|
|
14
16
|
work_dir: str
|
|
15
17
|
llm_config: llm_param.LLMConfigParameter
|
|
16
18
|
show_klaude_code_info: bool = True
|
|
19
|
+
loaded_skills: dict[str, list[str]] = Field(default_factory=dict)
|
|
17
20
|
|
|
18
21
|
|
|
19
22
|
class ErrorEvent(Event):
|
|
@@ -120,6 +120,7 @@ class LLMConfigProviderParameter(BaseModel):
|
|
|
120
120
|
|
|
121
121
|
class LLMConfigModelParameter(BaseModel):
|
|
122
122
|
model_id: str | None = None
|
|
123
|
+
disabled: bool = False
|
|
123
124
|
temperature: float | None = None
|
|
124
125
|
max_tokens: int | None = None
|
|
125
126
|
context_limit: int | None = None
|