klaude-code 2.6.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/app/runtime.py +1 -1
- 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/auth/env.py +19 -15
- klaude_code/cli/auth_cmd.py +54 -4
- klaude_code/cli/cost_cmd.py +83 -160
- klaude_code/cli/list_model.py +50 -0
- klaude_code/cli/main.py +99 -9
- 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 +11 -1
- 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 +11 -7
- 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/commands.py +0 -1
- 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/skill/loader.py +12 -13
- klaude_code/skill/manager.py +3 -3
- klaude_code/tui/command/__init__.py +4 -4
- klaude_code/tui/command/compact_cmd.py +32 -0
- klaude_code/tui/command/copy_cmd.py +1 -1
- klaude_code/tui/command/fork_session_cmd.py +114 -18
- 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/command_output.py +1 -1
- klaude_code/tui/components/rich/markdown.py +117 -1
- klaude_code/tui/components/rich/theme.py +18 -2
- klaude_code/tui/components/tools.py +39 -25
- klaude_code/tui/components/user_input.py +39 -28
- klaude_code/tui/input/AGENTS.md +44 -0
- klaude_code/tui/input/__init__.py +5 -2
- klaude_code/tui/input/completers.py +10 -14
- klaude_code/tui/input/drag_drop.py +146 -0
- klaude_code/tui/input/images.py +227 -0
- klaude_code/tui/input/key_bindings.py +183 -19
- klaude_code/tui/input/paste.py +71 -0
- klaude_code/tui/input/prompt_toolkit.py +32 -9
- klaude_code/tui/machine.py +26 -1
- klaude_code/tui/renderer.py +67 -4
- klaude_code/tui/runner.py +19 -3
- klaude_code/tui/terminal/image.py +103 -10
- klaude_code/tui/terminal/selector.py +81 -7
- {klaude_code-2.6.0.dist-info → klaude_code-2.8.0.dist-info}/METADATA +10 -10
- {klaude_code-2.6.0.dist-info → klaude_code-2.8.0.dist-info}/RECORD +79 -61
- klaude_code/core/prompts/prompt-codex-gpt-5-1-codex-max.md +0 -117
- klaude_code/tui/command/terminal_setup_cmd.py +0 -248
- klaude_code/tui/input/clipboard.py +0 -152
- {klaude_code-2.6.0.dist-info → klaude_code-2.8.0.dist-info}/WHEEL +0 -0
- {klaude_code-2.6.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/commands.py
CHANGED
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>
|
klaude_code/skill/loader.py
CHANGED
|
@@ -209,22 +209,21 @@ class SkillLoader:
|
|
|
209
209
|
"""Get list of all loaded skill names"""
|
|
210
210
|
return list(self.loaded_skills.keys())
|
|
211
211
|
|
|
212
|
-
def
|
|
213
|
-
"""Generate
|
|
212
|
+
def get_skills_yaml(self) -> str:
|
|
213
|
+
"""Generate skill metadata in YAML format for system prompt.
|
|
214
214
|
|
|
215
215
|
Returns:
|
|
216
|
-
|
|
216
|
+
YAML string with all skill metadata
|
|
217
217
|
"""
|
|
218
|
-
|
|
219
|
-
# Prefer showing higher-priority skills first (project > user > system).
|
|
218
|
+
yaml_parts: list[str] = []
|
|
220
219
|
location_order = {"project": 0, "user": 1, "system": 2}
|
|
221
220
|
for skill in sorted(self.loaded_skills.values(), key=lambda s: location_order.get(s.location, 3)):
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
221
|
+
# Escape description for YAML (handle multi-line and special chars)
|
|
222
|
+
desc = skill.description.replace("\n", " ").strip()
|
|
223
|
+
yaml_parts.append(
|
|
224
|
+
f"- name: {skill.name}\n"
|
|
225
|
+
f" description: {desc}\n"
|
|
226
|
+
f" scope: {skill.location}\n"
|
|
227
|
+
f" location: {skill.skill_path}"
|
|
229
228
|
)
|
|
230
|
-
return "\n".join(
|
|
229
|
+
return "\n".join(yaml_parts)
|
klaude_code/skill/manager.py
CHANGED
|
@@ -80,8 +80,8 @@ def format_available_skills_for_system_prompt() -> str:
|
|
|
80
80
|
|
|
81
81
|
try:
|
|
82
82
|
loader = _ensure_initialized()
|
|
83
|
-
|
|
84
|
-
if not
|
|
83
|
+
skills_yaml = loader.get_skills_yaml().strip()
|
|
84
|
+
if not skills_yaml:
|
|
85
85
|
return ""
|
|
86
86
|
|
|
87
87
|
return f"""
|
|
@@ -102,7 +102,7 @@ Important:
|
|
|
102
102
|
The list below is metadata only (name/description/location). The full instructions live in the referenced file.
|
|
103
103
|
|
|
104
104
|
<available_skills>
|
|
105
|
-
{
|
|
105
|
+
{skills_yaml}
|
|
106
106
|
</available_skills>"""
|
|
107
107
|
except Exception:
|
|
108
108
|
# Skills are an optional enhancement; do not fail prompt construction if discovery breaks.
|
|
@@ -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
|
|
@@ -40,12 +41,12 @@ def ensure_commands_loaded() -> None:
|
|
|
40
41
|
from .resume_cmd import ResumeCommand
|
|
41
42
|
from .status_cmd import StatusCommand
|
|
42
43
|
from .sub_agent_model_cmd import SubAgentModelCommand
|
|
43
|
-
from .terminal_setup_cmd import TerminalSetupCommand
|
|
44
44
|
from .thinking_cmd import ThinkingCommand
|
|
45
45
|
|
|
46
46
|
# Register in desired display order
|
|
47
47
|
register(CopyCommand())
|
|
48
48
|
register(ExportCommand())
|
|
49
|
+
register(CompactCommand())
|
|
49
50
|
register(RefreshTerminalCommand())
|
|
50
51
|
register(ModelCommand())
|
|
51
52
|
register(SubAgentModelCommand())
|
|
@@ -55,7 +56,6 @@ def ensure_commands_loaded() -> None:
|
|
|
55
56
|
register(StatusCommand())
|
|
56
57
|
register(ResumeCommand())
|
|
57
58
|
register(ExportOnlineCommand())
|
|
58
|
-
register(TerminalSetupCommand())
|
|
59
59
|
register(DebugCommand())
|
|
60
60
|
register(ClearCommand())
|
|
61
61
|
|
|
@@ -66,6 +66,7 @@ def ensure_commands_loaded() -> None:
|
|
|
66
66
|
def __getattr__(name: str) -> object:
|
|
67
67
|
_commands_map = {
|
|
68
68
|
"ClearCommand": "clear_cmd",
|
|
69
|
+
"CompactCommand": "compact_cmd",
|
|
69
70
|
"CopyCommand": "copy_cmd",
|
|
70
71
|
"DebugCommand": "debug_cmd",
|
|
71
72
|
"ExportCommand": "export_cmd",
|
|
@@ -76,7 +77,6 @@ def __getattr__(name: str) -> object:
|
|
|
76
77
|
"ResumeCommand": "resume_cmd",
|
|
77
78
|
"StatusCommand": "status_cmd",
|
|
78
79
|
"SubAgentModelCommand": "sub_agent_model_cmd",
|
|
79
|
-
"TerminalSetupCommand": "terminal_setup_cmd",
|
|
80
80
|
"ThinkingCommand": "thinking_cmd",
|
|
81
81
|
}
|
|
82
82
|
if name in _commands_map:
|
|
@@ -91,7 +91,7 @@ __all__ = [
|
|
|
91
91
|
# Command classes are lazily loaded via __getattr__
|
|
92
92
|
# "ClearCommand", "DiffCommand", "HelpCommand", "ModelCommand",
|
|
93
93
|
# "ExportCommand", "RefreshTerminalCommand", "ReleaseNotesCommand",
|
|
94
|
-
# "StatusCommand",
|
|
94
|
+
# "StatusCommand",
|
|
95
95
|
"CommandABC",
|
|
96
96
|
"CommandResult",
|
|
97
97
|
"dispatch_command",
|
|
@@ -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
|
+
)
|