klaude-code 1.2.8__py3-none-any.whl → 1.2.9__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 (52) hide show
  1. klaude_code/auth/codex/__init__.py +1 -1
  2. klaude_code/command/__init__.py +2 -0
  3. klaude_code/command/prompt-deslop.md +14 -0
  4. klaude_code/command/release_notes_cmd.py +86 -0
  5. klaude_code/command/status_cmd.py +92 -54
  6. klaude_code/core/agent.py +13 -19
  7. klaude_code/core/manager/sub_agent_manager.py +5 -1
  8. klaude_code/core/prompt.py +38 -28
  9. klaude_code/core/reminders.py +4 -4
  10. klaude_code/core/task.py +59 -40
  11. klaude_code/core/tool/__init__.py +2 -0
  12. klaude_code/core/tool/file/apply_patch_tool.py +1 -1
  13. klaude_code/core/tool/file/edit_tool.py +1 -1
  14. klaude_code/core/tool/file/multi_edit_tool.py +1 -1
  15. klaude_code/core/tool/file/write_tool.py +1 -1
  16. klaude_code/core/tool/memory/memory_tool.py +2 -2
  17. klaude_code/core/tool/sub_agent_tool.py +2 -1
  18. klaude_code/core/tool/todo/todo_write_tool.py +1 -1
  19. klaude_code/core/tool/todo/update_plan_tool.py +1 -1
  20. klaude_code/core/tool/tool_context.py +21 -4
  21. klaude_code/core/tool/tool_runner.py +5 -8
  22. klaude_code/core/tool/web/mermaid_tool.py +1 -4
  23. klaude_code/core/turn.py +40 -37
  24. klaude_code/llm/anthropic/client.py +13 -44
  25. klaude_code/llm/client.py +1 -1
  26. klaude_code/llm/codex/client.py +4 -3
  27. klaude_code/llm/input_common.py +0 -6
  28. klaude_code/llm/openai_compatible/client.py +28 -72
  29. klaude_code/llm/openai_compatible/input.py +6 -4
  30. klaude_code/llm/openai_compatible/stream_processor.py +82 -0
  31. klaude_code/llm/openrouter/client.py +29 -59
  32. klaude_code/llm/openrouter/input.py +4 -27
  33. klaude_code/llm/responses/client.py +15 -48
  34. klaude_code/llm/usage.py +51 -10
  35. klaude_code/protocol/commands.py +1 -0
  36. klaude_code/protocol/events.py +11 -2
  37. klaude_code/protocol/model.py +142 -24
  38. klaude_code/protocol/sub_agent.py +5 -1
  39. klaude_code/session/export.py +51 -27
  40. klaude_code/session/session.py +28 -16
  41. klaude_code/session/templates/export_session.html +4 -1
  42. klaude_code/ui/modes/repl/__init__.py +1 -5
  43. klaude_code/ui/modes/repl/event_handler.py +153 -54
  44. klaude_code/ui/modes/repl/renderer.py +4 -4
  45. klaude_code/ui/renderers/developer.py +35 -25
  46. klaude_code/ui/renderers/metadata.py +68 -30
  47. klaude_code/ui/renderers/tools.py +53 -87
  48. klaude_code/ui/rich/markdown.py +5 -5
  49. {klaude_code-1.2.8.dist-info → klaude_code-1.2.9.dist-info}/METADATA +1 -1
  50. {klaude_code-1.2.8.dist-info → klaude_code-1.2.9.dist-info}/RECORD +52 -49
  51. {klaude_code-1.2.8.dist-info → klaude_code-1.2.9.dist-info}/WHEEL +0 -0
  52. {klaude_code-1.2.8.dist-info → klaude_code-1.2.9.dist-info}/entry_points.txt +0 -0
klaude_code/core/task.py CHANGED
@@ -25,28 +25,30 @@ class MetadataAccumulator:
25
25
  """
26
26
 
27
27
  def __init__(self, model_name: str) -> None:
28
- self._accumulated = model.ResponseMetadataItem(model_name=model_name)
28
+ self._main = model.TaskMetadata(model_name=model_name)
29
+ self._sub_agent_metadata: list[model.TaskMetadata] = []
29
30
  self._throughput_weighted_sum: float = 0.0
30
31
  self._throughput_tracked_tokens: int = 0
31
32
 
32
33
  def add(self, turn_metadata: model.ResponseMetadataItem) -> None:
33
34
  """Merge a turn's metadata into the accumulated state."""
34
- accumulated = self._accumulated
35
+ main = self._main
35
36
  usage = turn_metadata.usage
36
37
 
37
38
  if usage is not None:
38
- if accumulated.usage is None:
39
- accumulated.usage = model.Usage()
40
- acc_usage = accumulated.usage
39
+ if main.usage is None:
40
+ main.usage = model.Usage()
41
+ acc_usage = main.usage
41
42
  acc_usage.input_tokens += usage.input_tokens
42
43
  acc_usage.cached_tokens += usage.cached_tokens
43
44
  acc_usage.reasoning_tokens += usage.reasoning_tokens
44
45
  acc_usage.output_tokens += usage.output_tokens
45
- acc_usage.total_tokens += usage.total_tokens
46
46
  acc_usage.currency = usage.currency
47
47
 
48
- if usage.context_usage_percent is not None:
49
- acc_usage.context_usage_percent = usage.context_usage_percent
48
+ if usage.context_window_size is not None:
49
+ acc_usage.context_window_size = usage.context_window_size
50
+ if usage.context_limit is not None:
51
+ acc_usage.context_limit = usage.context_limit
50
52
 
51
53
  if usage.first_token_latency_ms is not None:
52
54
  if acc_usage.first_token_latency_ms is None:
@@ -63,47 +65,56 @@ class MetadataAccumulator:
63
65
  self._throughput_weighted_sum += usage.throughput_tps * current_output
64
66
  self._throughput_tracked_tokens += current_output
65
67
 
66
- # Accumulate costs
67
68
  if usage.input_cost is not None:
68
69
  acc_usage.input_cost = (acc_usage.input_cost or 0.0) + usage.input_cost
69
70
  if usage.output_cost is not None:
70
71
  acc_usage.output_cost = (acc_usage.output_cost or 0.0) + usage.output_cost
71
72
  if usage.cache_read_cost is not None:
72
73
  acc_usage.cache_read_cost = (acc_usage.cache_read_cost or 0.0) + usage.cache_read_cost
73
- if usage.total_cost is not None:
74
- acc_usage.total_cost = (acc_usage.total_cost or 0.0) + usage.total_cost
75
74
 
76
75
  if turn_metadata.provider is not None:
77
- accumulated.provider = turn_metadata.provider
76
+ main.provider = turn_metadata.provider
78
77
  if turn_metadata.model_name:
79
- accumulated.model_name = turn_metadata.model_name
80
- if turn_metadata.response_id:
81
- accumulated.response_id = turn_metadata.response_id
78
+ main.model_name = turn_metadata.model_name
82
79
 
83
- def finalize(self, task_duration_s: float) -> model.ResponseMetadataItem:
80
+ def add_sub_agent_metadata(self, sub_agent_metadata: model.TaskMetadata) -> None:
81
+ """Add sub-agent task metadata to the accumulated state."""
82
+ self._sub_agent_metadata.append(sub_agent_metadata)
83
+
84
+ def finalize(self, task_duration_s: float) -> model.TaskMetadataItem:
84
85
  """Return the final accumulated metadata with computed throughput and duration."""
85
- accumulated = self._accumulated
86
- if accumulated.usage is not None:
86
+ main = self._main
87
+ if main.usage is not None:
87
88
  if self._throughput_tracked_tokens > 0:
88
- accumulated.usage.throughput_tps = self._throughput_weighted_sum / self._throughput_tracked_tokens
89
+ main.usage.throughput_tps = self._throughput_weighted_sum / self._throughput_tracked_tokens
89
90
  else:
90
- accumulated.usage.throughput_tps = None
91
+ main.usage.throughput_tps = None
91
92
 
92
- accumulated.task_duration_s = task_duration_s
93
- return accumulated
93
+ main.task_duration_s = task_duration_s
94
+ return model.TaskMetadataItem(main=main, sub_agent_task_metadata=self._sub_agent_metadata)
94
95
 
95
96
 
96
97
  @dataclass
97
- class TaskExecutionContext:
98
- """Execution context required to run a task."""
98
+ class SessionContext:
99
+ """Shared session-level context for task and turn execution.
100
+
101
+ Contains common fields that both TaskExecutionContext and TurnExecutionContext need.
102
+ """
99
103
 
100
104
  session_id: str
101
- profile: AgentProfile
102
105
  get_conversation_history: Callable[[], list[model.ConversationItem]]
103
106
  append_history: Callable[[Sequence[model.ConversationItem]], None]
104
- tool_registry: dict[str, type[ToolABC]]
105
107
  file_tracker: MutableMapping[str, float]
106
108
  todo_context: TodoContext
109
+
110
+
111
+ @dataclass
112
+ class TaskExecutionContext:
113
+ """Execution context required to run a task."""
114
+
115
+ session_ctx: SessionContext
116
+ profile: AgentProfile
117
+ tool_registry: dict[str, type[ToolABC]]
107
118
  # For reminder processing - needs access to session
108
119
  process_reminder: Callable[[Reminder], AsyncGenerator[events.DeveloperMessageEvent, None]]
109
120
  sub_agent_state: model.SubAgentState | None
@@ -135,18 +146,18 @@ class TaskExecutor:
135
146
  async def run(self, user_input: model.UserInputPayload) -> AsyncGenerator[events.Event, None]:
136
147
  """Execute the task, yielding events as they occur."""
137
148
  ctx = self._context
149
+ session_ctx = ctx.session_ctx
138
150
  self._started_at = time.perf_counter()
139
151
 
140
152
  yield events.TaskStartEvent(
141
- session_id=ctx.session_id,
153
+ session_id=session_ctx.session_id,
142
154
  sub_agent_state=ctx.sub_agent_state,
143
155
  )
144
156
 
145
- ctx.append_history([model.UserMessageItem(content=user_input.text, images=user_input.images)])
157
+ session_ctx.append_history([model.UserMessageItem(content=user_input.text, images=user_input.images)])
146
158
 
147
159
  profile = ctx.profile
148
160
  metadata_accumulator = MetadataAccumulator(model_name=profile.llm_client.model_name)
149
- last_assistant_message: events.AssistantMessageEvent | None = None
150
161
 
151
162
  while True:
152
163
  # Process reminders at the start of each turn
@@ -155,15 +166,11 @@ class TaskExecutor:
155
166
  yield event
156
167
 
157
168
  turn_context = TurnExecutionContext(
158
- session_id=ctx.session_id,
159
- get_conversation_history=ctx.get_conversation_history,
160
- append_history=ctx.append_history,
169
+ session_ctx=session_ctx,
161
170
  llm_client=profile.llm_client,
162
171
  system_prompt=profile.system_prompt,
163
172
  tools=profile.tools,
164
173
  tool_registry=ctx.tool_registry,
165
- file_tracker=ctx.file_tracker,
166
- todo_context=ctx.todo_context,
167
174
  )
168
175
 
169
176
  turn: TurnExecutor | None = None
@@ -178,11 +185,14 @@ class TaskExecutor:
178
185
  async for turn_event in turn.run():
179
186
  match turn_event:
180
187
  case events.AssistantMessageEvent() as am:
181
- if am.content.strip() != "":
182
- last_assistant_message = am
183
188
  yield am
184
189
  case events.ResponseMetadataEvent() as e:
185
190
  metadata_accumulator.add(e.metadata)
191
+ case events.ToolResultEvent() as e:
192
+ # Collect sub-agent task metadata from tool results
193
+ if e.task_metadata is not None:
194
+ metadata_accumulator.add_sub_agent_metadata(e.task_metadata)
195
+ yield turn_event
186
196
  case _:
187
197
  yield turn_event
188
198
 
@@ -219,14 +229,23 @@ class TaskExecutor:
219
229
  task_duration_s = time.perf_counter() - self._started_at
220
230
  accumulated = metadata_accumulator.finalize(task_duration_s)
221
231
 
222
- yield events.ResponseMetadataEvent(metadata=accumulated, session_id=ctx.session_id)
223
- ctx.append_history([accumulated])
232
+ yield events.TaskMetadataEvent(metadata=accumulated, session_id=session_ctx.session_id)
233
+ session_ctx.append_history([accumulated])
224
234
  yield events.TaskFinishEvent(
225
- session_id=ctx.session_id,
226
- task_result=last_assistant_message.content if last_assistant_message else "",
235
+ session_id=session_ctx.session_id,
236
+ task_result=_get_last_assistant_message(session_ctx.get_conversation_history()) or "",
227
237
  )
228
238
 
229
239
 
240
+ def _get_last_assistant_message(history: list[model.ConversationItem]) -> str | None:
241
+ """Return the content of the most recent assistant message in history."""
242
+
243
+ for item in reversed(history):
244
+ if isinstance(item, model.AssistantMessageItem):
245
+ return item.content or ""
246
+ return None
247
+
248
+
230
249
  def _retry_delay_seconds(attempt: int) -> float:
231
250
  """Compute exponential backoff delay for the given attempt count."""
232
251
  capped_attempt = max(1, attempt)
@@ -16,6 +16,7 @@ from .tool_abc import ToolABC
16
16
  from .tool_context import (
17
17
  TodoContext,
18
18
  ToolContextToken,
19
+ build_todo_context,
19
20
  current_run_subtask_callback,
20
21
  reset_tool_context,
21
22
  set_tool_context_from_session,
@@ -46,6 +47,7 @@ __all__ = [
46
47
  "ToolABC",
47
48
  # Tool context
48
49
  "TodoContext",
50
+ "build_todo_context",
49
51
  "ToolContextToken",
50
52
  "current_run_subtask_callback",
51
53
  "reset_tool_context",
@@ -26,7 +26,7 @@ class ApplyPatchHandler:
26
26
  return model.ToolResultItem(
27
27
  status="success",
28
28
  output=output,
29
- ui_extra=model.ToolResultUIExtra(type=model.ToolResultUIExtraType.DIFF_TEXT, diff_text=diff_text),
29
+ ui_extra=model.DiffTextUIExtra(diff_text=diff_text),
30
30
  )
31
31
 
32
32
  @staticmethod
@@ -212,7 +212,7 @@ class EditTool(ToolABC):
212
212
  )
213
213
  )
214
214
  diff_text = "\n".join(diff_lines)
215
- ui_extra = model.ToolResultUIExtra(type=model.ToolResultUIExtraType.DIFF_TEXT, diff_text=diff_text)
215
+ ui_extra = model.DiffTextUIExtra(diff_text=diff_text)
216
216
 
217
217
  # Update tracker with new mtime
218
218
  if file_tracker is not None:
@@ -183,7 +183,7 @@ class MultiEditTool(ToolABC):
183
183
  )
184
184
  )
185
185
  diff_text = "\n".join(diff_lines)
186
- ui_extra = model.ToolResultUIExtra(type=model.ToolResultUIExtraType.DIFF_TEXT, diff_text=diff_text)
186
+ ui_extra = model.DiffTextUIExtra(diff_text=diff_text)
187
187
 
188
188
  # Update tracker
189
189
  if file_tracker is not None:
@@ -140,7 +140,7 @@ class WriteTool(ToolABC):
140
140
  )
141
141
  )
142
142
  diff_text = "\n".join(diff_lines)
143
- ui_extra = model.ToolResultUIExtra(type=model.ToolResultUIExtraType.DIFF_TEXT, diff_text=diff_text)
143
+ ui_extra = model.DiffTextUIExtra(diff_text=diff_text)
144
144
 
145
145
  message = f"File {'overwritten' if exists else 'created'} successfully at: {file_path}"
146
146
  return model.ToolResultItem(status="success", output=message, ui_extra=ui_extra)
@@ -100,7 +100,7 @@ def _format_numbered_line(line_no: int, content: str) -> str:
100
100
  return f"{line_no:>6}|{content}"
101
101
 
102
102
 
103
- def _make_diff_ui_extra(before: str, after: str, path: str) -> model.ToolResultUIExtra:
103
+ def _make_diff_ui_extra(before: str, after: str, path: str) -> model.DiffTextUIExtra:
104
104
  diff_lines = list(
105
105
  difflib.unified_diff(
106
106
  before.splitlines(),
@@ -111,7 +111,7 @@ def _make_diff_ui_extra(before: str, after: str, path: str) -> model.ToolResultU
111
111
  )
112
112
  )
113
113
  diff_text = "\n".join(diff_lines)
114
- return model.ToolResultUIExtra(type=model.ToolResultUIExtraType.DIFF_TEXT, diff_text=diff_text)
114
+ return model.DiffTextUIExtra(diff_text=diff_text)
115
115
 
116
116
 
117
117
  @register(tools.MEMORY)
@@ -79,5 +79,6 @@ class SubAgentTool(ToolABC):
79
79
  return model.ToolResultItem(
80
80
  status="success" if not result.error else "error",
81
81
  output=result.task_result or "",
82
- ui_extra=model.ToolResultUIExtra(type=model.ToolResultUIExtraType.SESSION_ID, session_id=result.session_id),
82
+ ui_extra=model.SessionIdUIExtra(session_id=result.session_id),
83
+ task_metadata=result.task_metadata,
83
84
  )
@@ -116,6 +116,6 @@ Your todo list has changed. DO NOT mention this explicitly to the user. Here are
116
116
  return model.ToolResultItem(
117
117
  status="success",
118
118
  output=response,
119
- ui_extra=model.ToolResultUIExtra(type=model.ToolResultUIExtraType.TODO_LIST, todo_list=ui_extra),
119
+ ui_extra=model.TodoListUIExtra(todo_list=ui_extra),
120
120
  side_effects=[model.ToolSideEffect.TODO_CHANGE],
121
121
  )
@@ -99,6 +99,6 @@ class UpdatePlanTool(ToolABC):
99
99
  return model.ToolResultItem(
100
100
  status="success",
101
101
  output="Plan updated",
102
- ui_extra=model.ToolResultUIExtra(type=model.ToolResultUIExtraType.TODO_LIST, todo_list=ui_extra),
102
+ ui_extra=model.TodoListUIExtra(todo_list=ui_extra),
103
103
  side_effects=[model.ToolSideEffect.TODO_CHANGE],
104
104
  )
@@ -22,6 +22,19 @@ class TodoContext:
22
22
  set_todos: Callable[[list[model.TodoItem]], None]
23
23
 
24
24
 
25
+ @dataclass
26
+ class SessionTodoStore:
27
+ """Adapter exposing session todos through an explicit interface."""
28
+
29
+ session: Session
30
+
31
+ def get(self) -> list[model.TodoItem]:
32
+ return self.session.todos
33
+
34
+ def set(self, todos: list[model.TodoItem]) -> None:
35
+ self.session.todos = todos
36
+
37
+
25
38
  @dataclass
26
39
  class ToolContextToken:
27
40
  """Tokens used to restore tool execution context.
@@ -55,10 +68,7 @@ def set_tool_context_from_session(session: Session) -> ToolContextToken:
55
68
  """
56
69
 
57
70
  file_tracker_token = current_file_tracker_var.set(session.file_tracker)
58
- todo_ctx = TodoContext(
59
- get_todos=lambda: session.todos,
60
- set_todos=lambda todos: setattr(session, "todos", todos),
61
- )
71
+ todo_ctx = build_todo_context(session)
62
72
  todo_token = current_todo_context_var.set(todo_ctx)
63
73
  return ToolContextToken(file_tracker_token=file_tracker_token, todo_token=todo_token)
64
74
 
@@ -87,6 +97,13 @@ def tool_context(
87
97
  reset_tool_context(token)
88
98
 
89
99
 
100
+ def build_todo_context(session: Session) -> TodoContext:
101
+ """Create a TodoContext backed by the given session."""
102
+
103
+ store = SessionTodoStore(session)
104
+ return TodoContext(get_todos=store.get, set_todos=store.set)
105
+
106
+
90
107
  def get_current_file_tracker() -> MutableMapping[str, float] | None:
91
108
  """Return the current file tracker mapping for this tool context."""
92
109
 
@@ -34,13 +34,10 @@ async def run_tool(tool_call: model.ToolCallItem, registry: dict[str, type[ToolA
34
34
  truncation_result = truncate_tool_output(tool_result.output, tool_call)
35
35
  tool_result.output = truncation_result.output
36
36
  if truncation_result.was_truncated and truncation_result.saved_file_path:
37
- tool_result.ui_extra = model.ToolResultUIExtra(
38
- type=model.ToolResultUIExtraType.TRUNCATION,
39
- truncation=model.TruncationUIExtra(
40
- saved_file_path=truncation_result.saved_file_path,
41
- original_length=truncation_result.original_length,
42
- truncated_length=truncation_result.truncated_length,
43
- ),
37
+ tool_result.ui_extra = model.TruncationUIExtra(
38
+ saved_file_path=truncation_result.saved_file_path,
39
+ original_length=truncation_result.original_length,
40
+ truncated_length=truncation_result.truncated_length,
44
41
  )
45
42
  return tool_result
46
43
  except asyncio.CancelledError:
@@ -244,7 +241,7 @@ class ToolExecutor:
244
241
  for side_effect in side_effects:
245
242
  if side_effect == model.ToolSideEffect.TODO_CHANGE:
246
243
  todos: list[model.TodoItem] | None = None
247
- if tool_result.ui_extra is not None and tool_result.ui_extra.todo_list is not None:
244
+ if isinstance(tool_result.ui_extra, model.TodoListUIExtra):
248
245
  todos = tool_result.ui_extra.todo_list.todos
249
246
  if todos is not None:
250
247
  side_effect_events.append(ToolExecutionTodoChange(todos=todos))
@@ -49,10 +49,7 @@ class MermaidTool(ToolABC):
49
49
 
50
50
  link = cls._build_link(args.code)
51
51
  line_count = cls._count_lines(args.code)
52
- ui_extra = model.ToolResultUIExtra(
53
- type=model.ToolResultUIExtraType.MERMAID_LINK,
54
- mermaid_link=model.MermaidLinkUIExtra(link=link, line_count=line_count),
55
- )
52
+ ui_extra = model.MermaidLinkUIExtra(link=link, line_count=line_count)
56
53
  output = f"Mermaid diagram rendered successfully ({line_count} lines)."
57
54
  return model.ToolResultItem(status="success", output=output, ui_extra=ui_extra)
58
55
 
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
3
+ from collections.abc import AsyncGenerator
4
4
  from dataclasses import dataclass
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,
@@ -26,16 +31,11 @@ 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
39
 
40
40
 
41
41
  @dataclass
@@ -74,6 +74,7 @@ def build_events_from_tool_executor_event(session_id: str, event: ToolExecutorEv
74
74
  result=tool_result.output or "",
75
75
  ui_extra=tool_result.ui_extra,
76
76
  status=tool_result.status,
77
+ task_metadata=tool_result.task_metadata,
77
78
  )
78
79
  )
79
80
  case ToolExecutionTodoChange(todos=todos):
@@ -97,18 +98,18 @@ class TurnExecutor:
97
98
  def __init__(self, context: TurnExecutionContext) -> None:
98
99
  self._context = context
99
100
  self._tool_executor: ToolExecutor | None = None
100
- self._has_tool_call: bool = False
101
+ self._turn_result: TurnResult | None = None
101
102
 
102
103
  @property
103
104
  def has_tool_call(self) -> bool:
104
- return self._has_tool_call
105
+ return bool(self._turn_result and self._turn_result.tool_calls)
105
106
 
106
107
  def cancel(self) -> list[events.Event]:
107
108
  """Cancel running tools and return any resulting events."""
108
109
  ui_events: list[events.Event] = []
109
110
  if self._tool_executor is not None:
110
111
  for exec_event in self._tool_executor.cancel():
111
- for ui_event in build_events_from_tool_executor_event(self._context.session_id, exec_event):
112
+ for ui_event in build_events_from_tool_executor_event(self._context.session_ctx.session_id, exec_event):
112
113
  ui_events.append(ui_event)
113
114
  self._tool_executor = None
114
115
  return ui_events
@@ -120,44 +121,45 @@ class TurnExecutor:
120
121
  TurnError: If the turn fails (stream error or non-completed status).
121
122
  """
122
123
  ctx = self._context
124
+ session_ctx = ctx.session_ctx
123
125
 
124
- yield events.TurnStartEvent(session_id=ctx.session_id)
126
+ yield events.TurnStartEvent(session_id=session_ctx.session_id)
125
127
 
126
- turn_result = TurnResult(
128
+ self._turn_result = TurnResult(
127
129
  reasoning_items=[],
128
130
  assistant_message=None,
129
131
  tool_calls=[],
130
132
  stream_error=None,
131
133
  )
132
134
 
133
- async for event in self._consume_llm_stream(turn_result):
135
+ async for event in self._consume_llm_stream(self._turn_result):
134
136
  yield event
135
137
 
136
- if turn_result.stream_error is not None:
137
- ctx.append_history([turn_result.stream_error])
138
- yield events.TurnEndEvent(session_id=ctx.session_id)
139
- raise TurnError(turn_result.stream_error.error)
138
+ if self._turn_result.stream_error is not None:
139
+ session_ctx.append_history([self._turn_result.stream_error])
140
+ yield events.TurnEndEvent(session_id=session_ctx.session_id)
141
+ raise TurnError(self._turn_result.stream_error.error)
140
142
 
141
- self._append_success_history(turn_result)
142
- self._has_tool_call = bool(turn_result.tool_calls)
143
+ self._append_success_history(self._turn_result)
143
144
 
144
- if turn_result.tool_calls:
145
- async for ui_event in self._run_tool_executor(turn_result.tool_calls):
145
+ if self._turn_result.tool_calls:
146
+ async for ui_event in self._run_tool_executor(self._turn_result.tool_calls):
146
147
  yield ui_event
147
148
 
148
- yield events.TurnEndEvent(session_id=ctx.session_id)
149
+ yield events.TurnEndEvent(session_id=session_ctx.session_id)
149
150
 
150
151
  async def _consume_llm_stream(self, turn_result: TurnResult) -> AsyncGenerator[events.Event, None]:
151
152
  """Stream events from LLM and update turn_result in place."""
152
153
 
153
154
  ctx = self._context
155
+ session_ctx = ctx.session_ctx
154
156
  async for response_item in ctx.llm_client.call(
155
157
  llm_param.LLMCallParameter(
156
- input=ctx.get_conversation_history(),
158
+ input=session_ctx.get_conversation_history(),
157
159
  system=ctx.system_prompt,
158
160
  tools=ctx.tools,
159
161
  store=False,
160
- session_id=ctx.session_id,
162
+ session_id=session_ctx.session_id,
161
163
  )
162
164
  ):
163
165
  log_debug(
@@ -174,7 +176,7 @@ class TurnExecutor:
174
176
  yield events.ThinkingEvent(
175
177
  content=item.content,
176
178
  response_id=item.response_id,
177
- session_id=ctx.session_id,
179
+ session_id=session_ctx.session_id,
178
180
  )
179
181
  case model.ReasoningEncryptedItem() as item:
180
182
  turn_result.reasoning_items.append(item)
@@ -182,18 +184,18 @@ class TurnExecutor:
182
184
  yield events.AssistantMessageDeltaEvent(
183
185
  content=item.content,
184
186
  response_id=item.response_id,
185
- session_id=ctx.session_id,
187
+ session_id=session_ctx.session_id,
186
188
  )
187
189
  case model.AssistantMessageItem() as item:
188
190
  turn_result.assistant_message = item
189
191
  yield events.AssistantMessageEvent(
190
192
  content=item.content or "",
191
193
  response_id=item.response_id,
192
- session_id=ctx.session_id,
194
+ session_id=session_ctx.session_id,
193
195
  )
194
196
  case model.ResponseMetadataItem() as item:
195
197
  yield events.ResponseMetadataEvent(
196
- session_id=ctx.session_id,
198
+ session_id=session_ctx.session_id,
197
199
  metadata=item,
198
200
  )
199
201
  case model.StreamErrorItem() as item:
@@ -206,7 +208,7 @@ class TurnExecutor:
206
208
  )
207
209
  case model.ToolCallStartItem() as item:
208
210
  yield events.TurnToolCallStartEvent(
209
- session_id=ctx.session_id,
211
+ session_id=session_ctx.session_id,
210
212
  response_id=item.response_id,
211
213
  tool_call_id=item.call_id,
212
214
  tool_name=item.name,
@@ -219,27 +221,28 @@ class TurnExecutor:
219
221
 
220
222
  def _append_success_history(self, turn_result: TurnResult) -> None:
221
223
  """Persist successful turn artifacts to conversation history."""
222
- ctx = self._context
224
+ session_ctx = self._context.session_ctx
223
225
  if turn_result.reasoning_items:
224
- ctx.append_history(turn_result.reasoning_items)
226
+ session_ctx.append_history(turn_result.reasoning_items)
225
227
  if turn_result.assistant_message:
226
- ctx.append_history([turn_result.assistant_message])
228
+ session_ctx.append_history([turn_result.assistant_message])
227
229
  if turn_result.tool_calls:
228
- ctx.append_history(turn_result.tool_calls)
230
+ session_ctx.append_history(turn_result.tool_calls)
229
231
 
230
232
  async def _run_tool_executor(self, tool_calls: list[model.ToolCallItem]) -> AsyncGenerator[events.Event, None]:
231
233
  """Run tools for the turn and translate executor events to UI events."""
232
234
 
233
235
  ctx = self._context
234
- with tool_context(ctx.file_tracker, ctx.todo_context):
236
+ session_ctx = ctx.session_ctx
237
+ with tool_context(session_ctx.file_tracker, session_ctx.todo_context):
235
238
  executor = ToolExecutor(
236
239
  registry=ctx.tool_registry,
237
- append_history=ctx.append_history,
240
+ append_history=session_ctx.append_history,
238
241
  )
239
242
  self._tool_executor = executor
240
243
  try:
241
244
  async for exec_event in executor.run_tools(tool_calls):
242
- for ui_event in build_events_from_tool_executor_event(ctx.session_id, exec_event):
245
+ for ui_event in build_events_from_tool_executor_event(session_ctx.session_id, exec_event):
243
246
  yield ui_event
244
247
  finally:
245
248
  self._tool_executor = None