klaude-code 2.7.0__py3-none-any.whl → 2.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- klaude_code/auth/AGENTS.md +325 -0
- klaude_code/auth/__init__.py +17 -1
- klaude_code/auth/antigravity/__init__.py +20 -0
- klaude_code/auth/antigravity/exceptions.py +17 -0
- klaude_code/auth/antigravity/oauth.py +320 -0
- klaude_code/auth/antigravity/pkce.py +25 -0
- klaude_code/auth/antigravity/token_manager.py +45 -0
- klaude_code/auth/base.py +4 -0
- klaude_code/auth/claude/oauth.py +29 -9
- klaude_code/auth/codex/exceptions.py +4 -0
- klaude_code/cli/auth_cmd.py +53 -3
- klaude_code/cli/cost_cmd.py +83 -160
- klaude_code/cli/list_model.py +50 -0
- klaude_code/cli/main.py +1 -1
- klaude_code/config/assets/builtin_config.yaml +108 -0
- klaude_code/config/builtin_config.py +5 -11
- klaude_code/config/config.py +24 -10
- klaude_code/const.py +1 -0
- klaude_code/core/agent.py +5 -1
- klaude_code/core/agent_profile.py +28 -32
- klaude_code/core/compaction/AGENTS.md +112 -0
- klaude_code/core/compaction/__init__.py +11 -0
- klaude_code/core/compaction/compaction.py +707 -0
- klaude_code/core/compaction/overflow.py +30 -0
- klaude_code/core/compaction/prompts.py +97 -0
- klaude_code/core/executor.py +103 -2
- klaude_code/core/manager/llm_clients.py +5 -0
- klaude_code/core/manager/llm_clients_builder.py +14 -2
- klaude_code/core/prompts/prompt-antigravity.md +80 -0
- klaude_code/core/prompts/prompt-codex-gpt-5-2.md +335 -0
- klaude_code/core/reminders.py +7 -2
- klaude_code/core/task.py +126 -0
- klaude_code/core/tool/todo/todo_write_tool.py +1 -1
- klaude_code/core/turn.py +3 -1
- klaude_code/llm/antigravity/__init__.py +3 -0
- klaude_code/llm/antigravity/client.py +558 -0
- klaude_code/llm/antigravity/input.py +261 -0
- klaude_code/llm/registry.py +1 -0
- klaude_code/protocol/events.py +18 -0
- klaude_code/protocol/llm_param.py +1 -0
- klaude_code/protocol/message.py +23 -1
- klaude_code/protocol/op.py +15 -1
- klaude_code/protocol/op_handler.py +5 -0
- klaude_code/session/session.py +36 -0
- klaude_code/skill/assets/create-plan/SKILL.md +6 -6
- klaude_code/tui/command/__init__.py +3 -0
- klaude_code/tui/command/compact_cmd.py +32 -0
- klaude_code/tui/command/fork_session_cmd.py +110 -14
- klaude_code/tui/command/model_picker.py +5 -1
- klaude_code/tui/command/thinking_cmd.py +1 -1
- klaude_code/tui/commands.py +6 -0
- klaude_code/tui/components/rich/markdown.py +57 -1
- klaude_code/tui/components/rich/theme.py +10 -2
- klaude_code/tui/components/tools.py +39 -25
- klaude_code/tui/components/user_input.py +1 -1
- klaude_code/tui/input/__init__.py +5 -2
- klaude_code/tui/input/drag_drop.py +6 -57
- klaude_code/tui/input/key_bindings.py +10 -0
- klaude_code/tui/input/prompt_toolkit.py +19 -6
- klaude_code/tui/machine.py +25 -0
- klaude_code/tui/renderer.py +67 -4
- klaude_code/tui/runner.py +18 -2
- klaude_code/tui/terminal/image.py +72 -10
- klaude_code/tui/terminal/selector.py +31 -7
- {klaude_code-2.7.0.dist-info → klaude_code-2.8.0.dist-info}/METADATA +1 -1
- {klaude_code-2.7.0.dist-info → klaude_code-2.8.0.dist-info}/RECORD +68 -52
- klaude_code/core/prompts/prompt-codex-gpt-5-1-codex-max.md +0 -117
- {klaude_code-2.7.0.dist-info → klaude_code-2.8.0.dist-info}/WHEEL +0 -0
- {klaude_code-2.7.0.dist-info → klaude_code-2.8.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"""Message conversion utilities for Antigravity Cloud Code Assist API."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from base64 import b64decode
|
|
5
|
+
from binascii import Error as BinasciiError
|
|
6
|
+
from typing import Any, TypedDict
|
|
7
|
+
|
|
8
|
+
from klaude_code.const import EMPTY_TOOL_OUTPUT_MESSAGE
|
|
9
|
+
from klaude_code.llm.image import assistant_image_to_data_url, parse_data_url
|
|
10
|
+
from klaude_code.llm.input_common import (
|
|
11
|
+
DeveloperAttachment,
|
|
12
|
+
attach_developer_messages,
|
|
13
|
+
merge_reminder_text,
|
|
14
|
+
split_thinking_parts,
|
|
15
|
+
)
|
|
16
|
+
from klaude_code.protocol import llm_param, message
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class InlineData(TypedDict, total=False):
|
|
20
|
+
mimeType: str
|
|
21
|
+
data: str
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class FunctionCall(TypedDict, total=False):
|
|
25
|
+
id: str
|
|
26
|
+
name: str
|
|
27
|
+
args: dict[str, Any]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class FunctionResponse(TypedDict, total=False):
|
|
31
|
+
id: str
|
|
32
|
+
name: str
|
|
33
|
+
response: dict[str, Any]
|
|
34
|
+
parts: list[dict[str, Any]]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Part(TypedDict, total=False):
|
|
38
|
+
text: str
|
|
39
|
+
thought: bool
|
|
40
|
+
thoughtSignature: str
|
|
41
|
+
inlineData: InlineData
|
|
42
|
+
functionCall: FunctionCall
|
|
43
|
+
functionResponse: FunctionResponse
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class Content(TypedDict):
|
|
47
|
+
role: str
|
|
48
|
+
parts: list[Part]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class FunctionDeclaration(TypedDict, total=False):
|
|
52
|
+
name: str
|
|
53
|
+
description: str
|
|
54
|
+
parameters: dict[str, Any]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class Tool(TypedDict):
|
|
58
|
+
functionDeclarations: list[FunctionDeclaration]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _data_url_to_inline_data(url: str) -> InlineData:
|
|
62
|
+
"""Convert data URL to inline_data dict."""
|
|
63
|
+
media_type, _, decoded = parse_data_url(url)
|
|
64
|
+
import base64
|
|
65
|
+
|
|
66
|
+
return InlineData(mimeType=media_type, data=base64.b64encode(decoded).decode("ascii"))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _image_part_to_part(image: message.ImageURLPart) -> Part:
|
|
70
|
+
"""Convert ImageURLPart to Part dict."""
|
|
71
|
+
url = image.url
|
|
72
|
+
if url.startswith("data:"):
|
|
73
|
+
return Part(inlineData=_data_url_to_inline_data(url))
|
|
74
|
+
# For non-data URLs, best-effort using inline_data format
|
|
75
|
+
return Part(text=f"[Image: {url}]")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _user_message_to_content(msg: message.UserMessage, attachment: DeveloperAttachment) -> Content:
|
|
79
|
+
"""Convert UserMessage to Content dict."""
|
|
80
|
+
parts: list[Part] = []
|
|
81
|
+
for part in msg.parts:
|
|
82
|
+
if isinstance(part, message.TextPart):
|
|
83
|
+
parts.append(Part(text=part.text))
|
|
84
|
+
elif isinstance(part, message.ImageURLPart):
|
|
85
|
+
parts.append(_image_part_to_part(part))
|
|
86
|
+
if attachment.text:
|
|
87
|
+
parts.append(Part(text=attachment.text))
|
|
88
|
+
for image in attachment.images:
|
|
89
|
+
parts.append(_image_part_to_part(image))
|
|
90
|
+
if not parts:
|
|
91
|
+
parts.append(Part(text=""))
|
|
92
|
+
return Content(role="user", parts=parts)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _tool_messages_to_contents(
|
|
96
|
+
msgs: list[tuple[message.ToolResultMessage, DeveloperAttachment]], model_name: str | None
|
|
97
|
+
) -> list[Content]:
|
|
98
|
+
"""Convert tool result messages to Content dicts."""
|
|
99
|
+
supports_multimodal_function_response = bool(model_name and "gemini-3" in model_name.lower())
|
|
100
|
+
|
|
101
|
+
response_parts: list[Part] = []
|
|
102
|
+
extra_image_contents: list[Content] = []
|
|
103
|
+
|
|
104
|
+
for msg, attachment in msgs:
|
|
105
|
+
merged_text = merge_reminder_text(
|
|
106
|
+
msg.output_text or EMPTY_TOOL_OUTPUT_MESSAGE,
|
|
107
|
+
attachment.text,
|
|
108
|
+
)
|
|
109
|
+
has_text = merged_text.strip() != ""
|
|
110
|
+
|
|
111
|
+
images = [part for part in msg.parts if isinstance(part, message.ImageURLPart)] + attachment.images
|
|
112
|
+
image_parts: list[Part] = []
|
|
113
|
+
function_response_parts: list[dict[str, Any]] = []
|
|
114
|
+
|
|
115
|
+
for image in images:
|
|
116
|
+
try:
|
|
117
|
+
image_parts.append(_image_part_to_part(image))
|
|
118
|
+
if image.url.startswith("data:"):
|
|
119
|
+
inline_data = _data_url_to_inline_data(image.url)
|
|
120
|
+
function_response_parts.append({"inlineData": inline_data})
|
|
121
|
+
except ValueError:
|
|
122
|
+
continue
|
|
123
|
+
|
|
124
|
+
has_images = len(image_parts) > 0
|
|
125
|
+
response_value = merged_text if has_text else "(see attached image)" if has_images else ""
|
|
126
|
+
response_payload = {"error": response_value} if msg.status != "success" else {"output": response_value}
|
|
127
|
+
|
|
128
|
+
function_response = FunctionResponse(
|
|
129
|
+
id=msg.call_id,
|
|
130
|
+
name=msg.tool_name,
|
|
131
|
+
response=response_payload,
|
|
132
|
+
)
|
|
133
|
+
if has_images and supports_multimodal_function_response:
|
|
134
|
+
function_response["parts"] = function_response_parts
|
|
135
|
+
|
|
136
|
+
response_parts.append(Part(functionResponse=function_response))
|
|
137
|
+
|
|
138
|
+
if has_images and not supports_multimodal_function_response:
|
|
139
|
+
extra_image_contents.append(Content(role="user", parts=[Part(text="Tool result image:"), *image_parts]))
|
|
140
|
+
|
|
141
|
+
contents: list[Content] = []
|
|
142
|
+
if response_parts:
|
|
143
|
+
contents.append(Content(role="user", parts=response_parts))
|
|
144
|
+
contents.extend(extra_image_contents)
|
|
145
|
+
return contents
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _decode_thought_signature(sig: str | None) -> str | None:
|
|
149
|
+
"""Validate base64 thought signature."""
|
|
150
|
+
if not sig:
|
|
151
|
+
return None
|
|
152
|
+
try:
|
|
153
|
+
b64decode(sig)
|
|
154
|
+
return sig
|
|
155
|
+
except (BinasciiError, ValueError):
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _assistant_message_to_content(msg: message.AssistantMessage, model_name: str | None) -> Content | None:
|
|
160
|
+
"""Convert AssistantMessage to Content dict."""
|
|
161
|
+
parts: list[Part] = []
|
|
162
|
+
native_thinking_parts, degraded_thinking_texts = split_thinking_parts(msg, model_name)
|
|
163
|
+
native_thinking_ids = {id(part) for part in native_thinking_parts}
|
|
164
|
+
|
|
165
|
+
for part in msg.parts:
|
|
166
|
+
if isinstance(part, message.ThinkingTextPart):
|
|
167
|
+
if id(part) not in native_thinking_ids:
|
|
168
|
+
continue
|
|
169
|
+
parts.append(Part(text=part.text, thought=True))
|
|
170
|
+
|
|
171
|
+
elif isinstance(part, message.ThinkingSignaturePart):
|
|
172
|
+
if id(part) not in native_thinking_ids:
|
|
173
|
+
continue
|
|
174
|
+
if not part.signature or part.format != "google":
|
|
175
|
+
continue
|
|
176
|
+
# Attach signature to the previous part
|
|
177
|
+
if parts:
|
|
178
|
+
sig = _decode_thought_signature(part.signature)
|
|
179
|
+
if sig:
|
|
180
|
+
parts[-1]["thoughtSignature"] = sig
|
|
181
|
+
|
|
182
|
+
elif isinstance(part, message.TextPart):
|
|
183
|
+
# Skip empty text blocks
|
|
184
|
+
if not part.text or part.text.strip() == "":
|
|
185
|
+
continue
|
|
186
|
+
parts.append(Part(text=part.text))
|
|
187
|
+
|
|
188
|
+
elif isinstance(part, message.ToolCallPart):
|
|
189
|
+
args: dict[str, Any]
|
|
190
|
+
if part.arguments_json:
|
|
191
|
+
try:
|
|
192
|
+
args = json.loads(part.arguments_json)
|
|
193
|
+
except json.JSONDecodeError:
|
|
194
|
+
args = {"_raw": part.arguments_json}
|
|
195
|
+
else:
|
|
196
|
+
args = {}
|
|
197
|
+
parts.append(Part(functionCall=FunctionCall(id=part.call_id, name=part.tool_name, args=args)))
|
|
198
|
+
|
|
199
|
+
elif isinstance(part, message.ImageFilePart):
|
|
200
|
+
try:
|
|
201
|
+
data_url = assistant_image_to_data_url(part)
|
|
202
|
+
parts.append(_image_part_to_part(message.ImageURLPart(url=data_url)))
|
|
203
|
+
except (ValueError, FileNotFoundError):
|
|
204
|
+
pass
|
|
205
|
+
|
|
206
|
+
if degraded_thinking_texts:
|
|
207
|
+
parts.insert(0, Part(text="<thinking>\n" + "\n".join(degraded_thinking_texts) + "\n</thinking>"))
|
|
208
|
+
|
|
209
|
+
if not parts:
|
|
210
|
+
return None
|
|
211
|
+
return Content(role="model", parts=parts)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def convert_history_to_contents(
|
|
215
|
+
history: list[message.Message],
|
|
216
|
+
model_name: str | None,
|
|
217
|
+
) -> list[Content]:
|
|
218
|
+
"""Convert message history to Cloud Code Assist Content format."""
|
|
219
|
+
contents: list[Content] = []
|
|
220
|
+
pending_tool_messages: list[tuple[message.ToolResultMessage, DeveloperAttachment]] = []
|
|
221
|
+
|
|
222
|
+
def flush_tool_messages() -> None:
|
|
223
|
+
nonlocal pending_tool_messages
|
|
224
|
+
if pending_tool_messages:
|
|
225
|
+
contents.extend(_tool_messages_to_contents(pending_tool_messages, model_name=model_name))
|
|
226
|
+
pending_tool_messages = []
|
|
227
|
+
|
|
228
|
+
for msg, attachment in attach_developer_messages(history):
|
|
229
|
+
match msg:
|
|
230
|
+
case message.ToolResultMessage():
|
|
231
|
+
pending_tool_messages.append((msg, attachment))
|
|
232
|
+
case message.UserMessage():
|
|
233
|
+
flush_tool_messages()
|
|
234
|
+
contents.append(_user_message_to_content(msg, attachment))
|
|
235
|
+
case message.AssistantMessage():
|
|
236
|
+
flush_tool_messages()
|
|
237
|
+
content = _assistant_message_to_content(msg, model_name=model_name)
|
|
238
|
+
if content is not None:
|
|
239
|
+
contents.append(content)
|
|
240
|
+
case message.SystemMessage():
|
|
241
|
+
continue
|
|
242
|
+
case _:
|
|
243
|
+
continue
|
|
244
|
+
|
|
245
|
+
flush_tool_messages()
|
|
246
|
+
return contents
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def convert_tool_schema(tools: list[llm_param.ToolSchema] | None) -> list[Tool] | None:
|
|
250
|
+
"""Convert tool schemas to Cloud Code Assist Tool format."""
|
|
251
|
+
if tools is None or len(tools) == 0:
|
|
252
|
+
return None
|
|
253
|
+
declarations = [
|
|
254
|
+
FunctionDeclaration(
|
|
255
|
+
name=tool.name,
|
|
256
|
+
description=tool.description,
|
|
257
|
+
parameters=dict(tool.parameters) if tool.parameters else {},
|
|
258
|
+
)
|
|
259
|
+
for tool in tools
|
|
260
|
+
]
|
|
261
|
+
return [Tool(functionDeclarations=declarations)]
|
klaude_code/llm/registry.py
CHANGED
|
@@ -21,6 +21,7 @@ _PROTOCOL_MODULES: dict[llm_param.LLMClientProtocol, str] = {
|
|
|
21
21
|
llm_param.LLMClientProtocol.OPENROUTER: "klaude_code.llm.openrouter",
|
|
22
22
|
llm_param.LLMClientProtocol.RESPONSES: "klaude_code.llm.responses",
|
|
23
23
|
llm_param.LLMClientProtocol.GOOGLE: "klaude_code.llm.google",
|
|
24
|
+
llm_param.LLMClientProtocol.ANTIGRAVITY: "klaude_code.llm.antigravity",
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
|
klaude_code/protocol/events.py
CHANGED
|
@@ -14,6 +14,8 @@ __all__ = [
|
|
|
14
14
|
"AssistantTextEndEvent",
|
|
15
15
|
"AssistantTextStartEvent",
|
|
16
16
|
"CommandOutputEvent",
|
|
17
|
+
"CompactionEndEvent",
|
|
18
|
+
"CompactionStartEvent",
|
|
17
19
|
"DeveloperMessageEvent",
|
|
18
20
|
"EndEvent",
|
|
19
21
|
"ErrorEvent",
|
|
@@ -83,6 +85,20 @@ class TaskStartEvent(Event):
|
|
|
83
85
|
model_id: str | None = None
|
|
84
86
|
|
|
85
87
|
|
|
88
|
+
class CompactionStartEvent(Event):
|
|
89
|
+
reason: Literal["threshold", "overflow", "manual"]
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class CompactionEndEvent(Event):
|
|
93
|
+
reason: Literal["threshold", "overflow", "manual"]
|
|
94
|
+
aborted: bool = False
|
|
95
|
+
will_retry: bool = False
|
|
96
|
+
tokens_before: int | None = None
|
|
97
|
+
kept_from_index: int | None = None
|
|
98
|
+
summary: str | None = None
|
|
99
|
+
kept_items_brief: list[message.KeptItemBrief] = Field(default_factory=list) # pyright: ignore[reportUnknownVariableType]
|
|
100
|
+
|
|
101
|
+
|
|
86
102
|
class TaskFinishEvent(Event):
|
|
87
103
|
task_result: str
|
|
88
104
|
has_structured_output: bool = False
|
|
@@ -185,6 +201,8 @@ type ReplayEventUnion = (
|
|
|
185
201
|
| InterruptEvent
|
|
186
202
|
| DeveloperMessageEvent
|
|
187
203
|
| ErrorEvent
|
|
204
|
+
| CompactionStartEvent
|
|
205
|
+
| CompactionEndEvent
|
|
188
206
|
)
|
|
189
207
|
|
|
190
208
|
|
klaude_code/protocol/message.py
CHANGED
|
@@ -63,6 +63,28 @@ class StreamErrorItem(BaseModel):
|
|
|
63
63
|
created_at: datetime = Field(default_factory=datetime.now)
|
|
64
64
|
|
|
65
65
|
|
|
66
|
+
class CompactionDetails(BaseModel):
|
|
67
|
+
read_files: list[str] = Field(default_factory=list)
|
|
68
|
+
modified_files: list[str] = Field(default_factory=list)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class KeptItemBrief(BaseModel):
|
|
72
|
+
"""Brief info about a kept (non-compacted) message item."""
|
|
73
|
+
|
|
74
|
+
item_type: str # "User", "Assistant", "Read", "Edit", "Bash", etc.
|
|
75
|
+
count: int = 1
|
|
76
|
+
preview: str = "" # Short preview text
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class CompactionEntry(BaseModel):
|
|
80
|
+
summary: str
|
|
81
|
+
first_kept_index: int
|
|
82
|
+
tokens_before: int | None = None
|
|
83
|
+
details: CompactionDetails | None = None
|
|
84
|
+
kept_items_brief: list[KeptItemBrief] = Field(default_factory=list) # pyright: ignore[reportUnknownVariableType]
|
|
85
|
+
created_at: datetime = Field(default_factory=datetime.now)
|
|
86
|
+
|
|
87
|
+
|
|
66
88
|
# Part types
|
|
67
89
|
|
|
68
90
|
|
|
@@ -173,7 +195,7 @@ class ToolResultMessage(MessageBase):
|
|
|
173
195
|
|
|
174
196
|
Message = SystemMessage | DeveloperMessage | UserMessage | AssistantMessage | ToolResultMessage
|
|
175
197
|
|
|
176
|
-
HistoryEvent = Message | StreamErrorItem | TaskMetadataItem
|
|
198
|
+
HistoryEvent = Message | StreamErrorItem | TaskMetadataItem | CompactionEntry
|
|
177
199
|
|
|
178
200
|
StreamItem = AssistantTextDelta | AssistantImageDelta | ThinkingTextDelta | ToolCallStartDelta
|
|
179
201
|
|
klaude_code/protocol/op.py
CHANGED
|
@@ -8,7 +8,7 @@ that the executor uses to handle different types of requests.
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
from enum import Enum
|
|
11
|
-
from typing import TYPE_CHECKING
|
|
11
|
+
from typing import TYPE_CHECKING, Literal
|
|
12
12
|
from uuid import uuid4
|
|
13
13
|
|
|
14
14
|
from pydantic import BaseModel, Field
|
|
@@ -24,6 +24,7 @@ class OperationType(Enum):
|
|
|
24
24
|
"""Enumeration of supported operation types."""
|
|
25
25
|
|
|
26
26
|
RUN_AGENT = "run_agent"
|
|
27
|
+
COMPACT_SESSION = "compact_session"
|
|
27
28
|
CHANGE_MODEL = "change_model"
|
|
28
29
|
CHANGE_SUB_AGENT_MODEL = "change_sub_agent_model"
|
|
29
30
|
CHANGE_THINKING = "change_thinking"
|
|
@@ -57,6 +58,19 @@ class RunAgentOperation(Operation):
|
|
|
57
58
|
await handler.handle_run_agent(self)
|
|
58
59
|
|
|
59
60
|
|
|
61
|
+
class CompactSessionOperation(Operation):
|
|
62
|
+
"""Operation for compacting a session's conversation history."""
|
|
63
|
+
|
|
64
|
+
type: OperationType = OperationType.COMPACT_SESSION
|
|
65
|
+
session_id: str
|
|
66
|
+
reason: Literal["threshold", "overflow", "manual"]
|
|
67
|
+
focus: str | None = None
|
|
68
|
+
will_retry: bool = False
|
|
69
|
+
|
|
70
|
+
async def execute(self, handler: OperationHandler) -> None:
|
|
71
|
+
await handler.handle_compact_session(self)
|
|
72
|
+
|
|
73
|
+
|
|
60
74
|
class ChangeModelOperation(Operation):
|
|
61
75
|
"""Operation for changing the model used by the active agent session."""
|
|
62
76
|
|
|
@@ -14,6 +14,7 @@ if TYPE_CHECKING:
|
|
|
14
14
|
ChangeSubAgentModelOperation,
|
|
15
15
|
ChangeThinkingOperation,
|
|
16
16
|
ClearSessionOperation,
|
|
17
|
+
CompactSessionOperation,
|
|
17
18
|
ExportSessionOperation,
|
|
18
19
|
InitAgentOperation,
|
|
19
20
|
InterruptOperation,
|
|
@@ -29,6 +30,10 @@ class OperationHandler(Protocol):
|
|
|
29
30
|
"""Handle a run agent operation."""
|
|
30
31
|
...
|
|
31
32
|
|
|
33
|
+
async def handle_compact_session(self, operation: CompactSessionOperation) -> None:
|
|
34
|
+
"""Handle a compact session operation."""
|
|
35
|
+
...
|
|
36
|
+
|
|
32
37
|
async def handle_change_model(self, operation: ChangeModelOperation) -> None:
|
|
33
38
|
"""Handle a change model operation."""
|
|
34
39
|
...
|
klaude_code/session/session.py
CHANGED
|
@@ -228,6 +228,30 @@ class Session(BaseModel):
|
|
|
228
228
|
)
|
|
229
229
|
self._store.append_and_flush(session_id=self.id, items=items, meta=meta)
|
|
230
230
|
|
|
231
|
+
def get_llm_history(self) -> list[message.HistoryEvent]:
|
|
232
|
+
"""Return the LLM-facing history view with compaction summary injected."""
|
|
233
|
+
history = self.conversation_history
|
|
234
|
+
last_compaction: message.CompactionEntry | None = None
|
|
235
|
+
for item in reversed(history):
|
|
236
|
+
if isinstance(item, message.CompactionEntry):
|
|
237
|
+
last_compaction = item
|
|
238
|
+
break
|
|
239
|
+
if last_compaction is None:
|
|
240
|
+
return [it for it in history if not isinstance(it, message.CompactionEntry)]
|
|
241
|
+
|
|
242
|
+
summary_message = message.UserMessage(parts=[message.TextPart(text=last_compaction.summary)])
|
|
243
|
+
kept = [it for it in history[last_compaction.first_kept_index :] if not isinstance(it, message.CompactionEntry)]
|
|
244
|
+
|
|
245
|
+
# Guard against old/bad persisted compaction boundaries that start with tool results.
|
|
246
|
+
# Tool results must not appear without their corresponding assistant tool call.
|
|
247
|
+
if kept and isinstance(kept[0], message.ToolResultMessage):
|
|
248
|
+
first_non_tool = 0
|
|
249
|
+
while first_non_tool < len(kept) and isinstance(kept[first_non_tool], message.ToolResultMessage):
|
|
250
|
+
first_non_tool += 1
|
|
251
|
+
kept = kept[first_non_tool:]
|
|
252
|
+
|
|
253
|
+
return [summary_message, *kept]
|
|
254
|
+
|
|
231
255
|
def fork(self, *, new_id: str | None = None, until_index: int | None = None) -> Session:
|
|
232
256
|
"""Create a new session as a fork of the current session.
|
|
233
257
|
|
|
@@ -399,6 +423,18 @@ class Session(BaseModel):
|
|
|
399
423
|
yield events.DeveloperMessageEvent(session_id=self.id, item=dm)
|
|
400
424
|
case message.StreamErrorItem() as se:
|
|
401
425
|
yield events.ErrorEvent(error_message=se.error, can_retry=False, session_id=self.id)
|
|
426
|
+
case message.CompactionEntry() as ce:
|
|
427
|
+
yield events.CompactionStartEvent(session_id=self.id, reason="threshold")
|
|
428
|
+
yield events.CompactionEndEvent(
|
|
429
|
+
session_id=self.id,
|
|
430
|
+
reason="threshold",
|
|
431
|
+
aborted=False,
|
|
432
|
+
will_retry=False,
|
|
433
|
+
tokens_before=ce.tokens_before,
|
|
434
|
+
kept_from_index=ce.first_kept_index,
|
|
435
|
+
summary=ce.summary,
|
|
436
|
+
kept_items_brief=ce.kept_items_brief,
|
|
437
|
+
)
|
|
402
438
|
case message.SystemMessage():
|
|
403
439
|
pass
|
|
404
440
|
prev_item = it
|
|
@@ -49,12 +49,12 @@ Throughout the entire workflow, operate in read-only mode. Do not write or updat
|
|
|
49
49
|
- Out:
|
|
50
50
|
|
|
51
51
|
## Action items
|
|
52
|
-
[ ] <Step 1>
|
|
53
|
-
[ ] <Step 2>
|
|
54
|
-
[ ] <Step 3>
|
|
55
|
-
[ ] <Step 4>
|
|
56
|
-
[ ] <Step 5>
|
|
57
|
-
[ ] <Step 6>
|
|
52
|
+
- [ ] <Step 1>
|
|
53
|
+
- [ ] <Step 2>
|
|
54
|
+
- [ ] <Step 3>
|
|
55
|
+
- [ ] <Step 4>
|
|
56
|
+
- [ ] <Step 5>
|
|
57
|
+
- [ ] <Step 6>
|
|
58
58
|
|
|
59
59
|
## Open questions
|
|
60
60
|
- <Question 1>
|
|
@@ -30,6 +30,7 @@ def ensure_commands_loaded() -> None:
|
|
|
30
30
|
|
|
31
31
|
# Import and register commands in display order
|
|
32
32
|
from .clear_cmd import ClearCommand
|
|
33
|
+
from .compact_cmd import CompactCommand
|
|
33
34
|
from .copy_cmd import CopyCommand
|
|
34
35
|
from .debug_cmd import DebugCommand
|
|
35
36
|
from .export_cmd import ExportCommand
|
|
@@ -45,6 +46,7 @@ def ensure_commands_loaded() -> None:
|
|
|
45
46
|
# Register in desired display order
|
|
46
47
|
register(CopyCommand())
|
|
47
48
|
register(ExportCommand())
|
|
49
|
+
register(CompactCommand())
|
|
48
50
|
register(RefreshTerminalCommand())
|
|
49
51
|
register(ModelCommand())
|
|
50
52
|
register(SubAgentModelCommand())
|
|
@@ -64,6 +66,7 @@ def ensure_commands_loaded() -> None:
|
|
|
64
66
|
def __getattr__(name: str) -> object:
|
|
65
67
|
_commands_map = {
|
|
66
68
|
"ClearCommand": "clear_cmd",
|
|
69
|
+
"CompactCommand": "compact_cmd",
|
|
67
70
|
"CopyCommand": "copy_cmd",
|
|
68
71
|
"DebugCommand": "debug_cmd",
|
|
69
72
|
"ExportCommand": "export_cmd",
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from klaude_code.protocol import commands, message, op
|
|
2
|
+
from klaude_code.tui.command.command_abc import Agent, CommandABC, CommandResult
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class CompactCommand(CommandABC):
|
|
6
|
+
@property
|
|
7
|
+
def name(self) -> commands.CommandName:
|
|
8
|
+
return commands.CommandName.COMPACT
|
|
9
|
+
|
|
10
|
+
@property
|
|
11
|
+
def summary(self) -> str:
|
|
12
|
+
return "summarize older context to free up the model window"
|
|
13
|
+
|
|
14
|
+
@property
|
|
15
|
+
def support_addition_params(self) -> bool:
|
|
16
|
+
return True
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def placeholder(self) -> str:
|
|
20
|
+
return "optional focus for the summary"
|
|
21
|
+
|
|
22
|
+
async def run(self, agent: Agent, user_input: message.UserInputPayload) -> CommandResult:
|
|
23
|
+
focus = user_input.text.strip() if user_input.text else None
|
|
24
|
+
return CommandResult(
|
|
25
|
+
operations=[
|
|
26
|
+
op.CompactSessionOperation(
|
|
27
|
+
session_id=agent.session.id,
|
|
28
|
+
reason="manual",
|
|
29
|
+
focus=focus or None,
|
|
30
|
+
)
|
|
31
|
+
]
|
|
32
|
+
)
|