klaude-code 2.10.2__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 +7 -35
- 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/anthropic/input.py +28 -4
- 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/errors.py +2 -4
- klaude_code/tui/components/metadata.py +5 -10
- klaude_code/tui/components/rich/markdown.py +5 -1
- klaude_code/tui/components/rich/status.py +7 -76
- klaude_code/tui/components/rich/theme.py +12 -2
- klaude_code/tui/components/tools.py +31 -18
- klaude_code/tui/components/user_input.py +1 -1
- klaude_code/tui/display.py +4 -0
- klaude_code/tui/input/completers.py +51 -17
- klaude_code/tui/input/images.py +127 -0
- klaude_code/tui/input/prompt_toolkit.py +16 -2
- 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.2.dist-info → klaude_code-2.10.4.dist-info}/METADATA +1 -1
- {klaude_code-2.10.2.dist-info → klaude_code-2.10.4.dist-info}/RECORD +53 -56
- 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.2.dist-info → klaude_code-2.10.4.dist-info}/WHEEL +0 -0
- {klaude_code-2.10.2.dist-info → klaude_code-2.10.4.dist-info}/entry_points.txt +0 -0
klaude_code/core/task.py
CHANGED
|
@@ -7,6 +7,7 @@ from dataclasses import dataclass
|
|
|
7
7
|
|
|
8
8
|
from klaude_code.const import INITIAL_RETRY_DELAY_S, MAX_FAILED_TURN_RETRIES, MAX_RETRY_DELAY_S
|
|
9
9
|
from klaude_code.core.agent_profile import AgentProfile, Reminder
|
|
10
|
+
from klaude_code.core.backtrack import BacktrackManager
|
|
10
11
|
from klaude_code.core.compaction import (
|
|
11
12
|
CompactionReason,
|
|
12
13
|
is_context_overflow,
|
|
@@ -178,6 +179,7 @@ class TaskExecutor:
|
|
|
178
179
|
self._current_turn: TurnExecutor | None = None
|
|
179
180
|
self._started_at: float = 0.0
|
|
180
181
|
self._metadata_accumulator: MetadataAccumulator | None = None
|
|
182
|
+
self._backtrack_manager: BacktrackManager | None = None
|
|
181
183
|
|
|
182
184
|
def get_partial_metadata(self) -> model.TaskMetadata | None:
|
|
183
185
|
"""Get the currently accumulated metadata without finalizing.
|
|
@@ -221,6 +223,11 @@ class TaskExecutor:
|
|
|
221
223
|
session_ctx = ctx.session_ctx
|
|
222
224
|
self._started_at = time.perf_counter()
|
|
223
225
|
|
|
226
|
+
if ctx.sub_agent_state is None:
|
|
227
|
+
self._backtrack_manager = BacktrackManager()
|
|
228
|
+
self._backtrack_manager.set_n_checkpoints(ctx.session.n_checkpoints)
|
|
229
|
+
self._backtrack_manager.sync_checkpoints(ctx.session.get_checkpoint_user_messages())
|
|
230
|
+
|
|
224
231
|
yield events.TaskStartEvent(
|
|
225
232
|
session_id=session_ctx.session_id,
|
|
226
233
|
sub_agent_state=ctx.sub_agent_state,
|
|
@@ -262,6 +269,9 @@ class TaskExecutor:
|
|
|
262
269
|
log_debug("[Compact] result", str(result.to_entry()), debug_type=DebugType.RESPONSE)
|
|
263
270
|
|
|
264
271
|
session_ctx.append_history([result.to_entry()])
|
|
272
|
+
if self._backtrack_manager is not None:
|
|
273
|
+
self._backtrack_manager.set_n_checkpoints(ctx.session.n_checkpoints)
|
|
274
|
+
self._backtrack_manager.sync_checkpoints(ctx.session.get_checkpoint_user_messages())
|
|
265
275
|
yield events.CompactionEndEvent(
|
|
266
276
|
session_id=session_ctx.session_id,
|
|
267
277
|
reason=CompactionReason.THRESHOLD.value,
|
|
@@ -298,6 +308,12 @@ class TaskExecutor:
|
|
|
298
308
|
will_retry=False,
|
|
299
309
|
)
|
|
300
310
|
|
|
311
|
+
if self._backtrack_manager is not None:
|
|
312
|
+
checkpoint_id = ctx.session.create_checkpoint()
|
|
313
|
+
self._backtrack_manager.set_n_checkpoints(ctx.session.n_checkpoints)
|
|
314
|
+
user_msg = ctx.session.get_user_message_before_checkpoint(checkpoint_id) or ""
|
|
315
|
+
self._backtrack_manager.register_checkpoint(checkpoint_id, user_msg)
|
|
316
|
+
|
|
301
317
|
turn_context = TurnExecutionContext(
|
|
302
318
|
session_ctx=session_ctx,
|
|
303
319
|
llm_client=profile.llm_client,
|
|
@@ -305,6 +321,7 @@ class TaskExecutor:
|
|
|
305
321
|
tools=profile.tools,
|
|
306
322
|
tool_registry=ctx.tool_registry,
|
|
307
323
|
sub_agent_state=ctx.sub_agent_state,
|
|
324
|
+
backtrack_manager=self._backtrack_manager,
|
|
308
325
|
)
|
|
309
326
|
|
|
310
327
|
turn: TurnExecutor | None = None
|
|
@@ -354,6 +371,9 @@ class TaskExecutor:
|
|
|
354
371
|
"[Compact:Overflow] result", str(result.to_entry()), debug_type=DebugType.RESPONSE
|
|
355
372
|
)
|
|
356
373
|
session_ctx.append_history([result.to_entry()])
|
|
374
|
+
if self._backtrack_manager is not None:
|
|
375
|
+
self._backtrack_manager.set_n_checkpoints(ctx.session.n_checkpoints)
|
|
376
|
+
self._backtrack_manager.sync_checkpoints(ctx.session.get_checkpoint_user_messages())
|
|
357
377
|
yield events.CompactionEndEvent(
|
|
358
378
|
session_id=session_ctx.session_id,
|
|
359
379
|
reason=CompactionReason.OVERFLOW.value,
|
|
@@ -414,14 +434,40 @@ class TaskExecutor:
|
|
|
414
434
|
yield events.ErrorEvent(error_message=final_error, can_retry=False, session_id=session_ctx.session_id)
|
|
415
435
|
return
|
|
416
436
|
|
|
417
|
-
if
|
|
418
|
-
|
|
419
|
-
if
|
|
420
|
-
|
|
421
|
-
|
|
437
|
+
if self._backtrack_manager is not None:
|
|
438
|
+
pending = self._backtrack_manager.fetch_pending()
|
|
439
|
+
if pending is not None:
|
|
440
|
+
try:
|
|
441
|
+
entry = ctx.session.revert_to_checkpoint(pending.checkpoint_id, pending.note, pending.rationale)
|
|
442
|
+
except ValueError as exc:
|
|
443
|
+
yield events.ErrorEvent(
|
|
444
|
+
error_message=str(exc),
|
|
445
|
+
can_retry=False,
|
|
446
|
+
session_id=session_ctx.session_id,
|
|
447
|
+
)
|
|
422
448
|
else:
|
|
423
|
-
|
|
424
|
-
|
|
449
|
+
messages_discarded = entry.reverted_from_index - len(ctx.session.conversation_history)
|
|
450
|
+
session_ctx.append_history([entry])
|
|
451
|
+
self._backtrack_manager.set_n_checkpoints(ctx.session.n_checkpoints)
|
|
452
|
+
self._backtrack_manager.sync_checkpoints(ctx.session.get_checkpoint_user_messages())
|
|
453
|
+
yield events.BacktrackEvent(
|
|
454
|
+
session_id=session_ctx.session_id,
|
|
455
|
+
checkpoint_id=pending.checkpoint_id,
|
|
456
|
+
note=pending.note,
|
|
457
|
+
rationale=pending.rationale,
|
|
458
|
+
original_user_message=entry.original_user_message,
|
|
459
|
+
messages_discarded=messages_discarded,
|
|
460
|
+
)
|
|
461
|
+
continue
|
|
462
|
+
|
|
463
|
+
if turn is None or turn.task_finished:
|
|
464
|
+
# Empty result should retry only for sub-agents
|
|
465
|
+
if turn is not None and not turn.task_result.strip() and ctx.sub_agent_state is not None:
|
|
466
|
+
yield events.ErrorEvent(
|
|
467
|
+
error_message="Sub-agent returned empty result, retrying…",
|
|
468
|
+
can_retry=True,
|
|
469
|
+
session_id=session_ctx.session_id,
|
|
470
|
+
)
|
|
425
471
|
continue
|
|
426
472
|
break
|
|
427
473
|
|
|
@@ -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(
|
|
@@ -107,11 +107,20 @@ def _tool_blocks_to_message(blocks: list[BetaToolResultBlockParam]) -> BetaMessa
|
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
|
|
110
|
+
def _model_supports_unsigned_thinking(model_name: str | None) -> bool:
|
|
111
|
+
"""Check if the model supports thinking blocks without signature (e.g., kimi, deepseek)."""
|
|
112
|
+
if not model_name:
|
|
113
|
+
return False
|
|
114
|
+
model_lower = model_name.lower()
|
|
115
|
+
return "kimi" in model_lower or "deepseek" in model_lower
|
|
116
|
+
|
|
117
|
+
|
|
110
118
|
def _assistant_message_to_message(msg: message.AssistantMessage, model_name: str | None) -> BetaMessageParam:
|
|
111
119
|
content: list[BetaContentBlockParam] = []
|
|
112
120
|
current_thinking_content: str | None = None
|
|
113
121
|
native_thinking_parts, _ = split_thinking_parts(msg, model_name)
|
|
114
122
|
native_thinking_ids = {id(part) for part in native_thinking_parts}
|
|
123
|
+
supports_unsigned = _model_supports_unsigned_thinking(model_name)
|
|
115
124
|
|
|
116
125
|
def _degraded_thinking_block(text: str) -> BetaTextBlockParam | None:
|
|
117
126
|
stripped = text.strip()
|
|
@@ -125,11 +134,18 @@ def _assistant_message_to_message(msg: message.AssistantMessage, model_name: str
|
|
|
125
134
|
},
|
|
126
135
|
)
|
|
127
136
|
|
|
128
|
-
def
|
|
137
|
+
def _flush_thinking() -> None:
|
|
129
138
|
nonlocal current_thinking_content
|
|
130
139
|
if current_thinking_content is None:
|
|
131
140
|
return
|
|
132
|
-
if
|
|
141
|
+
if supports_unsigned:
|
|
142
|
+
content.append(
|
|
143
|
+
cast(
|
|
144
|
+
BetaContentBlockParam,
|
|
145
|
+
{"type": "thinking", "thinking": current_thinking_content},
|
|
146
|
+
)
|
|
147
|
+
)
|
|
148
|
+
elif block := _degraded_thinking_block(current_thinking_content):
|
|
133
149
|
content.append(block)
|
|
134
150
|
current_thinking_content = None
|
|
135
151
|
|
|
@@ -156,9 +172,17 @@ def _assistant_message_to_message(msg: message.AssistantMessage, model_name: str
|
|
|
156
172
|
)
|
|
157
173
|
)
|
|
158
174
|
current_thinking_content = None
|
|
175
|
+
elif supports_unsigned:
|
|
176
|
+
content.append(
|
|
177
|
+
cast(
|
|
178
|
+
BetaContentBlockParam,
|
|
179
|
+
{"type": "thinking", "thinking": current_thinking_content or ""},
|
|
180
|
+
)
|
|
181
|
+
)
|
|
182
|
+
current_thinking_content = None
|
|
159
183
|
continue
|
|
160
184
|
|
|
161
|
-
|
|
185
|
+
_flush_thinking()
|
|
162
186
|
if isinstance(part, message.TextPart):
|
|
163
187
|
content.append(cast(BetaTextBlockParam, {"type": "text", "text": part.text}))
|
|
164
188
|
elif isinstance(part, message.ToolCallPart):
|
|
@@ -182,7 +206,7 @@ def _assistant_message_to_message(msg: message.AssistantMessage, model_name: str
|
|
|
182
206
|
)
|
|
183
207
|
)
|
|
184
208
|
|
|
185
|
-
|
|
209
|
+
_flush_thinking()
|
|
186
210
|
|
|
187
211
|
return {"role": "assistant", "content": content}
|
|
188
212
|
|
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