klaude-code 2.10.3__py3-none-any.whl → 2.10.4__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 +4 -24
- klaude_code/auth/__init__.py +1 -17
- klaude_code/cli/auth_cmd.py +3 -53
- klaude_code/cli/list_model.py +0 -50
- klaude_code/config/assets/builtin_config.yaml +0 -28
- klaude_code/config/config.py +5 -42
- klaude_code/const.py +5 -2
- klaude_code/core/agent_profile.py +2 -10
- klaude_code/core/backtrack/__init__.py +3 -0
- klaude_code/core/backtrack/manager.py +48 -0
- klaude_code/core/memory.py +25 -9
- klaude_code/core/task.py +53 -7
- klaude_code/core/tool/__init__.py +2 -0
- klaude_code/core/tool/backtrack/__init__.py +3 -0
- klaude_code/core/tool/backtrack/backtrack_tool.md +17 -0
- klaude_code/core/tool/backtrack/backtrack_tool.py +65 -0
- klaude_code/core/tool/context.py +5 -0
- klaude_code/core/turn.py +3 -0
- klaude_code/llm/input_common.py +70 -1
- klaude_code/llm/openai_compatible/input.py +5 -2
- klaude_code/llm/openrouter/input.py +5 -2
- klaude_code/llm/registry.py +0 -1
- klaude_code/protocol/events.py +10 -0
- klaude_code/protocol/llm_param.py +0 -1
- klaude_code/protocol/message.py +10 -1
- klaude_code/protocol/tools.py +1 -0
- klaude_code/session/session.py +111 -2
- klaude_code/session/store.py +2 -0
- klaude_code/skill/assets/executing-plans/SKILL.md +84 -0
- klaude_code/skill/assets/writing-plans/SKILL.md +116 -0
- klaude_code/tui/commands.py +15 -0
- klaude_code/tui/components/developer.py +1 -1
- klaude_code/tui/components/rich/status.py +7 -76
- klaude_code/tui/components/rich/theme.py +10 -0
- klaude_code/tui/components/tools.py +31 -18
- klaude_code/tui/display.py +4 -0
- klaude_code/tui/input/prompt_toolkit.py +15 -1
- klaude_code/tui/machine.py +26 -8
- klaude_code/tui/renderer.py +97 -0
- klaude_code/tui/runner.py +7 -2
- klaude_code/tui/terminal/image.py +28 -12
- klaude_code/ui/terminal/title.py +8 -3
- {klaude_code-2.10.3.dist-info → klaude_code-2.10.4.dist-info}/METADATA +1 -1
- {klaude_code-2.10.3.dist-info → klaude_code-2.10.4.dist-info}/RECORD +46 -49
- klaude_code/auth/antigravity/__init__.py +0 -20
- klaude_code/auth/antigravity/exceptions.py +0 -17
- klaude_code/auth/antigravity/oauth.py +0 -315
- klaude_code/auth/antigravity/pkce.py +0 -25
- klaude_code/auth/antigravity/token_manager.py +0 -27
- klaude_code/core/prompts/prompt-antigravity.md +0 -80
- klaude_code/llm/antigravity/__init__.py +0 -3
- klaude_code/llm/antigravity/client.py +0 -558
- klaude_code/llm/antigravity/input.py +0 -268
- klaude_code/skill/assets/create-plan/SKILL.md +0 -74
- {klaude_code-2.10.3.dist-info → klaude_code-2.10.4.dist-info}/WHEEL +0 -0
- {klaude_code-2.10.3.dist-info → klaude_code-2.10.4.dist-info}/entry_points.txt +0 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from .backtrack.backtrack_tool import BacktrackTool
|
|
1
2
|
from .context import FileTracker, RunSubtask, SubAgentResumeClaims, TodoContext, ToolContext, build_todo_context
|
|
2
3
|
from .file.apply_patch import DiffError, process_patch
|
|
3
4
|
from .file.apply_patch_tool import ApplyPatchTool
|
|
@@ -19,6 +20,7 @@ from .web.web_search_tool import WebSearchTool
|
|
|
19
20
|
|
|
20
21
|
__all__ = [
|
|
21
22
|
"ApplyPatchTool",
|
|
23
|
+
"BacktrackTool",
|
|
22
24
|
"BashTool",
|
|
23
25
|
"DiffError",
|
|
24
26
|
"EditTool",
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Revert conversation history to a previous checkpoint, discarding everything after it.
|
|
2
|
+
|
|
3
|
+
Use this tool when:
|
|
4
|
+
- You spent many tokens on exploration that turned out unproductive
|
|
5
|
+
- You read large files but only need to keep key information
|
|
6
|
+
- A deep debugging session can be summarized before continuing
|
|
7
|
+
- The current approach is stuck and you want to try a different path
|
|
8
|
+
|
|
9
|
+
The note you provide will be shown to your future self at the checkpoint, so include:
|
|
10
|
+
- Key findings from your exploration
|
|
11
|
+
- What approaches did not work and why
|
|
12
|
+
- Important context needed to continue
|
|
13
|
+
|
|
14
|
+
IMPORTANT:
|
|
15
|
+
- File system changes are NOT reverted - only conversation history is affected
|
|
16
|
+
- Checkpoints are created automatically at the start of each turn
|
|
17
|
+
- Available checkpoints appear as <system>Checkpoint N</system> in the conversation
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
from klaude_code.core.tool.context import ToolContext
|
|
6
|
+
from klaude_code.core.tool.tool_abc import ToolABC, load_desc
|
|
7
|
+
from klaude_code.core.tool.tool_registry import register
|
|
8
|
+
from klaude_code.protocol import llm_param, message, tools
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BacktrackArguments(BaseModel):
|
|
12
|
+
checkpoint_id: int = Field(description="The checkpoint ID to revert to")
|
|
13
|
+
note: str = Field(description="A note to your future self with key findings/context to preserve")
|
|
14
|
+
rationale: str = Field(description="Why you are performing this backtrack")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@register(tools.BACKTRACK)
|
|
18
|
+
class BacktrackTool(ToolABC):
|
|
19
|
+
@classmethod
|
|
20
|
+
def schema(cls) -> llm_param.ToolSchema:
|
|
21
|
+
return llm_param.ToolSchema(
|
|
22
|
+
name=tools.BACKTRACK,
|
|
23
|
+
type="function",
|
|
24
|
+
description=load_desc(Path(__file__).parent / "backtrack_tool.md"),
|
|
25
|
+
parameters={
|
|
26
|
+
"type": "object",
|
|
27
|
+
"properties": {
|
|
28
|
+
"checkpoint_id": {
|
|
29
|
+
"type": "integer",
|
|
30
|
+
"description": "The checkpoint ID to revert to",
|
|
31
|
+
},
|
|
32
|
+
"note": {
|
|
33
|
+
"type": "string",
|
|
34
|
+
"description": "A note to your future self with key findings/context",
|
|
35
|
+
},
|
|
36
|
+
"rationale": {
|
|
37
|
+
"type": "string",
|
|
38
|
+
"description": "Why you are performing this backtrack",
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
"required": ["checkpoint_id", "note", "rationale"],
|
|
42
|
+
"additionalProperties": False,
|
|
43
|
+
},
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
async def call(cls, arguments: str, context: ToolContext) -> message.ToolResultMessage:
|
|
48
|
+
try:
|
|
49
|
+
args = BacktrackArguments.model_validate_json(arguments)
|
|
50
|
+
except ValueError as exc:
|
|
51
|
+
return message.ToolResultMessage(status="error", output_text=f"Invalid arguments: {exc}")
|
|
52
|
+
|
|
53
|
+
backtrack_manager = context.backtrack_manager
|
|
54
|
+
if backtrack_manager is None:
|
|
55
|
+
return message.ToolResultMessage(
|
|
56
|
+
status="error",
|
|
57
|
+
output_text="Backtrack is not available in this context",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
result = backtrack_manager.send_backtrack(args.checkpoint_id, args.note, args.rationale)
|
|
62
|
+
except ValueError as exc:
|
|
63
|
+
return message.ToolResultMessage(status="error", output_text=str(exc))
|
|
64
|
+
|
|
65
|
+
return message.ToolResultMessage(status="success", output_text=result)
|
klaude_code/core/tool/context.py
CHANGED
|
@@ -4,6 +4,7 @@ import asyncio
|
|
|
4
4
|
from collections.abc import Awaitable, Callable, MutableMapping
|
|
5
5
|
from dataclasses import dataclass, replace
|
|
6
6
|
|
|
7
|
+
from klaude_code.core.backtrack import BacktrackManager
|
|
7
8
|
from klaude_code.protocol import model
|
|
8
9
|
from klaude_code.protocol.sub_agent import SubAgentResult
|
|
9
10
|
from klaude_code.session.session import Session
|
|
@@ -85,9 +86,13 @@ class ToolContext:
|
|
|
85
86
|
sub_agent_resume_claims: SubAgentResumeClaims | None = None
|
|
86
87
|
record_sub_agent_session_id: Callable[[str], None] | None = None
|
|
87
88
|
register_sub_agent_metadata_getter: Callable[[GetMetadataFn], None] | None = None
|
|
89
|
+
backtrack_manager: BacktrackManager | None = None
|
|
88
90
|
|
|
89
91
|
def with_record_sub_agent_session_id(self, callback: Callable[[str], None] | None) -> ToolContext:
|
|
90
92
|
return replace(self, record_sub_agent_session_id=callback)
|
|
91
93
|
|
|
92
94
|
def with_register_sub_agent_metadata_getter(self, callback: Callable[[GetMetadataFn], None] | None) -> ToolContext:
|
|
93
95
|
return replace(self, register_sub_agent_metadata_getter=callback)
|
|
96
|
+
|
|
97
|
+
def with_backtrack_manager(self, manager: BacktrackManager | None) -> ToolContext:
|
|
98
|
+
return replace(self, backtrack_manager=manager)
|
klaude_code/core/turn.py
CHANGED
|
@@ -5,6 +5,7 @@ from dataclasses import dataclass, field
|
|
|
5
5
|
from typing import TYPE_CHECKING
|
|
6
6
|
|
|
7
7
|
from klaude_code.const import RETRY_PRESERVE_PARTIAL_MESSAGE, SUPPORTED_IMAGE_SIZES
|
|
8
|
+
from klaude_code.core.backtrack import BacktrackManager
|
|
8
9
|
from klaude_code.core.tool import ToolABC
|
|
9
10
|
from klaude_code.core.tool.context import SubAgentResumeClaims, ToolContext
|
|
10
11
|
|
|
@@ -49,6 +50,7 @@ class TurnExecutionContext:
|
|
|
49
50
|
tools: list[llm_param.ToolSchema]
|
|
50
51
|
tool_registry: dict[str, type[ToolABC]]
|
|
51
52
|
sub_agent_state: model.SubAgentState | None = None
|
|
53
|
+
backtrack_manager: BacktrackManager | None = None
|
|
52
54
|
|
|
53
55
|
|
|
54
56
|
@dataclass
|
|
@@ -404,6 +406,7 @@ class TurnExecutor:
|
|
|
404
406
|
session_id=session_ctx.session_id,
|
|
405
407
|
run_subtask=session_ctx.run_subtask,
|
|
406
408
|
sub_agent_resume_claims=SubAgentResumeClaims(),
|
|
409
|
+
backtrack_manager=ctx.backtrack_manager,
|
|
407
410
|
)
|
|
408
411
|
|
|
409
412
|
executor = ToolExecutor(
|
klaude_code/llm/input_common.py
CHANGED
|
@@ -108,17 +108,86 @@ def build_tool_message(
|
|
|
108
108
|
msg: message.ToolResultMessage,
|
|
109
109
|
attachment: DeveloperAttachment,
|
|
110
110
|
) -> dict[str, object]:
|
|
111
|
+
"""Build a tool message. Note: image_url in tool message is not supported by
|
|
112
|
+
OpenAI Chat Completions API. Use build_tool_message_for_chat_completions instead.
|
|
113
|
+
"""
|
|
111
114
|
merged_text = merge_reminder_text(
|
|
112
115
|
msg.output_text or EMPTY_TOOL_OUTPUT_MESSAGE,
|
|
113
116
|
attachment.text,
|
|
114
117
|
)
|
|
118
|
+
content: list[dict[str, object]] = [{"type": "text", "text": merged_text}]
|
|
119
|
+
for part in msg.parts:
|
|
120
|
+
if isinstance(part, message.ImageFilePart):
|
|
121
|
+
content.append({"type": "image_url", "image_url": {"url": image_file_to_data_url(part)}})
|
|
122
|
+
elif isinstance(part, message.ImageURLPart):
|
|
123
|
+
content.append({"type": "image_url", "image_url": {"url": part.url}})
|
|
124
|
+
for image in attachment.images:
|
|
125
|
+
if isinstance(image, message.ImageFilePart):
|
|
126
|
+
content.append({"type": "image_url", "image_url": {"url": image_file_to_data_url(image)}})
|
|
127
|
+
else:
|
|
128
|
+
content.append({"type": "image_url", "image_url": {"url": image.url}})
|
|
115
129
|
return {
|
|
116
130
|
"role": "tool",
|
|
117
|
-
"content":
|
|
131
|
+
"content": content,
|
|
118
132
|
"tool_call_id": msg.call_id,
|
|
119
133
|
}
|
|
120
134
|
|
|
121
135
|
|
|
136
|
+
def build_tool_message_for_chat_completions(
|
|
137
|
+
msg: message.ToolResultMessage,
|
|
138
|
+
attachment: DeveloperAttachment,
|
|
139
|
+
) -> tuple[dict[str, object], dict[str, object] | None]:
|
|
140
|
+
"""Build tool message for OpenAI Chat Completions API.
|
|
141
|
+
|
|
142
|
+
OpenAI Chat Completions API does not support image_url in tool messages.
|
|
143
|
+
Images are extracted and returned as a separate user message to be appended after the tool message.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
A tuple of (tool_message, optional_user_message_with_images).
|
|
147
|
+
The user_message is None if there are no images.
|
|
148
|
+
"""
|
|
149
|
+
merged_text = merge_reminder_text(
|
|
150
|
+
msg.output_text or EMPTY_TOOL_OUTPUT_MESSAGE,
|
|
151
|
+
attachment.text,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Collect all images
|
|
155
|
+
image_urls: list[dict[str, object]] = []
|
|
156
|
+
for part in msg.parts:
|
|
157
|
+
if isinstance(part, message.ImageFilePart):
|
|
158
|
+
image_urls.append({"type": "image_url", "image_url": {"url": image_file_to_data_url(part)}})
|
|
159
|
+
elif isinstance(part, message.ImageURLPart):
|
|
160
|
+
image_urls.append({"type": "image_url", "image_url": {"url": part.url}})
|
|
161
|
+
for image in attachment.images:
|
|
162
|
+
if isinstance(image, message.ImageFilePart):
|
|
163
|
+
image_urls.append({"type": "image_url", "image_url": {"url": image_file_to_data_url(image)}})
|
|
164
|
+
else:
|
|
165
|
+
image_urls.append({"type": "image_url", "image_url": {"url": image.url}})
|
|
166
|
+
|
|
167
|
+
# If only images (no text), use placeholder
|
|
168
|
+
has_text = bool(merged_text.strip())
|
|
169
|
+
tool_content = merged_text if has_text else "(see attached image)"
|
|
170
|
+
|
|
171
|
+
tool_message: dict[str, object] = {
|
|
172
|
+
"role": "tool",
|
|
173
|
+
"content": tool_content,
|
|
174
|
+
"tool_call_id": msg.call_id,
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
# Build user message with images if any
|
|
178
|
+
user_message: dict[str, object] | None = None
|
|
179
|
+
if image_urls:
|
|
180
|
+
user_message = {
|
|
181
|
+
"role": "user",
|
|
182
|
+
"content": [
|
|
183
|
+
{"type": "text", "text": "Attached image(s) from tool result:"},
|
|
184
|
+
*image_urls,
|
|
185
|
+
],
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return tool_message, user_message
|
|
189
|
+
|
|
190
|
+
|
|
122
191
|
def build_assistant_common_fields(
|
|
123
192
|
msg: message.AssistantMessage,
|
|
124
193
|
*,
|
|
@@ -12,7 +12,7 @@ from klaude_code.llm.input_common import (
|
|
|
12
12
|
attach_developer_messages,
|
|
13
13
|
build_assistant_common_fields,
|
|
14
14
|
build_chat_content_parts,
|
|
15
|
-
|
|
15
|
+
build_tool_message_for_chat_completions,
|
|
16
16
|
collect_text_content,
|
|
17
17
|
)
|
|
18
18
|
from klaude_code.protocol import llm_param, message
|
|
@@ -50,7 +50,10 @@ def convert_history_to_input(
|
|
|
50
50
|
parts = build_chat_content_parts(msg, attachment)
|
|
51
51
|
messages.append(cast(chat.ChatCompletionMessageParam, {"role": "user", "content": parts}))
|
|
52
52
|
case message.ToolResultMessage():
|
|
53
|
-
|
|
53
|
+
tool_msg, user_msg = build_tool_message_for_chat_completions(msg, attachment)
|
|
54
|
+
messages.append(cast(chat.ChatCompletionMessageParam, tool_msg))
|
|
55
|
+
if user_msg is not None:
|
|
56
|
+
messages.append(cast(chat.ChatCompletionMessageParam, user_msg))
|
|
54
57
|
case message.AssistantMessage():
|
|
55
58
|
messages.append(_assistant_message_to_openai(msg))
|
|
56
59
|
case _:
|
|
@@ -15,7 +15,7 @@ from klaude_code.llm.input_common import (
|
|
|
15
15
|
attach_developer_messages,
|
|
16
16
|
build_assistant_common_fields,
|
|
17
17
|
build_chat_content_parts,
|
|
18
|
-
|
|
18
|
+
build_tool_message_for_chat_completions,
|
|
19
19
|
collect_text_content,
|
|
20
20
|
split_thinking_parts,
|
|
21
21
|
)
|
|
@@ -153,7 +153,10 @@ def convert_history_to_input(
|
|
|
153
153
|
parts = build_chat_content_parts(msg, attachment)
|
|
154
154
|
messages.append(cast(chat.ChatCompletionMessageParam, {"role": "user", "content": parts}))
|
|
155
155
|
case message.ToolResultMessage():
|
|
156
|
-
|
|
156
|
+
tool_msg, user_msg = build_tool_message_for_chat_completions(msg, attachment)
|
|
157
|
+
messages.append(cast(chat.ChatCompletionMessageParam, tool_msg))
|
|
158
|
+
if user_msg is not None:
|
|
159
|
+
messages.append(cast(chat.ChatCompletionMessageParam, user_msg))
|
|
157
160
|
case message.AssistantMessage():
|
|
158
161
|
messages.append(_assistant_message_to_openrouter(msg, model_name))
|
|
159
162
|
case _:
|
klaude_code/llm/registry.py
CHANGED
|
@@ -21,7 +21,6 @@ _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.openai_responses",
|
|
23
23
|
llm_param.LLMClientProtocol.GOOGLE: "klaude_code.llm.google",
|
|
24
|
-
llm_param.LLMClientProtocol.ANTIGRAVITY: "klaude_code.llm.antigravity",
|
|
25
24
|
}
|
|
26
25
|
|
|
27
26
|
|
klaude_code/protocol/events.py
CHANGED
|
@@ -14,6 +14,7 @@ __all__ = [
|
|
|
14
14
|
"AssistantTextDeltaEvent",
|
|
15
15
|
"AssistantTextEndEvent",
|
|
16
16
|
"AssistantTextStartEvent",
|
|
17
|
+
"BacktrackEvent",
|
|
17
18
|
"BashCommandEndEvent",
|
|
18
19
|
"BashCommandOutputDeltaEvent",
|
|
19
20
|
"BashCommandStartEvent",
|
|
@@ -116,6 +117,14 @@ class CompactionEndEvent(Event):
|
|
|
116
117
|
kept_items_brief: list[message.KeptItemBrief] = Field(default_factory=list) # pyright: ignore[reportUnknownVariableType]
|
|
117
118
|
|
|
118
119
|
|
|
120
|
+
class BacktrackEvent(Event):
|
|
121
|
+
checkpoint_id: int
|
|
122
|
+
note: str
|
|
123
|
+
rationale: str
|
|
124
|
+
original_user_message: str
|
|
125
|
+
messages_discarded: int | None = None
|
|
126
|
+
|
|
127
|
+
|
|
119
128
|
class TaskFinishEvent(Event):
|
|
120
129
|
task_result: str
|
|
121
130
|
has_structured_output: bool = False
|
|
@@ -220,6 +229,7 @@ type ReplayEventUnion = (
|
|
|
220
229
|
| ErrorEvent
|
|
221
230
|
| CompactionStartEvent
|
|
222
231
|
| CompactionEndEvent
|
|
232
|
+
| BacktrackEvent
|
|
223
233
|
)
|
|
224
234
|
|
|
225
235
|
|
klaude_code/protocol/message.py
CHANGED
|
@@ -85,6 +85,15 @@ class CompactionEntry(BaseModel):
|
|
|
85
85
|
created_at: datetime = Field(default_factory=datetime.now)
|
|
86
86
|
|
|
87
87
|
|
|
88
|
+
class BacktrackEntry(BaseModel):
|
|
89
|
+
checkpoint_id: int
|
|
90
|
+
note: str
|
|
91
|
+
rationale: str
|
|
92
|
+
reverted_from_index: int
|
|
93
|
+
original_user_message: str
|
|
94
|
+
created_at: datetime = Field(default_factory=datetime.now)
|
|
95
|
+
|
|
96
|
+
|
|
88
97
|
# Part types
|
|
89
98
|
|
|
90
99
|
|
|
@@ -196,7 +205,7 @@ class ToolResultMessage(MessageBase):
|
|
|
196
205
|
|
|
197
206
|
Message = SystemMessage | DeveloperMessage | UserMessage | AssistantMessage | ToolResultMessage
|
|
198
207
|
|
|
199
|
-
HistoryEvent = Message | StreamErrorItem | TaskMetadataItem | CompactionEntry
|
|
208
|
+
HistoryEvent = Message | StreamErrorItem | TaskMetadataItem | CompactionEntry | BacktrackEntry
|
|
200
209
|
|
|
201
210
|
StreamItem = AssistantTextDelta | AssistantImageDelta | ThinkingTextDelta | ToolCallStartDelta
|
|
202
211
|
|
klaude_code/protocol/tools.py
CHANGED
klaude_code/session/session.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
import re
|
|
4
5
|
import time
|
|
5
6
|
import uuid
|
|
6
7
|
from collections.abc import Iterable, Sequence
|
|
@@ -15,6 +16,15 @@ from klaude_code.session.store import JsonlSessionStore, build_meta_snapshot
|
|
|
15
16
|
|
|
16
17
|
_DEFAULT_STORES: dict[str, JsonlSessionStore] = {}
|
|
17
18
|
|
|
19
|
+
_CHECKPOINT_RE = re.compile(r"<system>Checkpoint (\d+)</system>")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _extract_checkpoint_id(text: str) -> int | None:
|
|
23
|
+
match = _CHECKPOINT_RE.search(text)
|
|
24
|
+
if match is None:
|
|
25
|
+
return None
|
|
26
|
+
return int(match.group(1))
|
|
27
|
+
|
|
18
28
|
|
|
19
29
|
def _read_json_dict(path: Path) -> dict[str, Any] | None:
|
|
20
30
|
try:
|
|
@@ -51,6 +61,8 @@ class Session(BaseModel):
|
|
|
51
61
|
todos: list[model.TodoItem] = Field(default_factory=list) # pyright: ignore[reportUnknownVariableType]
|
|
52
62
|
model_name: str | None = None
|
|
53
63
|
|
|
64
|
+
next_checkpoint_id: int = 0
|
|
65
|
+
|
|
54
66
|
model_config_name: str | None = None
|
|
55
67
|
model_thinking: llm_param.Thinking | None = None
|
|
56
68
|
created_at: float = Field(default_factory=lambda: time.time())
|
|
@@ -153,6 +165,8 @@ class Session(BaseModel):
|
|
|
153
165
|
model_name = raw.get("model_name") if isinstance(raw.get("model_name"), str) else None
|
|
154
166
|
model_config_name = raw.get("model_config_name") if isinstance(raw.get("model_config_name"), str) else None
|
|
155
167
|
|
|
168
|
+
next_checkpoint_id = int(raw.get("next_checkpoint_id", 0))
|
|
169
|
+
|
|
156
170
|
model_thinking_raw = raw.get("model_thinking")
|
|
157
171
|
model_thinking = (
|
|
158
172
|
llm_param.Thinking.model_validate(model_thinking_raw) if isinstance(model_thinking_raw, dict) else None
|
|
@@ -169,6 +183,7 @@ class Session(BaseModel):
|
|
|
169
183
|
model_name=model_name,
|
|
170
184
|
model_config_name=model_config_name,
|
|
171
185
|
model_thinking=model_thinking,
|
|
186
|
+
next_checkpoint_id=next_checkpoint_id,
|
|
172
187
|
)
|
|
173
188
|
session._store = store
|
|
174
189
|
return session
|
|
@@ -221,19 +236,103 @@ class Session(BaseModel):
|
|
|
221
236
|
model_name=self.model_name,
|
|
222
237
|
model_config_name=self.model_config_name,
|
|
223
238
|
model_thinking=self.model_thinking,
|
|
239
|
+
next_checkpoint_id=self.next_checkpoint_id,
|
|
224
240
|
)
|
|
225
241
|
self._store.append_and_flush(session_id=self.id, items=items, meta=meta)
|
|
226
242
|
|
|
243
|
+
@property
|
|
244
|
+
def n_checkpoints(self) -> int:
|
|
245
|
+
return self.next_checkpoint_id
|
|
246
|
+
|
|
247
|
+
def create_checkpoint(self) -> int:
|
|
248
|
+
checkpoint_id = self.next_checkpoint_id
|
|
249
|
+
self.next_checkpoint_id += 1
|
|
250
|
+
checkpoint_msg = message.DeveloperMessage(
|
|
251
|
+
parts=[message.TextPart(text=f"<system>Checkpoint {checkpoint_id}</system>")]
|
|
252
|
+
)
|
|
253
|
+
self.append_history([checkpoint_msg])
|
|
254
|
+
return checkpoint_id
|
|
255
|
+
|
|
256
|
+
def find_checkpoint_index(self, checkpoint_id: int) -> int | None:
|
|
257
|
+
target_text = f"<system>Checkpoint {checkpoint_id}</system>"
|
|
258
|
+
for i, item in enumerate(self.conversation_history):
|
|
259
|
+
if not isinstance(item, message.DeveloperMessage):
|
|
260
|
+
continue
|
|
261
|
+
text = message.join_text_parts(item.parts)
|
|
262
|
+
if target_text in text:
|
|
263
|
+
return i
|
|
264
|
+
return None
|
|
265
|
+
|
|
266
|
+
def get_user_message_before_checkpoint(self, checkpoint_id: int) -> str | None:
|
|
267
|
+
checkpoint_idx = self.find_checkpoint_index(checkpoint_id)
|
|
268
|
+
if checkpoint_idx is None:
|
|
269
|
+
return None
|
|
270
|
+
|
|
271
|
+
for i in range(checkpoint_idx - 1, -1, -1):
|
|
272
|
+
item = self.conversation_history[i]
|
|
273
|
+
if isinstance(item, message.UserMessage):
|
|
274
|
+
return message.join_text_parts(item.parts)
|
|
275
|
+
return None
|
|
276
|
+
|
|
277
|
+
def get_checkpoint_user_messages(self) -> dict[int, str]:
|
|
278
|
+
checkpoints: dict[int, str] = {}
|
|
279
|
+
last_user_message = ""
|
|
280
|
+
for item in self.conversation_history:
|
|
281
|
+
if isinstance(item, message.UserMessage):
|
|
282
|
+
last_user_message = message.join_text_parts(item.parts)
|
|
283
|
+
continue
|
|
284
|
+
if not isinstance(item, message.DeveloperMessage):
|
|
285
|
+
continue
|
|
286
|
+
text = message.join_text_parts(item.parts)
|
|
287
|
+
checkpoint_id = _extract_checkpoint_id(text)
|
|
288
|
+
if checkpoint_id is None:
|
|
289
|
+
continue
|
|
290
|
+
checkpoints[checkpoint_id] = last_user_message
|
|
291
|
+
return checkpoints
|
|
292
|
+
|
|
293
|
+
def revert_to_checkpoint(self, checkpoint_id: int, note: str, rationale: str) -> message.BacktrackEntry:
|
|
294
|
+
target_idx = self.find_checkpoint_index(checkpoint_id)
|
|
295
|
+
if target_idx is None:
|
|
296
|
+
raise ValueError(f"Checkpoint {checkpoint_id} not found")
|
|
297
|
+
|
|
298
|
+
user_message = self.get_user_message_before_checkpoint(checkpoint_id) or ""
|
|
299
|
+
reverted_from = len(self.conversation_history)
|
|
300
|
+
entry = message.BacktrackEntry(
|
|
301
|
+
checkpoint_id=checkpoint_id,
|
|
302
|
+
note=note,
|
|
303
|
+
rationale=rationale,
|
|
304
|
+
reverted_from_index=reverted_from,
|
|
305
|
+
original_user_message=user_message,
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
self.conversation_history = self.conversation_history[: target_idx + 1]
|
|
309
|
+
self.next_checkpoint_id = checkpoint_id + 1
|
|
310
|
+
self._invalidate_messages_count_cache()
|
|
311
|
+
self._user_messages_cache = None
|
|
312
|
+
return entry
|
|
313
|
+
|
|
227
314
|
def get_llm_history(self) -> list[message.HistoryEvent]:
|
|
228
315
|
"""Return the LLM-facing history view with compaction summary injected."""
|
|
229
316
|
history = self.conversation_history
|
|
317
|
+
|
|
318
|
+
def _convert(item: message.HistoryEvent) -> message.HistoryEvent:
|
|
319
|
+
if isinstance(item, message.BacktrackEntry):
|
|
320
|
+
return message.DeveloperMessage(
|
|
321
|
+
parts=[
|
|
322
|
+
message.TextPart(
|
|
323
|
+
text=f"<system>After this, some operations were performed and context was refined via Backtrack. Rationale: {item.rationale}. Summary: {item.note}. Please continue.</system>"
|
|
324
|
+
)
|
|
325
|
+
]
|
|
326
|
+
)
|
|
327
|
+
return item
|
|
328
|
+
|
|
230
329
|
last_compaction: message.CompactionEntry | None = None
|
|
231
330
|
for item in reversed(history):
|
|
232
331
|
if isinstance(item, message.CompactionEntry):
|
|
233
332
|
last_compaction = item
|
|
234
333
|
break
|
|
235
334
|
if last_compaction is None:
|
|
236
|
-
return [it for it in history if not isinstance(it, message.CompactionEntry)]
|
|
335
|
+
return [_convert(it) for it in history if not isinstance(it, message.CompactionEntry)]
|
|
237
336
|
|
|
238
337
|
summary_message = message.UserMessage(parts=[message.TextPart(text=last_compaction.summary)])
|
|
239
338
|
kept = [it for it in history[last_compaction.first_kept_index :] if not isinstance(it, message.CompactionEntry)]
|
|
@@ -246,7 +345,7 @@ class Session(BaseModel):
|
|
|
246
345
|
first_non_tool += 1
|
|
247
346
|
kept = kept[first_non_tool:]
|
|
248
347
|
|
|
249
|
-
return [summary_message, *kept]
|
|
348
|
+
return [summary_message, *[_convert(it) for it in kept]]
|
|
250
349
|
|
|
251
350
|
def fork(self, *, new_id: str | None = None, until_index: int | None = None) -> Session:
|
|
252
351
|
"""Create a new session as a fork of the current session.
|
|
@@ -266,6 +365,7 @@ class Session(BaseModel):
|
|
|
266
365
|
forked.model_name = self.model_name
|
|
267
366
|
forked.model_config_name = self.model_config_name
|
|
268
367
|
forked.model_thinking = self.model_thinking.model_copy(deep=True) if self.model_thinking is not None else None
|
|
368
|
+
forked.next_checkpoint_id = self.next_checkpoint_id
|
|
269
369
|
forked.file_tracker = {k: v.model_copy(deep=True) for k, v in self.file_tracker.items()}
|
|
270
370
|
forked.todos = [todo.model_copy(deep=True) for todo in self.todos]
|
|
271
371
|
|
|
@@ -437,6 +537,15 @@ class Session(BaseModel):
|
|
|
437
537
|
yield events.DeveloperMessageEvent(session_id=self.id, item=dm)
|
|
438
538
|
case message.StreamErrorItem() as se:
|
|
439
539
|
yield events.ErrorEvent(error_message=se.error, can_retry=False, session_id=self.id)
|
|
540
|
+
case message.BacktrackEntry() as be:
|
|
541
|
+
yield events.BacktrackEvent(
|
|
542
|
+
session_id=self.id,
|
|
543
|
+
checkpoint_id=be.checkpoint_id,
|
|
544
|
+
note=be.note,
|
|
545
|
+
rationale=be.rationale,
|
|
546
|
+
original_user_message=be.original_user_message,
|
|
547
|
+
messages_discarded=None,
|
|
548
|
+
)
|
|
440
549
|
case message.CompactionEntry() as ce:
|
|
441
550
|
yield events.CompactionStartEvent(session_id=self.id, reason="threshold")
|
|
442
551
|
yield events.CompactionEndEvent(
|
klaude_code/session/store.py
CHANGED
|
@@ -169,6 +169,7 @@ def build_meta_snapshot(
|
|
|
169
169
|
model_name: str | None,
|
|
170
170
|
model_config_name: str | None,
|
|
171
171
|
model_thinking: llm_param.Thinking | None,
|
|
172
|
+
next_checkpoint_id: int = 0,
|
|
172
173
|
) -> dict[str, Any]:
|
|
173
174
|
return {
|
|
174
175
|
"id": session_id,
|
|
@@ -186,4 +187,5 @@ def build_meta_snapshot(
|
|
|
186
187
|
"model_thinking": model_thinking.model_dump(mode="json", exclude_defaults=True, exclude_none=True)
|
|
187
188
|
if model_thinking
|
|
188
189
|
else None,
|
|
190
|
+
"next_checkpoint_id": next_checkpoint_id,
|
|
189
191
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: executing-plans
|
|
3
|
+
description: Use when you have a written implementation plan to execute in a separate session with review checkpoints
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Executing Plans
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
Load plan, review critically, execute tasks in batches, report for review between batches.
|
|
11
|
+
|
|
12
|
+
**Core principle:** Batch execution with checkpoints for architect review.
|
|
13
|
+
|
|
14
|
+
**Announce at start:** "I'm using the executing-plans skill to implement this plan."
|
|
15
|
+
|
|
16
|
+
## The Process
|
|
17
|
+
|
|
18
|
+
### Step 1: Load and Review Plan
|
|
19
|
+
1. Read plan file
|
|
20
|
+
2. Review critically - identify any questions or concerns about the plan
|
|
21
|
+
3. If concerns: Raise them with your human partner before starting
|
|
22
|
+
4. If no concerns: Create TodoWrite and proceed
|
|
23
|
+
|
|
24
|
+
### Step 2: Execute Batch
|
|
25
|
+
**Default: First 3 tasks**
|
|
26
|
+
|
|
27
|
+
For each task:
|
|
28
|
+
1. Mark as in_progress
|
|
29
|
+
2. Follow each step exactly (plan has bite-sized steps)
|
|
30
|
+
3. Run verifications as specified
|
|
31
|
+
4. Mark as completed
|
|
32
|
+
|
|
33
|
+
### Step 3: Report
|
|
34
|
+
When batch complete:
|
|
35
|
+
- Show what was implemented
|
|
36
|
+
- Show verification output
|
|
37
|
+
- Say: "Ready for feedback."
|
|
38
|
+
|
|
39
|
+
### Step 4: Continue
|
|
40
|
+
Based on feedback:
|
|
41
|
+
- Apply changes if needed
|
|
42
|
+
- Execute next batch
|
|
43
|
+
- Repeat until complete
|
|
44
|
+
|
|
45
|
+
### Step 5: Complete Development
|
|
46
|
+
|
|
47
|
+
After all tasks complete and verified:
|
|
48
|
+
- Announce: "I'm using the finishing-a-development-branch skill to complete this work."
|
|
49
|
+
- **REQUIRED SUB-SKILL:** Use superpowers:finishing-a-development-branch
|
|
50
|
+
- Follow that skill to verify tests, present options, execute choice
|
|
51
|
+
|
|
52
|
+
## When to Stop and Ask for Help
|
|
53
|
+
|
|
54
|
+
**STOP executing immediately when:**
|
|
55
|
+
- Hit a blocker mid-batch (missing dependency, test fails, instruction unclear)
|
|
56
|
+
- Plan has critical gaps preventing starting
|
|
57
|
+
- You don't understand an instruction
|
|
58
|
+
- Verification fails repeatedly
|
|
59
|
+
|
|
60
|
+
**Ask for clarification rather than guessing.**
|
|
61
|
+
|
|
62
|
+
## When to Revisit Earlier Steps
|
|
63
|
+
|
|
64
|
+
**Return to Review (Step 1) when:**
|
|
65
|
+
- Partner updates the plan based on your feedback
|
|
66
|
+
- Fundamental approach needs rethinking
|
|
67
|
+
|
|
68
|
+
**Don't force through blockers** - stop and ask.
|
|
69
|
+
|
|
70
|
+
## Remember
|
|
71
|
+
- Review plan critically first
|
|
72
|
+
- Follow plan steps exactly
|
|
73
|
+
- Don't skip verifications
|
|
74
|
+
- Reference skills when plan says to
|
|
75
|
+
- Between batches: just report and wait
|
|
76
|
+
- Stop when blocked, don't guess
|
|
77
|
+
- Never start implementation on main/master branch without explicit user consent
|
|
78
|
+
|
|
79
|
+
## Integration
|
|
80
|
+
|
|
81
|
+
**Required workflow skills:**
|
|
82
|
+
- **superpowers:using-git-worktrees** - REQUIRED: Set up isolated workspace before starting
|
|
83
|
+
- **superpowers:writing-plans** - Creates the plan this skill executes
|
|
84
|
+
- **superpowers:finishing-a-development-branch** - Complete development after all tasks
|