klaude-code 1.2.6__py3-none-any.whl → 1.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.
Files changed (205) hide show
  1. klaude_code/auth/__init__.py +24 -0
  2. klaude_code/auth/codex/__init__.py +20 -0
  3. klaude_code/auth/codex/exceptions.py +17 -0
  4. klaude_code/auth/codex/jwt_utils.py +45 -0
  5. klaude_code/auth/codex/oauth.py +229 -0
  6. klaude_code/auth/codex/token_manager.py +84 -0
  7. klaude_code/cli/auth_cmd.py +73 -0
  8. klaude_code/cli/config_cmd.py +91 -0
  9. klaude_code/cli/cost_cmd.py +338 -0
  10. klaude_code/cli/debug.py +78 -0
  11. klaude_code/cli/list_model.py +307 -0
  12. klaude_code/cli/main.py +233 -134
  13. klaude_code/cli/runtime.py +309 -117
  14. klaude_code/{version.py → cli/self_update.py} +114 -5
  15. klaude_code/cli/session_cmd.py +37 -21
  16. klaude_code/command/__init__.py +88 -27
  17. klaude_code/command/clear_cmd.py +8 -7
  18. klaude_code/command/command_abc.py +31 -31
  19. klaude_code/command/debug_cmd.py +79 -0
  20. klaude_code/command/export_cmd.py +19 -53
  21. klaude_code/command/export_online_cmd.py +154 -0
  22. klaude_code/command/fork_session_cmd.py +267 -0
  23. klaude_code/command/help_cmd.py +7 -8
  24. klaude_code/command/model_cmd.py +60 -10
  25. klaude_code/command/model_select.py +84 -0
  26. klaude_code/command/prompt-jj-describe.md +32 -0
  27. klaude_code/command/prompt_command.py +19 -11
  28. klaude_code/command/refresh_cmd.py +8 -10
  29. klaude_code/command/registry.py +139 -40
  30. klaude_code/command/release_notes_cmd.py +84 -0
  31. klaude_code/command/resume_cmd.py +111 -0
  32. klaude_code/command/status_cmd.py +104 -60
  33. klaude_code/command/terminal_setup_cmd.py +7 -9
  34. klaude_code/command/thinking_cmd.py +98 -0
  35. klaude_code/config/__init__.py +14 -6
  36. klaude_code/config/assets/__init__.py +1 -0
  37. klaude_code/config/assets/builtin_config.yaml +303 -0
  38. klaude_code/config/builtin_config.py +38 -0
  39. klaude_code/config/config.py +378 -109
  40. klaude_code/config/select_model.py +117 -53
  41. klaude_code/config/thinking.py +269 -0
  42. klaude_code/{const/__init__.py → const.py} +50 -19
  43. klaude_code/core/agent.py +20 -28
  44. klaude_code/core/executor.py +327 -112
  45. klaude_code/core/manager/__init__.py +2 -4
  46. klaude_code/core/manager/llm_clients.py +1 -15
  47. klaude_code/core/manager/llm_clients_builder.py +10 -11
  48. klaude_code/core/manager/sub_agent_manager.py +37 -6
  49. klaude_code/core/prompt.py +63 -44
  50. klaude_code/core/prompts/prompt-claude-code.md +2 -13
  51. klaude_code/core/prompts/prompt-codex-gpt-5-1-codex-max.md +117 -0
  52. klaude_code/core/prompts/prompt-codex-gpt-5-2-codex.md +117 -0
  53. klaude_code/core/prompts/prompt-codex.md +9 -42
  54. klaude_code/core/prompts/prompt-minimal.md +12 -0
  55. klaude_code/core/prompts/{prompt-subagent-explore.md → prompt-sub-agent-explore.md} +16 -3
  56. klaude_code/core/prompts/{prompt-subagent-oracle.md → prompt-sub-agent-oracle.md} +1 -2
  57. klaude_code/core/prompts/prompt-sub-agent-web.md +51 -0
  58. klaude_code/core/reminders.py +283 -95
  59. klaude_code/core/task.py +113 -75
  60. klaude_code/core/tool/__init__.py +24 -31
  61. klaude_code/core/tool/file/_utils.py +36 -0
  62. klaude_code/core/tool/file/apply_patch.py +17 -25
  63. klaude_code/core/tool/file/apply_patch_tool.py +57 -77
  64. klaude_code/core/tool/file/diff_builder.py +151 -0
  65. klaude_code/core/tool/file/edit_tool.py +50 -63
  66. klaude_code/core/tool/file/move_tool.md +41 -0
  67. klaude_code/core/tool/file/move_tool.py +435 -0
  68. klaude_code/core/tool/file/read_tool.md +1 -1
  69. klaude_code/core/tool/file/read_tool.py +86 -86
  70. klaude_code/core/tool/file/write_tool.py +59 -69
  71. klaude_code/core/tool/report_back_tool.py +84 -0
  72. klaude_code/core/tool/shell/bash_tool.py +265 -22
  73. klaude_code/core/tool/shell/command_safety.py +3 -6
  74. klaude_code/core/tool/{memory → skill}/skill_tool.py +16 -26
  75. klaude_code/core/tool/sub_agent_tool.py +13 -2
  76. klaude_code/core/tool/todo/todo_write_tool.md +0 -157
  77. klaude_code/core/tool/todo/todo_write_tool.py +1 -1
  78. klaude_code/core/tool/todo/todo_write_tool_raw.md +182 -0
  79. klaude_code/core/tool/todo/update_plan_tool.py +1 -1
  80. klaude_code/core/tool/tool_abc.py +18 -0
  81. klaude_code/core/tool/tool_context.py +27 -12
  82. klaude_code/core/tool/tool_registry.py +7 -7
  83. klaude_code/core/tool/tool_runner.py +44 -36
  84. klaude_code/core/tool/truncation.py +29 -14
  85. klaude_code/core/tool/web/mermaid_tool.md +43 -0
  86. klaude_code/core/tool/web/mermaid_tool.py +2 -5
  87. klaude_code/core/tool/web/web_fetch_tool.md +1 -1
  88. klaude_code/core/tool/web/web_fetch_tool.py +112 -22
  89. klaude_code/core/tool/web/web_search_tool.md +23 -0
  90. klaude_code/core/tool/web/web_search_tool.py +130 -0
  91. klaude_code/core/turn.py +168 -66
  92. klaude_code/llm/__init__.py +2 -10
  93. klaude_code/llm/anthropic/client.py +190 -178
  94. klaude_code/llm/anthropic/input.py +39 -15
  95. klaude_code/llm/bedrock/__init__.py +3 -0
  96. klaude_code/llm/bedrock/client.py +60 -0
  97. klaude_code/llm/client.py +7 -21
  98. klaude_code/llm/codex/__init__.py +5 -0
  99. klaude_code/llm/codex/client.py +149 -0
  100. klaude_code/llm/google/__init__.py +3 -0
  101. klaude_code/llm/google/client.py +309 -0
  102. klaude_code/llm/google/input.py +215 -0
  103. klaude_code/llm/input_common.py +3 -9
  104. klaude_code/llm/openai_compatible/client.py +72 -164
  105. klaude_code/llm/openai_compatible/input.py +6 -4
  106. klaude_code/llm/openai_compatible/stream.py +273 -0
  107. klaude_code/llm/openai_compatible/tool_call_accumulator.py +17 -1
  108. klaude_code/llm/openrouter/client.py +89 -160
  109. klaude_code/llm/openrouter/input.py +18 -30
  110. klaude_code/llm/openrouter/reasoning.py +118 -0
  111. klaude_code/llm/registry.py +39 -7
  112. klaude_code/llm/responses/client.py +184 -171
  113. klaude_code/llm/responses/input.py +20 -1
  114. klaude_code/llm/usage.py +17 -12
  115. klaude_code/protocol/commands.py +17 -1
  116. klaude_code/protocol/events.py +31 -4
  117. klaude_code/protocol/llm_param.py +13 -10
  118. klaude_code/protocol/model.py +232 -29
  119. klaude_code/protocol/op.py +90 -1
  120. klaude_code/protocol/op_handler.py +35 -1
  121. klaude_code/protocol/sub_agent/__init__.py +117 -0
  122. klaude_code/protocol/sub_agent/explore.py +63 -0
  123. klaude_code/protocol/sub_agent/oracle.py +91 -0
  124. klaude_code/protocol/sub_agent/task.py +61 -0
  125. klaude_code/protocol/sub_agent/web.py +79 -0
  126. klaude_code/protocol/tools.py +4 -2
  127. klaude_code/session/__init__.py +2 -2
  128. klaude_code/session/codec.py +71 -0
  129. klaude_code/session/export.py +293 -86
  130. klaude_code/session/selector.py +89 -67
  131. klaude_code/session/session.py +320 -309
  132. klaude_code/session/store.py +220 -0
  133. klaude_code/session/templates/export_session.html +595 -83
  134. klaude_code/session/templates/mermaid_viewer.html +926 -0
  135. klaude_code/skill/__init__.py +27 -0
  136. klaude_code/skill/assets/deslop/SKILL.md +17 -0
  137. klaude_code/skill/assets/dev-docs/SKILL.md +108 -0
  138. klaude_code/skill/assets/handoff/SKILL.md +39 -0
  139. klaude_code/skill/assets/jj-workspace/SKILL.md +20 -0
  140. klaude_code/skill/assets/skill-creator/SKILL.md +139 -0
  141. klaude_code/{core/tool/memory/skill_loader.py → skill/loader.py} +55 -15
  142. klaude_code/skill/manager.py +70 -0
  143. klaude_code/skill/system_skills.py +192 -0
  144. klaude_code/trace/__init__.py +20 -2
  145. klaude_code/trace/log.py +150 -5
  146. klaude_code/ui/__init__.py +4 -9
  147. klaude_code/ui/core/input.py +1 -1
  148. klaude_code/ui/core/stage_manager.py +7 -7
  149. klaude_code/ui/modes/debug/display.py +2 -1
  150. klaude_code/ui/modes/repl/__init__.py +3 -48
  151. klaude_code/ui/modes/repl/clipboard.py +5 -5
  152. klaude_code/ui/modes/repl/completers.py +487 -123
  153. klaude_code/ui/modes/repl/display.py +5 -4
  154. klaude_code/ui/modes/repl/event_handler.py +370 -117
  155. klaude_code/ui/modes/repl/input_prompt_toolkit.py +552 -105
  156. klaude_code/ui/modes/repl/key_bindings.py +146 -23
  157. klaude_code/ui/modes/repl/renderer.py +189 -99
  158. klaude_code/ui/renderers/assistant.py +9 -2
  159. klaude_code/ui/renderers/bash_syntax.py +178 -0
  160. klaude_code/ui/renderers/common.py +78 -0
  161. klaude_code/ui/renderers/developer.py +104 -48
  162. klaude_code/ui/renderers/diffs.py +87 -6
  163. klaude_code/ui/renderers/errors.py +11 -6
  164. klaude_code/ui/renderers/mermaid_viewer.py +57 -0
  165. klaude_code/ui/renderers/metadata.py +112 -76
  166. klaude_code/ui/renderers/sub_agent.py +92 -7
  167. klaude_code/ui/renderers/thinking.py +40 -18
  168. klaude_code/ui/renderers/tools.py +405 -227
  169. klaude_code/ui/renderers/user_input.py +73 -13
  170. klaude_code/ui/rich/__init__.py +10 -1
  171. klaude_code/ui/rich/cjk_wrap.py +228 -0
  172. klaude_code/ui/rich/code_panel.py +131 -0
  173. klaude_code/ui/rich/live.py +17 -0
  174. klaude_code/ui/rich/markdown.py +305 -170
  175. klaude_code/ui/rich/searchable_text.py +10 -13
  176. klaude_code/ui/rich/status.py +190 -49
  177. klaude_code/ui/rich/theme.py +135 -39
  178. klaude_code/ui/terminal/__init__.py +55 -0
  179. klaude_code/ui/terminal/color.py +1 -1
  180. klaude_code/ui/terminal/control.py +13 -22
  181. klaude_code/ui/terminal/notifier.py +44 -4
  182. klaude_code/ui/terminal/selector.py +658 -0
  183. klaude_code/ui/utils/common.py +0 -18
  184. klaude_code-1.8.0.dist-info/METADATA +377 -0
  185. klaude_code-1.8.0.dist-info/RECORD +219 -0
  186. {klaude_code-1.2.6.dist-info → klaude_code-1.8.0.dist-info}/entry_points.txt +1 -0
  187. klaude_code/command/diff_cmd.py +0 -138
  188. klaude_code/command/prompt-dev-docs-update.md +0 -56
  189. klaude_code/command/prompt-dev-docs.md +0 -46
  190. klaude_code/config/list_model.py +0 -162
  191. klaude_code/core/manager/agent_manager.py +0 -127
  192. klaude_code/core/prompts/prompt-subagent-webfetch.md +0 -46
  193. klaude_code/core/tool/file/multi_edit_tool.md +0 -42
  194. klaude_code/core/tool/file/multi_edit_tool.py +0 -199
  195. klaude_code/core/tool/memory/memory_tool.md +0 -16
  196. klaude_code/core/tool/memory/memory_tool.py +0 -462
  197. klaude_code/llm/openrouter/reasoning_handler.py +0 -209
  198. klaude_code/protocol/sub_agent.py +0 -348
  199. klaude_code/ui/utils/debouncer.py +0 -42
  200. klaude_code-1.2.6.dist-info/METADATA +0 -178
  201. klaude_code-1.2.6.dist-info/RECORD +0 -167
  202. /klaude_code/core/prompts/{prompt-subagent.md → prompt-sub-agent.md} +0 -0
  203. /klaude_code/core/tool/{memory → skill}/__init__.py +0 -0
  204. /klaude_code/core/tool/{memory → skill}/skill_tool.md +0 -0
  205. {klaude_code-1.2.6.dist-info → klaude_code-1.8.0.dist-info}/WHEEL +0 -0
klaude_code/core/turn.py CHANGED
@@ -1,9 +1,14 @@
1
1
  from __future__ import annotations
2
2
 
3
- from collections.abc import AsyncGenerator, Callable, MutableMapping, Sequence
4
- from dataclasses import dataclass
3
+ from collections.abc import AsyncGenerator
4
+ from dataclasses import dataclass, field
5
+ from typing import TYPE_CHECKING
6
+
7
+ from klaude_code.core.tool import ToolABC, tool_context
8
+
9
+ if TYPE_CHECKING:
10
+ from klaude_code.core.task import SessionContext
5
11
 
6
- from klaude_code.core.tool import TodoContext, ToolABC, tool_context
7
12
  from klaude_code.core.tool.tool_runner import (
8
13
  ToolExecutionCallStarted,
9
14
  ToolExecutionResult,
@@ -12,7 +17,7 @@ from klaude_code.core.tool.tool_runner import (
12
17
  ToolExecutorEvent,
13
18
  )
14
19
  from klaude_code.llm import LLMClientABC
15
- from klaude_code.protocol import events, llm_param, model
20
+ from klaude_code.protocol import events, llm_param, model, tools
16
21
  from klaude_code.trace import DebugType, log_debug
17
22
 
18
23
 
@@ -26,16 +31,22 @@ class TurnError(Exception):
26
31
  class TurnExecutionContext:
27
32
  """Execution context required to run a single turn."""
28
33
 
29
- session_id: str
30
- get_conversation_history: Callable[[], list[model.ConversationItem]]
31
- append_history: Callable[[Sequence[model.ConversationItem]], None]
34
+ session_ctx: SessionContext
32
35
  llm_client: LLMClientABC
33
36
  system_prompt: str | None
34
37
  tools: list[llm_param.ToolSchema]
35
38
  tool_registry: dict[str, type[ToolABC]]
36
- # For tool context
37
- file_tracker: MutableMapping[str, float]
38
- todo_context: TodoContext
39
+
40
+
41
+ @dataclass
42
+ class TurnResult:
43
+ """Aggregated state produced while executing a turn."""
44
+
45
+ reasoning_items: list[model.ReasoningTextItem | model.ReasoningEncryptedItem]
46
+ assistant_message: model.AssistantMessageItem | None
47
+ tool_calls: list[model.ToolCallItem]
48
+ stream_error: model.StreamErrorItem | None
49
+ report_back_result: str | None = field(default=None)
39
50
 
40
51
 
41
52
  def build_events_from_tool_executor_event(session_id: str, event: ToolExecutorEvent) -> list[events.Event]:
@@ -64,6 +75,7 @@ def build_events_from_tool_executor_event(session_id: str, event: ToolExecutorEv
64
75
  result=tool_result.output or "",
65
76
  ui_extra=tool_result.ui_extra,
66
77
  status=tool_result.status,
78
+ task_metadata=tool_result.task_metadata,
67
79
  )
68
80
  )
69
81
  case ToolExecutionTodoChange(todos=todos):
@@ -87,45 +99,110 @@ class TurnExecutor:
87
99
  def __init__(self, context: TurnExecutionContext) -> None:
88
100
  self._context = context
89
101
  self._tool_executor: ToolExecutor | None = None
90
- self._has_tool_call: bool = False
102
+ self._turn_result: TurnResult | None = None
103
+ self._assistant_delta_buffer: list[str] = []
104
+ self._assistant_response_id: str | None = None
105
+
106
+ @property
107
+ def report_back_result(self) -> str | None:
108
+ return self._turn_result.report_back_result if self._turn_result else None
109
+
110
+ @property
111
+ def task_finished(self) -> bool:
112
+ """Check if this turn indicates the task should end.
113
+
114
+ Task ends when there are no tool calls or report_back was called.
115
+ """
116
+ if self._turn_result is None:
117
+ return True
118
+ if not self._turn_result.tool_calls:
119
+ return True
120
+ return self._turn_result.report_back_result is not None
91
121
 
92
122
  @property
93
- def has_tool_call(self) -> bool:
94
- return self._has_tool_call
123
+ def task_result(self) -> str:
124
+ """Get the task result from this turn.
125
+
126
+ Returns report_back result if available, otherwise returns
127
+ the assistant message content.
128
+ """
129
+ if self._turn_result is not None and self._turn_result.report_back_result is not None:
130
+ return self._turn_result.report_back_result
131
+ if self._turn_result is not None and self._turn_result.assistant_message is not None:
132
+ return self._turn_result.assistant_message.content or ""
133
+ return ""
134
+
135
+ @property
136
+ def has_structured_output(self) -> bool:
137
+ """Check if the task result is structured output from report_back."""
138
+ return bool(self._turn_result and self._turn_result.report_back_result)
95
139
 
96
140
  def cancel(self) -> list[events.Event]:
97
141
  """Cancel running tools and return any resulting events."""
98
142
  ui_events: list[events.Event] = []
143
+ self._persist_partial_assistant_on_cancel()
99
144
  if self._tool_executor is not None:
100
145
  for exec_event in self._tool_executor.cancel():
101
- for ui_event in build_events_from_tool_executor_event(self._context.session_id, exec_event):
146
+ for ui_event in build_events_from_tool_executor_event(self._context.session_ctx.session_id, exec_event):
102
147
  ui_events.append(ui_event)
103
148
  self._tool_executor = None
104
149
  return ui_events
105
150
 
106
- async def run(self) -> AsyncGenerator[events.Event, None]:
151
+ async def run(self) -> AsyncGenerator[events.Event]:
107
152
  """Execute the turn, yielding events as they occur.
108
153
 
109
154
  Raises:
110
155
  TurnError: If the turn fails (stream error or non-completed status).
111
156
  """
112
157
  ctx = self._context
158
+ session_ctx = ctx.session_ctx
159
+
160
+ yield events.TurnStartEvent(session_id=session_ctx.session_id)
161
+
162
+ self._turn_result = TurnResult(
163
+ reasoning_items=[],
164
+ assistant_message=None,
165
+ tool_calls=[],
166
+ stream_error=None,
167
+ )
113
168
 
114
- yield events.TurnStartEvent(session_id=ctx.session_id)
169
+ async for event in self._consume_llm_stream(self._turn_result):
170
+ yield event
115
171
 
116
- turn_reasoning_items: list[model.ReasoningTextItem | model.ReasoningEncryptedItem] = []
117
- turn_assistant_message: model.AssistantMessageItem | None = None
118
- turn_tool_calls: list[model.ToolCallItem] = []
119
- response_failed = False
120
- error_message: str | None = None
172
+ if self._turn_result.stream_error is not None:
173
+ session_ctx.append_history([self._turn_result.stream_error])
174
+ yield events.TurnEndEvent(session_id=session_ctx.session_id)
175
+ raise TurnError(self._turn_result.stream_error.error)
121
176
 
177
+ self._append_success_history(self._turn_result)
178
+
179
+ if self._turn_result.tool_calls:
180
+ # Check for report_back before running tools
181
+ self._detect_report_back(self._turn_result)
182
+
183
+ async for ui_event in self._run_tool_executor(self._turn_result.tool_calls):
184
+ yield ui_event
185
+
186
+ yield events.TurnEndEvent(session_id=session_ctx.session_id)
187
+
188
+ def _detect_report_back(self, turn_result: TurnResult) -> None:
189
+ """Detect report_back tool call and store its arguments as JSON string."""
190
+ for tool_call in turn_result.tool_calls:
191
+ if tool_call.name == tools.REPORT_BACK:
192
+ turn_result.report_back_result = tool_call.arguments
193
+ break
194
+
195
+ async def _consume_llm_stream(self, turn_result: TurnResult) -> AsyncGenerator[events.Event]:
196
+ """Stream events from LLM and update turn_result in place."""
197
+
198
+ ctx = self._context
199
+ session_ctx = ctx.session_ctx
122
200
  async for response_item in ctx.llm_client.call(
123
201
  llm_param.LLMCallParameter(
124
- input=ctx.get_conversation_history(),
202
+ input=session_ctx.get_conversation_history(),
125
203
  system=ctx.system_prompt,
126
204
  tools=ctx.tools,
127
- store=False,
128
- session_id=ctx.session_id,
205
+ session_id=session_ctx.session_id,
129
206
  )
130
207
  ):
131
208
  log_debug(
@@ -136,41 +213,45 @@ class TurnExecutor:
136
213
  )
137
214
  match response_item:
138
215
  case model.StartItem():
139
- pass
216
+ continue
140
217
  case model.ReasoningTextItem() as item:
141
- turn_reasoning_items.append(item)
218
+ turn_result.reasoning_items.append(item)
142
219
  yield events.ThinkingEvent(
143
220
  content=item.content,
144
221
  response_id=item.response_id,
145
- session_id=ctx.session_id,
222
+ session_id=session_ctx.session_id,
146
223
  )
147
224
  case model.ReasoningEncryptedItem() as item:
148
- turn_reasoning_items.append(item)
225
+ turn_result.reasoning_items.append(item)
226
+ case model.ReasoningTextDelta() as item:
227
+ yield events.ThinkingDeltaEvent(
228
+ content=item.content,
229
+ response_id=item.response_id,
230
+ session_id=session_ctx.session_id,
231
+ )
149
232
  case model.AssistantMessageDelta() as item:
233
+ if item.response_id:
234
+ self._assistant_response_id = item.response_id
235
+ self._assistant_delta_buffer.append(item.content)
150
236
  yield events.AssistantMessageDeltaEvent(
151
237
  content=item.content,
152
238
  response_id=item.response_id,
153
- session_id=ctx.session_id,
239
+ session_id=session_ctx.session_id,
154
240
  )
155
241
  case model.AssistantMessageItem() as item:
156
- turn_assistant_message = item
242
+ turn_result.assistant_message = item
157
243
  yield events.AssistantMessageEvent(
158
244
  content=item.content or "",
159
245
  response_id=item.response_id,
160
- session_id=ctx.session_id,
246
+ session_id=session_ctx.session_id,
161
247
  )
162
248
  case model.ResponseMetadataItem() as item:
163
249
  yield events.ResponseMetadataEvent(
164
- session_id=ctx.session_id,
250
+ session_id=session_ctx.session_id,
165
251
  metadata=item,
166
252
  )
167
- status = item.status
168
- if status is not None and status != "completed":
169
- response_failed = True
170
- error_message = f"Response status: {status}"
171
253
  case model.StreamErrorItem() as item:
172
- response_failed = True
173
- error_message = item.error
254
+ turn_result.stream_error = item
174
255
  log_debug(
175
256
  "[StreamError]",
176
257
  item.error,
@@ -179,42 +260,63 @@ class TurnExecutor:
179
260
  )
180
261
  case model.ToolCallStartItem() as item:
181
262
  yield events.TurnToolCallStartEvent(
182
- session_id=ctx.session_id,
263
+ session_id=session_ctx.session_id,
183
264
  response_id=item.response_id,
184
265
  tool_call_id=item.call_id,
185
266
  tool_name=item.name,
186
267
  arguments="",
187
268
  )
188
269
  case model.ToolCallItem() as item:
189
- turn_tool_calls.append(item)
270
+ turn_result.tool_calls.append(item)
190
271
  case _:
191
- pass
192
-
193
- if response_failed:
194
- yield events.TurnEndEvent(session_id=ctx.session_id)
195
- raise TurnError(error_message or "Turn failed")
196
-
197
- # Append to history only on success
198
- if turn_reasoning_items:
199
- ctx.append_history(turn_reasoning_items)
200
- if turn_assistant_message:
201
- ctx.append_history([turn_assistant_message])
202
- if turn_tool_calls:
203
- ctx.append_history(turn_tool_calls)
204
- self._has_tool_call = True
205
-
206
- # Execute tools
207
- if turn_tool_calls:
208
- with tool_context(ctx.file_tracker, ctx.todo_context):
209
- executor = ToolExecutor(
210
- registry=ctx.tool_registry,
211
- append_history=ctx.append_history,
212
- )
213
- self._tool_executor = executor
272
+ continue
273
+
274
+ def _append_success_history(self, turn_result: TurnResult) -> None:
275
+ """Persist successful turn artifacts to conversation history."""
276
+ session_ctx = self._context.session_ctx
277
+ if turn_result.reasoning_items:
278
+ session_ctx.append_history(turn_result.reasoning_items)
279
+ if turn_result.assistant_message:
280
+ session_ctx.append_history([turn_result.assistant_message])
281
+ if turn_result.tool_calls:
282
+ session_ctx.append_history(turn_result.tool_calls)
283
+ self._assistant_delta_buffer.clear()
284
+ self._assistant_response_id = None
214
285
 
215
- async for exec_event in executor.run_tools(turn_tool_calls):
216
- for ui_event in build_events_from_tool_executor_event(ctx.session_id, exec_event):
286
+ async def _run_tool_executor(self, tool_calls: list[model.ToolCallItem]) -> AsyncGenerator[events.Event]:
287
+ """Run tools for the turn and translate executor events to UI events."""
288
+
289
+ ctx = self._context
290
+ session_ctx = ctx.session_ctx
291
+ with tool_context(session_ctx.file_tracker, session_ctx.todo_context):
292
+ executor = ToolExecutor(
293
+ registry=ctx.tool_registry,
294
+ append_history=session_ctx.append_history,
295
+ )
296
+ self._tool_executor = executor
297
+ try:
298
+ async for exec_event in executor.run_tools(tool_calls):
299
+ for ui_event in build_events_from_tool_executor_event(session_ctx.session_id, exec_event):
217
300
  yield ui_event
301
+ finally:
218
302
  self._tool_executor = None
219
303
 
220
- yield events.TurnEndEvent(session_id=ctx.session_id)
304
+ def _persist_partial_assistant_on_cancel(self) -> None:
305
+ """Persist streamed assistant text when a turn is interrupted.
306
+
307
+ Reasoning and tool calls are intentionally discarded on interrupt; only
308
+ the assistant message text collected so far is saved so it appears in
309
+ subsequent history/context.
310
+ """
311
+
312
+ if not self._assistant_delta_buffer:
313
+ return
314
+ partial_text = "".join(self._assistant_delta_buffer) + "<system interrupted by user>"
315
+ if not partial_text:
316
+ return
317
+ message_item = model.AssistantMessageItem(
318
+ content=partial_text,
319
+ response_id=self._assistant_response_id,
320
+ )
321
+ self._context.session_ctx.append_history([message_item])
322
+ self._assistant_delta_buffer.clear()
@@ -1,21 +1,13 @@
1
1
  """LLM package init.
2
2
 
3
- Imports built-in LLM clients so their ``@register`` decorators run and they
4
- become available via the registry.
3
+ LLM clients are lazily loaded to avoid heavy imports at module load time.
4
+ Only LLMClientABC and create_llm_client are exposed.
5
5
  """
6
6
 
7
- from .anthropic import AnthropicClient
8
7
  from .client import LLMClientABC
9
- from .openai_compatible import OpenAICompatibleClient
10
- from .openrouter import OpenRouterClient
11
8
  from .registry import create_llm_client
12
- from .responses import ResponsesClient
13
9
 
14
10
  __all__ = [
15
11
  "LLMClientABC",
16
- "ResponsesClient",
17
- "OpenAICompatibleClient",
18
- "OpenRouterClient",
19
- "AnthropicClient",
20
12
  "create_llm_client",
21
13
  ]