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.
- klaude_code/auth/codex/__init__.py +1 -1
- klaude_code/command/__init__.py +2 -0
- klaude_code/command/prompt-deslop.md +14 -0
- klaude_code/command/release_notes_cmd.py +86 -0
- klaude_code/command/status_cmd.py +92 -54
- klaude_code/core/agent.py +13 -19
- klaude_code/core/manager/sub_agent_manager.py +5 -1
- klaude_code/core/prompt.py +38 -28
- klaude_code/core/reminders.py +4 -4
- klaude_code/core/task.py +59 -40
- klaude_code/core/tool/__init__.py +2 -0
- klaude_code/core/tool/file/apply_patch_tool.py +1 -1
- klaude_code/core/tool/file/edit_tool.py +1 -1
- klaude_code/core/tool/file/multi_edit_tool.py +1 -1
- klaude_code/core/tool/file/write_tool.py +1 -1
- klaude_code/core/tool/memory/memory_tool.py +2 -2
- klaude_code/core/tool/sub_agent_tool.py +2 -1
- klaude_code/core/tool/todo/todo_write_tool.py +1 -1
- klaude_code/core/tool/todo/update_plan_tool.py +1 -1
- klaude_code/core/tool/tool_context.py +21 -4
- klaude_code/core/tool/tool_runner.py +5 -8
- klaude_code/core/tool/web/mermaid_tool.py +1 -4
- klaude_code/core/turn.py +40 -37
- klaude_code/llm/anthropic/client.py +13 -44
- klaude_code/llm/client.py +1 -1
- klaude_code/llm/codex/client.py +4 -3
- klaude_code/llm/input_common.py +0 -6
- klaude_code/llm/openai_compatible/client.py +28 -72
- klaude_code/llm/openai_compatible/input.py +6 -4
- klaude_code/llm/openai_compatible/stream_processor.py +82 -0
- klaude_code/llm/openrouter/client.py +29 -59
- klaude_code/llm/openrouter/input.py +4 -27
- klaude_code/llm/responses/client.py +15 -48
- klaude_code/llm/usage.py +51 -10
- klaude_code/protocol/commands.py +1 -0
- klaude_code/protocol/events.py +11 -2
- klaude_code/protocol/model.py +142 -24
- klaude_code/protocol/sub_agent.py +5 -1
- klaude_code/session/export.py +51 -27
- klaude_code/session/session.py +28 -16
- klaude_code/session/templates/export_session.html +4 -1
- klaude_code/ui/modes/repl/__init__.py +1 -5
- klaude_code/ui/modes/repl/event_handler.py +153 -54
- klaude_code/ui/modes/repl/renderer.py +4 -4
- klaude_code/ui/renderers/developer.py +35 -25
- klaude_code/ui/renderers/metadata.py +68 -30
- klaude_code/ui/renderers/tools.py +53 -87
- klaude_code/ui/rich/markdown.py +5 -5
- {klaude_code-1.2.8.dist-info → klaude_code-1.2.9.dist-info}/METADATA +1 -1
- {klaude_code-1.2.8.dist-info → klaude_code-1.2.9.dist-info}/RECORD +52 -49
- {klaude_code-1.2.8.dist-info → klaude_code-1.2.9.dist-info}/WHEEL +0 -0
- {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.
|
|
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
|
-
|
|
35
|
+
main = self._main
|
|
35
36
|
usage = turn_metadata.usage
|
|
36
37
|
|
|
37
38
|
if usage is not None:
|
|
38
|
-
if
|
|
39
|
-
|
|
40
|
-
acc_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.
|
|
49
|
-
acc_usage.
|
|
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
|
-
|
|
76
|
+
main.provider = turn_metadata.provider
|
|
78
77
|
if turn_metadata.model_name:
|
|
79
|
-
|
|
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
|
|
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
|
-
|
|
86
|
-
if
|
|
86
|
+
main = self._main
|
|
87
|
+
if main.usage is not None:
|
|
87
88
|
if self._throughput_tracked_tokens > 0:
|
|
88
|
-
|
|
89
|
+
main.usage.throughput_tps = self._throughput_weighted_sum / self._throughput_tracked_tokens
|
|
89
90
|
else:
|
|
90
|
-
|
|
91
|
+
main.usage.throughput_tps = None
|
|
91
92
|
|
|
92
|
-
|
|
93
|
-
return
|
|
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
|
|
98
|
-
"""
|
|
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=
|
|
153
|
+
session_id=session_ctx.session_id,
|
|
142
154
|
sub_agent_state=ctx.sub_agent_state,
|
|
143
155
|
)
|
|
144
156
|
|
|
145
|
-
|
|
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
|
-
|
|
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.
|
|
223
|
-
|
|
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=
|
|
226
|
-
task_result=
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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 =
|
|
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.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
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.
|
|
101
|
+
self._turn_result: TurnResult | None = None
|
|
101
102
|
|
|
102
103
|
@property
|
|
103
104
|
def has_tool_call(self) -> bool:
|
|
104
|
-
return self.
|
|
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=
|
|
126
|
+
yield events.TurnStartEvent(session_id=session_ctx.session_id)
|
|
125
127
|
|
|
126
|
-
|
|
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(
|
|
135
|
+
async for event in self._consume_llm_stream(self._turn_result):
|
|
134
136
|
yield event
|
|
135
137
|
|
|
136
|
-
if
|
|
137
|
-
|
|
138
|
-
yield events.TurnEndEvent(session_id=
|
|
139
|
-
raise TurnError(
|
|
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(
|
|
142
|
-
self._has_tool_call = bool(turn_result.tool_calls)
|
|
143
|
+
self._append_success_history(self._turn_result)
|
|
143
144
|
|
|
144
|
-
if
|
|
145
|
-
async for ui_event in self._run_tool_executor(
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
194
|
+
session_id=session_ctx.session_id,
|
|
193
195
|
)
|
|
194
196
|
case model.ResponseMetadataItem() as item:
|
|
195
197
|
yield events.ResponseMetadataEvent(
|
|
196
|
-
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=
|
|
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
|
-
|
|
224
|
+
session_ctx = self._context.session_ctx
|
|
223
225
|
if turn_result.reasoning_items:
|
|
224
|
-
|
|
226
|
+
session_ctx.append_history(turn_result.reasoning_items)
|
|
225
227
|
if turn_result.assistant_message:
|
|
226
|
-
|
|
228
|
+
session_ctx.append_history([turn_result.assistant_message])
|
|
227
229
|
if turn_result.tool_calls:
|
|
228
|
-
|
|
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
|
-
|
|
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=
|
|
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(
|
|
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
|