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.
Files changed (63) hide show
  1. klaude_code/auth/AGENTS.md +4 -24
  2. klaude_code/auth/__init__.py +1 -17
  3. klaude_code/cli/auth_cmd.py +3 -53
  4. klaude_code/cli/list_model.py +0 -50
  5. klaude_code/config/assets/builtin_config.yaml +7 -35
  6. klaude_code/config/config.py +5 -42
  7. klaude_code/const.py +5 -2
  8. klaude_code/core/agent_profile.py +2 -10
  9. klaude_code/core/backtrack/__init__.py +3 -0
  10. klaude_code/core/backtrack/manager.py +48 -0
  11. klaude_code/core/memory.py +25 -9
  12. klaude_code/core/task.py +53 -7
  13. klaude_code/core/tool/__init__.py +2 -0
  14. klaude_code/core/tool/backtrack/__init__.py +3 -0
  15. klaude_code/core/tool/backtrack/backtrack_tool.md +17 -0
  16. klaude_code/core/tool/backtrack/backtrack_tool.py +65 -0
  17. klaude_code/core/tool/context.py +5 -0
  18. klaude_code/core/turn.py +3 -0
  19. klaude_code/llm/anthropic/input.py +28 -4
  20. klaude_code/llm/input_common.py +70 -1
  21. klaude_code/llm/openai_compatible/input.py +5 -2
  22. klaude_code/llm/openrouter/input.py +5 -2
  23. klaude_code/llm/registry.py +0 -1
  24. klaude_code/protocol/events.py +10 -0
  25. klaude_code/protocol/llm_param.py +0 -1
  26. klaude_code/protocol/message.py +10 -1
  27. klaude_code/protocol/tools.py +1 -0
  28. klaude_code/session/session.py +111 -2
  29. klaude_code/session/store.py +2 -0
  30. klaude_code/skill/assets/executing-plans/SKILL.md +84 -0
  31. klaude_code/skill/assets/writing-plans/SKILL.md +116 -0
  32. klaude_code/tui/commands.py +15 -0
  33. klaude_code/tui/components/developer.py +1 -1
  34. klaude_code/tui/components/errors.py +2 -4
  35. klaude_code/tui/components/metadata.py +5 -10
  36. klaude_code/tui/components/rich/markdown.py +5 -1
  37. klaude_code/tui/components/rich/status.py +7 -76
  38. klaude_code/tui/components/rich/theme.py +12 -2
  39. klaude_code/tui/components/tools.py +31 -18
  40. klaude_code/tui/components/user_input.py +1 -1
  41. klaude_code/tui/display.py +4 -0
  42. klaude_code/tui/input/completers.py +51 -17
  43. klaude_code/tui/input/images.py +127 -0
  44. klaude_code/tui/input/prompt_toolkit.py +16 -2
  45. klaude_code/tui/machine.py +26 -8
  46. klaude_code/tui/renderer.py +97 -0
  47. klaude_code/tui/runner.py +7 -2
  48. klaude_code/tui/terminal/image.py +28 -12
  49. klaude_code/ui/terminal/title.py +8 -3
  50. {klaude_code-2.10.2.dist-info → klaude_code-2.10.4.dist-info}/METADATA +1 -1
  51. {klaude_code-2.10.2.dist-info → klaude_code-2.10.4.dist-info}/RECORD +53 -56
  52. klaude_code/auth/antigravity/__init__.py +0 -20
  53. klaude_code/auth/antigravity/exceptions.py +0 -17
  54. klaude_code/auth/antigravity/oauth.py +0 -315
  55. klaude_code/auth/antigravity/pkce.py +0 -25
  56. klaude_code/auth/antigravity/token_manager.py +0 -27
  57. klaude_code/core/prompts/prompt-antigravity.md +0 -80
  58. klaude_code/llm/antigravity/__init__.py +0 -3
  59. klaude_code/llm/antigravity/client.py +0 -558
  60. klaude_code/llm/antigravity/input.py +0 -268
  61. klaude_code/skill/assets/create-plan/SKILL.md +0 -74
  62. {klaude_code-2.10.2.dist-info → klaude_code-2.10.4.dist-info}/WHEEL +0 -0
  63. {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 turn is None or turn.task_finished:
418
- # Empty result should retry instead of finishing
419
- if turn is not None and not turn.task_result.strip():
420
- if ctx.sub_agent_state is not None:
421
- error_msg = "Sub-agent returned empty result, retrying…"
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
- error_msg = "Agent returned empty result, retrying…"
424
- yield events.ErrorEvent(error_message=error_msg, can_retry=True, session_id=session_ctx.session_id)
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,3 @@
1
+ from klaude_code.core.tool.backtrack.backtrack_tool import BacktrackTool
2
+
3
+ __all__ = ["BacktrackTool"]
@@ -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)
@@ -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 _flush_thinking_as_text_block() -> None:
137
+ def _flush_thinking() -> None:
129
138
  nonlocal current_thinking_content
130
139
  if current_thinking_content is None:
131
140
  return
132
- if block := _degraded_thinking_block(current_thinking_content):
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
- _flush_thinking_as_text_block()
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
- _flush_thinking_as_text_block()
209
+ _flush_thinking()
186
210
 
187
211
  return {"role": "assistant", "content": content}
188
212
 
@@ -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": [{"type": "text", "text": merged_text}],
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
- build_tool_message,
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
- messages.append(cast(chat.ChatCompletionMessageParam, build_tool_message(msg, attachment)))
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
- build_tool_message,
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
- messages.append(cast(chat.ChatCompletionMessageParam, build_tool_message(msg, attachment)))
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 _:
@@ -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
 
@@ -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
 
@@ -16,7 +16,6 @@ class LLMClientProtocol(Enum):
16
16
  BEDROCK = "bedrock"
17
17
  CODEX_OAUTH = "codex_oauth"
18
18
  GOOGLE = "google"
19
- ANTIGRAVITY = "antigravity"
20
19
 
21
20
 
22
21
  class ToolSchema(BaseModel):
@@ -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
 
@@ -12,6 +12,7 @@ WEB_SEARCH = "WebSearch"
12
12
  REPORT_BACK = "report_back"
13
13
  TASK = "Task"
14
14
  IMAGE_GEN = "ImageGen"
15
+ BACKTRACK = "Backtrack"
15
16
 
16
17
  # SubAgentType is just a string alias now; agent types are defined via SubAgentProfile
17
18
  SubAgentType = str