klaude-code 2.0.1__py3-none-any.whl → 2.1.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.
- klaude_code/app/__init__.py +12 -0
- klaude_code/app/runtime.py +215 -0
- klaude_code/cli/auth_cmd.py +2 -2
- klaude_code/cli/config_cmd.py +2 -2
- klaude_code/cli/cost_cmd.py +1 -1
- klaude_code/cli/debug.py +12 -36
- klaude_code/cli/list_model.py +3 -3
- klaude_code/cli/main.py +17 -60
- klaude_code/cli/self_update.py +2 -187
- klaude_code/cli/session_cmd.py +2 -2
- klaude_code/config/config.py +1 -1
- klaude_code/config/select_model.py +1 -1
- klaude_code/const.py +10 -1
- klaude_code/core/agent.py +9 -62
- klaude_code/core/agent_profile.py +284 -0
- klaude_code/core/executor.py +343 -230
- klaude_code/core/manager/llm_clients_builder.py +1 -1
- klaude_code/core/manager/sub_agent_manager.py +16 -29
- klaude_code/core/reminders.py +107 -155
- klaude_code/core/task.py +12 -20
- klaude_code/core/tool/__init__.py +5 -19
- klaude_code/core/tool/context.py +84 -0
- klaude_code/core/tool/file/apply_patch_tool.py +18 -21
- klaude_code/core/tool/file/edit_tool.py +42 -44
- klaude_code/core/tool/file/read_tool.py +14 -9
- klaude_code/core/tool/file/write_tool.py +12 -13
- klaude_code/core/tool/report_back_tool.py +4 -1
- klaude_code/core/tool/shell/bash_tool.py +6 -11
- klaude_code/core/tool/skill/skill_tool.py +3 -1
- klaude_code/core/tool/sub_agent_tool.py +8 -7
- klaude_code/core/tool/todo/todo_write_tool.py +3 -9
- klaude_code/core/tool/todo/update_plan_tool.py +3 -5
- klaude_code/core/tool/tool_abc.py +2 -1
- klaude_code/core/tool/tool_registry.py +2 -33
- klaude_code/core/tool/tool_runner.py +13 -10
- klaude_code/core/tool/web/mermaid_tool.py +3 -1
- klaude_code/core/tool/web/web_fetch_tool.py +5 -3
- klaude_code/core/tool/web/web_search_tool.py +5 -3
- klaude_code/core/turn.py +86 -26
- klaude_code/llm/anthropic/client.py +1 -1
- klaude_code/llm/bedrock/client.py +1 -1
- klaude_code/llm/claude/client.py +1 -1
- klaude_code/llm/codex/client.py +1 -1
- klaude_code/llm/google/client.py +1 -1
- klaude_code/llm/openai_compatible/client.py +1 -1
- klaude_code/llm/openai_compatible/tool_call_accumulator.py +1 -1
- klaude_code/llm/openrouter/client.py +1 -1
- klaude_code/llm/openrouter/reasoning.py +1 -1
- klaude_code/llm/responses/client.py +1 -1
- klaude_code/protocol/events/__init__.py +57 -0
- klaude_code/protocol/events/base.py +18 -0
- klaude_code/protocol/events/chat.py +20 -0
- klaude_code/protocol/events/lifecycle.py +22 -0
- klaude_code/protocol/events/metadata.py +15 -0
- klaude_code/protocol/events/streaming.py +43 -0
- klaude_code/protocol/events/system.py +53 -0
- klaude_code/protocol/events/tools.py +23 -0
- klaude_code/protocol/message.py +3 -11
- klaude_code/protocol/model.py +78 -9
- klaude_code/protocol/op.py +5 -0
- klaude_code/protocol/sub_agent/explore.py +0 -15
- klaude_code/protocol/sub_agent/task.py +1 -1
- klaude_code/protocol/sub_agent/web.py +1 -17
- klaude_code/protocol/tools.py +0 -1
- klaude_code/session/session.py +6 -5
- klaude_code/skill/assets/create-plan/SKILL.md +76 -0
- klaude_code/skill/loader.py +1 -1
- klaude_code/skill/system_skills.py +1 -1
- klaude_code/tui/__init__.py +8 -0
- klaude_code/{command → tui/command}/clear_cmd.py +2 -1
- klaude_code/{command → tui/command}/debug_cmd.py +4 -3
- klaude_code/{command → tui/command}/export_cmd.py +2 -1
- klaude_code/{command → tui/command}/export_online_cmd.py +6 -5
- klaude_code/{command → tui/command}/fork_session_cmd.py +10 -9
- klaude_code/{command → tui/command}/help_cmd.py +3 -2
- klaude_code/{command → tui/command}/model_cmd.py +5 -4
- klaude_code/{command → tui/command}/model_select.py +2 -2
- klaude_code/{command → tui/command}/prompt_command.py +4 -3
- klaude_code/{command → tui/command}/refresh_cmd.py +3 -1
- klaude_code/{command → tui/command}/registry.py +16 -6
- klaude_code/{command → tui/command}/release_notes_cmd.py +3 -2
- klaude_code/{command → tui/command}/resume_cmd.py +6 -5
- klaude_code/{command → tui/command}/status_cmd.py +4 -3
- klaude_code/{command → tui/command}/terminal_setup_cmd.py +4 -3
- klaude_code/{command → tui/command}/thinking_cmd.py +4 -3
- klaude_code/tui/commands.py +164 -0
- klaude_code/{ui/renderers → tui/components}/assistant.py +3 -3
- klaude_code/{ui/renderers → tui/components}/bash_syntax.py +2 -2
- klaude_code/{ui/renderers → tui/components}/common.py +1 -1
- klaude_code/tui/components/developer.py +231 -0
- klaude_code/{ui/renderers → tui/components}/diffs.py +2 -2
- klaude_code/{ui/renderers → tui/components}/errors.py +2 -2
- klaude_code/{ui/renderers → tui/components}/metadata.py +34 -21
- klaude_code/{ui → tui/components}/rich/markdown.py +78 -34
- klaude_code/{ui → tui/components}/rich/status.py +2 -2
- klaude_code/{ui → tui/components}/rich/theme.py +12 -5
- klaude_code/{ui/renderers → tui/components}/sub_agent.py +23 -43
- klaude_code/{ui/renderers → tui/components}/thinking.py +3 -3
- klaude_code/{ui/renderers → tui/components}/tools.py +11 -48
- klaude_code/{ui/renderers → tui/components}/user_input.py +3 -20
- klaude_code/tui/display.py +85 -0
- klaude_code/{ui/modes/repl → tui/input}/__init__.py +1 -1
- klaude_code/{ui/modes/repl → tui/input}/completers.py +1 -1
- klaude_code/{ui/modes/repl/input_prompt_toolkit.py → tui/input/prompt_toolkit.py} +11 -7
- klaude_code/tui/machine.py +606 -0
- klaude_code/tui/renderer.py +707 -0
- klaude_code/tui/runner.py +321 -0
- klaude_code/tui/terminal/__init__.py +56 -0
- klaude_code/{ui → tui}/terminal/color.py +1 -1
- klaude_code/{ui → tui}/terminal/control.py +1 -1
- klaude_code/{ui → tui}/terminal/notifier.py +1 -1
- klaude_code/{ui → tui}/terminal/selector.py +36 -17
- klaude_code/ui/__init__.py +6 -50
- klaude_code/ui/core/display.py +3 -3
- klaude_code/ui/core/input.py +2 -1
- klaude_code/ui/{modes/debug/display.py → debug_mode.py} +1 -1
- klaude_code/ui/{modes/exec/display.py → exec_mode.py} +1 -4
- klaude_code/ui/terminal/__init__.py +6 -54
- klaude_code/ui/terminal/title.py +31 -0
- klaude_code/update.py +163 -0
- {klaude_code-2.0.1.dist-info → klaude_code-2.1.0.dist-info}/METADATA +1 -1
- klaude_code-2.1.0.dist-info/RECORD +235 -0
- klaude_code/cli/runtime.py +0 -525
- klaude_code/core/prompt.py +0 -108
- klaude_code/core/tool/file/move_tool.md +0 -41
- klaude_code/core/tool/file/move_tool.py +0 -435
- klaude_code/core/tool/tool_context.py +0 -148
- klaude_code/protocol/events.py +0 -194
- klaude_code/skill/assets/dev-docs/SKILL.md +0 -108
- klaude_code/trace/__init__.py +0 -21
- klaude_code/ui/core/stage_manager.py +0 -48
- klaude_code/ui/modes/__init__.py +0 -1
- klaude_code/ui/modes/debug/__init__.py +0 -1
- klaude_code/ui/modes/exec/__init__.py +0 -1
- klaude_code/ui/modes/repl/display.py +0 -61
- klaude_code/ui/modes/repl/event_handler.py +0 -634
- klaude_code/ui/modes/repl/renderer.py +0 -463
- klaude_code/ui/renderers/developer.py +0 -215
- klaude_code/ui/utils/__init__.py +0 -1
- klaude_code-2.0.1.dist-info/RECORD +0 -229
- /klaude_code/{trace/log.py → log.py} +0 -0
- /klaude_code/{command → tui/command}/__init__.py +0 -0
- /klaude_code/{command → tui/command}/command_abc.py +0 -0
- /klaude_code/{command → tui/command}/prompt-commit.md +0 -0
- /klaude_code/{command → tui/command}/prompt-init.md +0 -0
- /klaude_code/{ui/renderers → tui/components}/__init__.py +0 -0
- /klaude_code/{ui/renderers → tui/components}/mermaid_viewer.py +0 -0
- /klaude_code/{ui → tui/components}/rich/__init__.py +0 -0
- /klaude_code/{ui → tui/components}/rich/cjk_wrap.py +0 -0
- /klaude_code/{ui → tui/components}/rich/code_panel.py +0 -0
- /klaude_code/{ui → tui/components}/rich/live.py +0 -0
- /klaude_code/{ui → tui/components}/rich/quote.py +0 -0
- /klaude_code/{ui → tui/components}/rich/searchable_text.py +0 -0
- /klaude_code/{ui/modes/repl → tui/input}/clipboard.py +0 -0
- /klaude_code/{ui/modes/repl → tui/input}/key_bindings.py +0 -0
- /klaude_code/{ui → tui}/terminal/image.py +0 -0
- /klaude_code/{ui → tui}/terminal/progress_bar.py +0 -0
- /klaude_code/ui/{utils/common.py → common.py} +0 -0
- {klaude_code-2.0.1.dist-info → klaude_code-2.1.0.dist-info}/WHEEL +0 -0
- {klaude_code-2.0.1.dist-info → klaude_code-2.1.0.dist-info}/entry_points.txt +0 -0
klaude_code/core/executor.py
CHANGED
|
@@ -10,22 +10,22 @@ from __future__ import annotations
|
|
|
10
10
|
import asyncio
|
|
11
11
|
import subprocess
|
|
12
12
|
import sys
|
|
13
|
-
from collections.abc import Callable
|
|
13
|
+
from collections.abc import Awaitable, Callable
|
|
14
14
|
from dataclasses import dataclass
|
|
15
15
|
from pathlib import Path
|
|
16
16
|
|
|
17
17
|
from klaude_code.config import load_config
|
|
18
|
-
from klaude_code.core.agent import Agent
|
|
18
|
+
from klaude_code.core.agent import Agent
|
|
19
|
+
from klaude_code.core.agent_profile import DefaultModelProfileProvider, ModelProfileProvider
|
|
19
20
|
from klaude_code.core.manager import LLMClients, SubAgentManager
|
|
20
|
-
from klaude_code.core.tool import current_run_subtask_callback
|
|
21
21
|
from klaude_code.llm.registry import create_llm_client
|
|
22
|
+
from klaude_code.log import DebugType, log_debug
|
|
22
23
|
from klaude_code.protocol import commands, events, message, model, op
|
|
23
|
-
from klaude_code.protocol.llm_param import Thinking
|
|
24
|
+
from klaude_code.protocol.llm_param import LLMConfigParameter, Thinking
|
|
24
25
|
from klaude_code.protocol.op_handler import OperationHandler
|
|
25
26
|
from klaude_code.protocol.sub_agent import SubAgentResult
|
|
26
27
|
from klaude_code.session.export import build_export_html, get_default_export_path
|
|
27
28
|
from klaude_code.session.session import Session
|
|
28
|
-
from klaude_code.trace import DebugType, log_debug
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
@dataclass
|
|
@@ -80,45 +80,26 @@ class TaskManager:
|
|
|
80
80
|
self._tasks.clear()
|
|
81
81
|
|
|
82
82
|
|
|
83
|
-
class
|
|
84
|
-
"""
|
|
85
|
-
Context object providing shared state and operations for the executor.
|
|
86
|
-
|
|
87
|
-
This context is passed to operations when they execute, allowing them
|
|
88
|
-
to access shared resources like the event queue and active sessions.
|
|
89
|
-
|
|
90
|
-
Implements the OperationHandler protocol via structural subtyping.
|
|
91
|
-
"""
|
|
83
|
+
class AgentRuntime:
|
|
84
|
+
"""Coordinate agent lifecycle and in-flight tasks for the executor."""
|
|
92
85
|
|
|
93
86
|
def __init__(
|
|
94
87
|
self,
|
|
95
|
-
|
|
88
|
+
*,
|
|
89
|
+
emit_event: Callable[[events.Event], Awaitable[None]],
|
|
96
90
|
llm_clients: LLMClients,
|
|
97
|
-
model_profile_provider: ModelProfileProvider
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
self.
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
self.
|
|
105
|
-
|
|
106
|
-
self.task_manager = TaskManager()
|
|
107
|
-
self.sub_agent_manager = SubAgentManager(event_queue, llm_clients, resolved_profile_provider)
|
|
108
|
-
self._on_model_change = on_model_change
|
|
91
|
+
model_profile_provider: ModelProfileProvider,
|
|
92
|
+
task_manager: TaskManager,
|
|
93
|
+
sub_agent_manager: SubAgentManager,
|
|
94
|
+
) -> None:
|
|
95
|
+
self._emit_event = emit_event
|
|
96
|
+
self._llm_clients = llm_clients
|
|
97
|
+
self._model_profile_provider = model_profile_provider
|
|
98
|
+
self._task_manager = task_manager
|
|
99
|
+
self._sub_agent_manager = sub_agent_manager
|
|
109
100
|
self._agent: Agent | None = None
|
|
110
101
|
|
|
111
|
-
async def emit_event(self, event: events.Event) -> None:
|
|
112
|
-
"""Emit an event to the UI display system."""
|
|
113
|
-
await self.event_queue.put(event)
|
|
114
|
-
|
|
115
102
|
def current_session_id(self) -> str | None:
|
|
116
|
-
"""Return the primary active session id, if any.
|
|
117
|
-
|
|
118
|
-
This is a convenience wrapper used by the CLI, which conceptually
|
|
119
|
-
operates on a single interactive session per process.
|
|
120
|
-
"""
|
|
121
|
-
|
|
122
103
|
agent = self._agent
|
|
123
104
|
if agent is None:
|
|
124
105
|
return None
|
|
@@ -126,19 +107,11 @@ class ExecutorContext:
|
|
|
126
107
|
|
|
127
108
|
@property
|
|
128
109
|
def current_agent(self) -> Agent | None:
|
|
129
|
-
"""Return the currently active agent, if any."""
|
|
130
|
-
|
|
131
110
|
return self._agent
|
|
132
111
|
|
|
133
|
-
async def
|
|
134
|
-
"""Return the active agent, creating or loading a session as needed.
|
|
112
|
+
async def ensure_agent(self, session_id: str | None = None) -> Agent:
|
|
113
|
+
"""Return the active agent, creating or loading a session as needed."""
|
|
135
114
|
|
|
136
|
-
If ``session_id`` is ``None``, a new session is created with an
|
|
137
|
-
auto-generated ID. If provided, the executor attempts to resume the
|
|
138
|
-
session from disk or creates a new one if not found.
|
|
139
|
-
"""
|
|
140
|
-
|
|
141
|
-
# Fast-path: reuse current agent when the session id already matches.
|
|
142
115
|
if session_id is not None and self._agent is not None and self._agent.session.id == session_id:
|
|
143
116
|
return self._agent
|
|
144
117
|
|
|
@@ -147,20 +120,21 @@ class ExecutorContext:
|
|
|
147
120
|
if (
|
|
148
121
|
session.model_thinking is not None
|
|
149
122
|
and session.model_name
|
|
150
|
-
and session.model_name == self.
|
|
123
|
+
and session.model_name == self._llm_clients.main.model_name
|
|
151
124
|
):
|
|
152
|
-
self.
|
|
125
|
+
self._llm_clients.main.get_llm_config().thinking = session.model_thinking
|
|
153
126
|
|
|
154
|
-
profile = self.
|
|
127
|
+
profile = self._model_profile_provider.build_profile(self._llm_clients.main)
|
|
155
128
|
agent = Agent(session=session, profile=profile)
|
|
156
129
|
|
|
157
130
|
async for evt in agent.replay_history():
|
|
158
|
-
await self.
|
|
131
|
+
await self._emit_event(evt)
|
|
159
132
|
|
|
160
|
-
await self.
|
|
133
|
+
await self._emit_event(
|
|
161
134
|
events.WelcomeEvent(
|
|
135
|
+
session_id=session.id,
|
|
162
136
|
work_dir=str(session.work_dir),
|
|
163
|
-
llm_config=self.
|
|
137
|
+
llm_config=self._llm_clients.main.get_llm_config(),
|
|
164
138
|
)
|
|
165
139
|
)
|
|
166
140
|
|
|
@@ -172,94 +146,44 @@ class ExecutorContext:
|
|
|
172
146
|
)
|
|
173
147
|
return agent
|
|
174
148
|
|
|
175
|
-
async def
|
|
176
|
-
|
|
177
|
-
await self._ensure_agent(operation.session_id)
|
|
149
|
+
async def init_agent(self, session_id: str | None) -> None:
|
|
150
|
+
await self.ensure_agent(session_id)
|
|
178
151
|
|
|
179
|
-
async def
|
|
180
|
-
agent = await self.
|
|
181
|
-
existing_active = self.task_manager.get(operation.id)
|
|
182
|
-
if existing_active is not None and not existing_active.task.done():
|
|
183
|
-
raise RuntimeError(f"Active task already registered for operation {operation.id}")
|
|
184
|
-
task: asyncio.Task[None] = asyncio.create_task(
|
|
185
|
-
self._run_agent_task(agent, operation.input, operation.id, operation.session_id)
|
|
186
|
-
)
|
|
187
|
-
self.task_manager.register(operation.id, task, operation.session_id)
|
|
188
|
-
|
|
189
|
-
async def handle_change_model(self, operation: op.ChangeModelOperation) -> None:
|
|
190
|
-
agent = await self._ensure_agent(operation.session_id)
|
|
191
|
-
config = load_config()
|
|
192
|
-
|
|
193
|
-
llm_config = config.get_model_config(operation.model_name)
|
|
194
|
-
llm_client = create_llm_client(llm_config)
|
|
195
|
-
agent.set_model_profile(self.model_profile_provider.build_profile(llm_client))
|
|
196
|
-
|
|
197
|
-
agent.session.model_config_name = operation.model_name
|
|
198
|
-
agent.session.model_thinking = llm_config.thinking
|
|
199
|
-
|
|
200
|
-
if operation.save_as_default:
|
|
201
|
-
config.main_model = operation.model_name
|
|
202
|
-
await config.save()
|
|
152
|
+
async def run_agent(self, operation: op.RunAgentOperation) -> None:
|
|
153
|
+
agent = await self.ensure_agent(operation.session_id)
|
|
203
154
|
|
|
204
|
-
if operation.
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
155
|
+
if operation.emit_user_message_event:
|
|
156
|
+
await self._emit_event(
|
|
157
|
+
events.UserMessageEvent(
|
|
158
|
+
content=operation.input.text,
|
|
159
|
+
session_id=agent.session.id,
|
|
160
|
+
images=operation.input.images,
|
|
161
|
+
)
|
|
209
162
|
)
|
|
210
|
-
agent.session.append_history([developer_item])
|
|
211
|
-
await self.emit_event(events.DeveloperMessageEvent(session_id=agent.session.id, item=developer_item))
|
|
212
|
-
|
|
213
|
-
if self._on_model_change is not None:
|
|
214
|
-
self._on_model_change(llm_client.model_name)
|
|
215
|
-
|
|
216
|
-
if operation.emit_welcome_event:
|
|
217
|
-
await self.emit_event(events.WelcomeEvent(llm_config=llm_config, work_dir=str(agent.session.work_dir)))
|
|
218
|
-
|
|
219
|
-
async def handle_change_thinking(self, operation: op.ChangeThinkingOperation) -> None:
|
|
220
|
-
"""Handle a change thinking operation.
|
|
221
|
-
|
|
222
|
-
Interactive thinking selection must happen in the UI/CLI layer. Core only
|
|
223
|
-
applies a concrete thinking configuration.
|
|
224
|
-
"""
|
|
225
|
-
agent = await self._ensure_agent(operation.session_id)
|
|
226
|
-
|
|
227
|
-
config = agent.profile.llm_client.get_llm_config()
|
|
228
|
-
|
|
229
|
-
def _format_thinking_for_display(thinking: Thinking | None) -> str:
|
|
230
|
-
if thinking is None:
|
|
231
|
-
return "not configured"
|
|
232
|
-
if thinking.reasoning_effort:
|
|
233
|
-
return f"reasoning_effort={thinking.reasoning_effort}"
|
|
234
|
-
if thinking.type == "disabled":
|
|
235
|
-
return "off"
|
|
236
|
-
if thinking.type == "enabled":
|
|
237
|
-
if thinking.budget_tokens is None:
|
|
238
|
-
return "enabled"
|
|
239
|
-
return f"enabled (budget_tokens={thinking.budget_tokens})"
|
|
240
|
-
return "not set"
|
|
241
|
-
|
|
242
|
-
if operation.thinking is None:
|
|
243
|
-
raise ValueError("thinking must be provided; interactive selection belongs to UI")
|
|
244
|
-
|
|
245
|
-
current = _format_thinking_for_display(config.thinking)
|
|
246
|
-
config.thinking = operation.thinking
|
|
247
|
-
agent.session.model_thinking = operation.thinking
|
|
248
|
-
new_status = _format_thinking_for_display(config.thinking)
|
|
249
163
|
|
|
250
|
-
if operation.
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
164
|
+
if operation.persist_user_input:
|
|
165
|
+
agent.session.append_history(
|
|
166
|
+
[
|
|
167
|
+
message.UserMessage(
|
|
168
|
+
parts=message.parts_from_text_and_images(
|
|
169
|
+
operation.input.text,
|
|
170
|
+
operation.input.images,
|
|
171
|
+
)
|
|
172
|
+
)
|
|
173
|
+
]
|
|
254
174
|
)
|
|
255
|
-
agent.session.append_history([developer_item])
|
|
256
|
-
await self.emit_event(events.DeveloperMessageEvent(session_id=agent.session.id, item=developer_item))
|
|
257
175
|
|
|
258
|
-
|
|
259
|
-
|
|
176
|
+
existing_active = self._task_manager.get(operation.id)
|
|
177
|
+
if existing_active is not None and not existing_active.task.done():
|
|
178
|
+
raise RuntimeError(f"Active task already registered for operation {operation.id}")
|
|
260
179
|
|
|
261
|
-
|
|
262
|
-
|
|
180
|
+
task: asyncio.Task[None] = asyncio.create_task(
|
|
181
|
+
self._run_agent_task(agent, operation.input, operation.id, operation.session_id)
|
|
182
|
+
)
|
|
183
|
+
self._task_manager.register(operation.id, task, operation.session_id)
|
|
184
|
+
|
|
185
|
+
async def clear_session(self, session_id: str) -> None:
|
|
186
|
+
agent = await self.ensure_agent(session_id)
|
|
263
187
|
new_session = Session.create(work_dir=agent.session.work_dir)
|
|
264
188
|
new_session.model_name = agent.session.model_name
|
|
265
189
|
new_session.model_config_name = agent.session.model_config_name
|
|
@@ -268,37 +192,37 @@ class ExecutorContext:
|
|
|
268
192
|
|
|
269
193
|
developer_item = message.DeveloperMessage(
|
|
270
194
|
parts=message.text_parts_from_str("started new conversation"),
|
|
271
|
-
|
|
195
|
+
ui_extra=model.build_command_output_extra(commands.CommandName.CLEAR),
|
|
272
196
|
)
|
|
273
|
-
await self.
|
|
274
|
-
await self.
|
|
197
|
+
await self._emit_event(events.DeveloperMessageEvent(session_id=agent.session.id, item=developer_item))
|
|
198
|
+
await self._emit_event(
|
|
275
199
|
events.WelcomeEvent(
|
|
200
|
+
session_id=agent.session.id,
|
|
276
201
|
work_dir=str(agent.session.work_dir),
|
|
277
|
-
llm_config=self.
|
|
202
|
+
llm_config=self._llm_clients.main.get_llm_config(),
|
|
278
203
|
)
|
|
279
204
|
)
|
|
280
205
|
|
|
281
|
-
async def
|
|
282
|
-
target_session = Session.load(
|
|
206
|
+
async def resume_session(self, target_session_id: str) -> None:
|
|
207
|
+
target_session = Session.load(target_session_id)
|
|
283
208
|
if (
|
|
284
209
|
target_session.model_thinking is not None
|
|
285
210
|
and target_session.model_name
|
|
286
|
-
and target_session.model_name == self.
|
|
211
|
+
and target_session.model_name == self._llm_clients.main.model_name
|
|
287
212
|
):
|
|
288
|
-
self.
|
|
289
|
-
|
|
290
|
-
profile = self.model_profile_provider.build_profile(self.llm_clients.main)
|
|
291
|
-
from klaude_code.core.agent import Agent
|
|
213
|
+
self._llm_clients.main.get_llm_config().thinking = target_session.model_thinking
|
|
292
214
|
|
|
215
|
+
profile = self._model_profile_provider.build_profile(self._llm_clients.main)
|
|
293
216
|
agent = Agent(session=target_session, profile=profile)
|
|
294
217
|
|
|
295
218
|
async for evt in agent.replay_history():
|
|
296
|
-
await self.
|
|
219
|
+
await self._emit_event(evt)
|
|
297
220
|
|
|
298
|
-
await self.
|
|
221
|
+
await self._emit_event(
|
|
299
222
|
events.WelcomeEvent(
|
|
223
|
+
session_id=target_session.id,
|
|
300
224
|
work_dir=str(target_session.work_dir),
|
|
301
|
-
llm_config=self.
|
|
225
|
+
llm_config=self._llm_clients.main.get_llm_config(),
|
|
302
226
|
)
|
|
303
227
|
)
|
|
304
228
|
|
|
@@ -309,29 +233,38 @@ class ExecutorContext:
|
|
|
309
233
|
debug_type=DebugType.EXECUTION,
|
|
310
234
|
)
|
|
311
235
|
|
|
312
|
-
async def
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
await asyncio.to_thread(output_path.write_text, html_doc, "utf-8")
|
|
319
|
-
await asyncio.to_thread(self._open_file, output_path)
|
|
320
|
-
developer_item = message.DeveloperMessage(
|
|
321
|
-
parts=message.text_parts_from_str(f"Session exported and opened: {output_path}"),
|
|
322
|
-
command_output=model.CommandOutput(command_name=commands.CommandName.EXPORT),
|
|
323
|
-
)
|
|
324
|
-
agent.session.append_history([developer_item])
|
|
325
|
-
await self.emit_event(events.DeveloperMessageEvent(session_id=agent.session.id, item=developer_item))
|
|
326
|
-
except Exception as exc: # pragma: no cover
|
|
327
|
-
import traceback
|
|
236
|
+
async def interrupt(self, target_session_id: str | None) -> None:
|
|
237
|
+
if target_session_id is not None:
|
|
238
|
+
session_ids: list[str] = [target_session_id]
|
|
239
|
+
else:
|
|
240
|
+
agent = self._agent
|
|
241
|
+
session_ids = [agent.session.id] if agent is not None else []
|
|
328
242
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
243
|
+
for sid in session_ids:
|
|
244
|
+
agent = self._get_active_agent(sid)
|
|
245
|
+
if agent is not None:
|
|
246
|
+
for evt in agent.cancel():
|
|
247
|
+
await self._emit_event(evt)
|
|
248
|
+
|
|
249
|
+
await self._emit_event(events.InterruptEvent(session_id=target_session_id or "all"))
|
|
250
|
+
|
|
251
|
+
if target_session_id is None:
|
|
252
|
+
session_filter: set[str] | None = None
|
|
253
|
+
else:
|
|
254
|
+
session_filter = {target_session_id}
|
|
255
|
+
|
|
256
|
+
tasks_to_cancel = self._task_manager.cancel_tasks_for_sessions(session_filter)
|
|
257
|
+
|
|
258
|
+
scope = target_session_id or "all"
|
|
259
|
+
log_debug(
|
|
260
|
+
f"Interrupting {len(tasks_to_cancel)} task(s) for: {scope}",
|
|
261
|
+
style="yellow",
|
|
262
|
+
debug_type=DebugType.EXECUTION,
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
for task_id, task in tasks_to_cancel:
|
|
266
|
+
task.cancel()
|
|
267
|
+
self._task_manager.remove(task_id)
|
|
335
268
|
|
|
336
269
|
async def _run_agent_task(
|
|
337
270
|
self,
|
|
@@ -347,15 +280,14 @@ class ExecutorContext:
|
|
|
347
280
|
debug_type=DebugType.EXECUTION,
|
|
348
281
|
)
|
|
349
282
|
|
|
350
|
-
async def _runner(
|
|
351
|
-
|
|
283
|
+
async def _runner(
|
|
284
|
+
state: model.SubAgentState,
|
|
285
|
+
record_session_id: Callable[[str], None] | None,
|
|
286
|
+
) -> SubAgentResult:
|
|
287
|
+
return await self._sub_agent_manager.run_sub_agent(agent, state, record_session_id=record_session_id)
|
|
352
288
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
async for event in agent.run_task(user_input):
|
|
356
|
-
await self.emit_event(event)
|
|
357
|
-
finally:
|
|
358
|
-
current_run_subtask_callback.reset(token)
|
|
289
|
+
async for event in agent.run_task(user_input, run_subtask=_runner):
|
|
290
|
+
await self._emit_event(event)
|
|
359
291
|
|
|
360
292
|
except asyncio.CancelledError:
|
|
361
293
|
log_debug(
|
|
@@ -363,7 +295,7 @@ class ExecutorContext:
|
|
|
363
295
|
style="yellow",
|
|
364
296
|
debug_type=DebugType.EXECUTION,
|
|
365
297
|
)
|
|
366
|
-
await self.
|
|
298
|
+
await self._emit_event(events.TaskFinishEvent(session_id=session_id, task_result="task cancelled"))
|
|
367
299
|
|
|
368
300
|
except Exception as e:
|
|
369
301
|
import traceback
|
|
@@ -374,20 +306,234 @@ class ExecutorContext:
|
|
|
374
306
|
debug_type=DebugType.EXECUTION,
|
|
375
307
|
)
|
|
376
308
|
log_debug(traceback.format_exc(), style="red", debug_type=DebugType.EXECUTION)
|
|
377
|
-
await self.
|
|
309
|
+
await self._emit_event(
|
|
378
310
|
events.ErrorEvent(
|
|
379
311
|
error_message=f"Agent task failed: [{e.__class__.__name__}] {e!s} {traceback.format_exc()}",
|
|
380
312
|
can_retry=False,
|
|
313
|
+
session_id=session_id,
|
|
381
314
|
)
|
|
382
315
|
)
|
|
383
316
|
finally:
|
|
384
|
-
self.
|
|
317
|
+
self._task_manager.remove(task_id)
|
|
385
318
|
log_debug(
|
|
386
319
|
f"Cleaned up agent task {task_id}",
|
|
387
320
|
style="cyan",
|
|
388
321
|
debug_type=DebugType.EXECUTION,
|
|
389
322
|
)
|
|
390
323
|
|
|
324
|
+
def _get_active_agent(self, session_id: str) -> Agent | None:
|
|
325
|
+
agent = self._agent
|
|
326
|
+
if agent is None:
|
|
327
|
+
return None
|
|
328
|
+
if agent.session.id != session_id:
|
|
329
|
+
return None
|
|
330
|
+
return agent
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
class ModelSwitcher:
|
|
334
|
+
"""Apply model changes to an agent session."""
|
|
335
|
+
|
|
336
|
+
def __init__(self, model_profile_provider: ModelProfileProvider) -> None:
|
|
337
|
+
self._model_profile_provider = model_profile_provider
|
|
338
|
+
|
|
339
|
+
async def change_model(
|
|
340
|
+
self,
|
|
341
|
+
agent: Agent,
|
|
342
|
+
*,
|
|
343
|
+
model_name: str,
|
|
344
|
+
save_as_default: bool,
|
|
345
|
+
) -> tuple[LLMConfigParameter, str]:
|
|
346
|
+
config = load_config()
|
|
347
|
+
llm_config = config.get_model_config(model_name)
|
|
348
|
+
llm_client = create_llm_client(llm_config)
|
|
349
|
+
agent.set_model_profile(self._model_profile_provider.build_profile(llm_client))
|
|
350
|
+
|
|
351
|
+
agent.session.model_config_name = model_name
|
|
352
|
+
agent.session.model_thinking = llm_config.thinking
|
|
353
|
+
|
|
354
|
+
if save_as_default:
|
|
355
|
+
config.main_model = model_name
|
|
356
|
+
await config.save()
|
|
357
|
+
|
|
358
|
+
return llm_config, llm_client.model_name
|
|
359
|
+
|
|
360
|
+
def change_thinking(self, agent: Agent, *, thinking: Thinking) -> Thinking | None:
|
|
361
|
+
"""Apply thinking configuration to the agent's active LLM config and persisted session."""
|
|
362
|
+
|
|
363
|
+
config = agent.profile.llm_client.get_llm_config()
|
|
364
|
+
previous = config.thinking
|
|
365
|
+
config.thinking = thinking
|
|
366
|
+
agent.session.model_thinking = thinking
|
|
367
|
+
return previous
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
class ExecutorContext:
|
|
371
|
+
"""
|
|
372
|
+
Context object providing shared state and operations for the executor.
|
|
373
|
+
|
|
374
|
+
This context is passed to operations when they execute, allowing them
|
|
375
|
+
to access shared resources like the event queue and active sessions.
|
|
376
|
+
|
|
377
|
+
Implements the OperationHandler protocol via structural subtyping.
|
|
378
|
+
"""
|
|
379
|
+
|
|
380
|
+
def __init__(
|
|
381
|
+
self,
|
|
382
|
+
event_queue: asyncio.Queue[events.Event],
|
|
383
|
+
llm_clients: LLMClients,
|
|
384
|
+
model_profile_provider: ModelProfileProvider | None = None,
|
|
385
|
+
on_model_change: Callable[[str], None] | None = None,
|
|
386
|
+
):
|
|
387
|
+
self.event_queue: asyncio.Queue[events.Event] = event_queue
|
|
388
|
+
self.llm_clients: LLMClients = llm_clients
|
|
389
|
+
|
|
390
|
+
resolved_profile_provider = model_profile_provider or DefaultModelProfileProvider()
|
|
391
|
+
self.model_profile_provider: ModelProfileProvider = resolved_profile_provider
|
|
392
|
+
|
|
393
|
+
self.task_manager = TaskManager()
|
|
394
|
+
self.sub_agent_manager = SubAgentManager(event_queue, llm_clients, resolved_profile_provider)
|
|
395
|
+
self._on_model_change = on_model_change
|
|
396
|
+
self._agent_runtime = AgentRuntime(
|
|
397
|
+
emit_event=self.emit_event,
|
|
398
|
+
llm_clients=llm_clients,
|
|
399
|
+
model_profile_provider=resolved_profile_provider,
|
|
400
|
+
task_manager=self.task_manager,
|
|
401
|
+
sub_agent_manager=self.sub_agent_manager,
|
|
402
|
+
)
|
|
403
|
+
self._model_switcher = ModelSwitcher(resolved_profile_provider)
|
|
404
|
+
|
|
405
|
+
async def emit_event(self, event: events.Event) -> None:
|
|
406
|
+
"""Emit an event to the UI display system."""
|
|
407
|
+
await self.event_queue.put(event)
|
|
408
|
+
|
|
409
|
+
def current_session_id(self) -> str | None:
|
|
410
|
+
"""Return the primary active session id, if any.
|
|
411
|
+
|
|
412
|
+
This is a convenience wrapper used by the CLI, which conceptually
|
|
413
|
+
operates on a single interactive session per process.
|
|
414
|
+
"""
|
|
415
|
+
|
|
416
|
+
return self._agent_runtime.current_session_id()
|
|
417
|
+
|
|
418
|
+
@property
|
|
419
|
+
def current_agent(self) -> Agent | None:
|
|
420
|
+
"""Return the currently active agent, if any."""
|
|
421
|
+
|
|
422
|
+
return self._agent_runtime.current_agent
|
|
423
|
+
|
|
424
|
+
async def handle_init_agent(self, operation: op.InitAgentOperation) -> None:
|
|
425
|
+
"""Initialize an agent for a session and replay history to UI."""
|
|
426
|
+
await self._agent_runtime.init_agent(operation.session_id)
|
|
427
|
+
|
|
428
|
+
async def handle_run_agent(self, operation: op.RunAgentOperation) -> None:
|
|
429
|
+
await self._agent_runtime.run_agent(operation)
|
|
430
|
+
|
|
431
|
+
async def handle_change_model(self, operation: op.ChangeModelOperation) -> None:
|
|
432
|
+
agent = await self._agent_runtime.ensure_agent(operation.session_id)
|
|
433
|
+
llm_config, llm_client_name = await self._model_switcher.change_model(
|
|
434
|
+
agent,
|
|
435
|
+
model_name=operation.model_name,
|
|
436
|
+
save_as_default=operation.save_as_default,
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
if operation.emit_switch_message:
|
|
440
|
+
default_note = " (saved as default)" if operation.save_as_default else ""
|
|
441
|
+
developer_item = message.DeveloperMessage(
|
|
442
|
+
parts=message.text_parts_from_str(f"Switched to: {llm_config.model}{default_note}"),
|
|
443
|
+
ui_extra=model.build_command_output_extra(commands.CommandName.MODEL),
|
|
444
|
+
)
|
|
445
|
+
agent.session.append_history([developer_item])
|
|
446
|
+
await self.emit_event(events.DeveloperMessageEvent(session_id=agent.session.id, item=developer_item))
|
|
447
|
+
|
|
448
|
+
if self._on_model_change is not None:
|
|
449
|
+
self._on_model_change(llm_client_name)
|
|
450
|
+
|
|
451
|
+
if operation.emit_welcome_event:
|
|
452
|
+
await self.emit_event(
|
|
453
|
+
events.WelcomeEvent(
|
|
454
|
+
session_id=agent.session.id,
|
|
455
|
+
llm_config=llm_config,
|
|
456
|
+
work_dir=str(agent.session.work_dir),
|
|
457
|
+
show_klaude_code_info=False,
|
|
458
|
+
)
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
async def handle_change_thinking(self, operation: op.ChangeThinkingOperation) -> None:
|
|
462
|
+
"""Handle a change thinking operation.
|
|
463
|
+
|
|
464
|
+
Interactive thinking selection must happen in the UI/CLI layer. Core only
|
|
465
|
+
applies a concrete thinking configuration.
|
|
466
|
+
"""
|
|
467
|
+
agent = await self._agent_runtime.ensure_agent(operation.session_id)
|
|
468
|
+
|
|
469
|
+
def _format_thinking_for_display(thinking: Thinking | None) -> str:
|
|
470
|
+
if thinking is None:
|
|
471
|
+
return "not configured"
|
|
472
|
+
if thinking.reasoning_effort:
|
|
473
|
+
return f"reasoning_effort={thinking.reasoning_effort}"
|
|
474
|
+
if thinking.type == "disabled":
|
|
475
|
+
return "off"
|
|
476
|
+
if thinking.type == "enabled":
|
|
477
|
+
if thinking.budget_tokens is None:
|
|
478
|
+
return "enabled"
|
|
479
|
+
return f"enabled (budget_tokens={thinking.budget_tokens})"
|
|
480
|
+
return "not set"
|
|
481
|
+
|
|
482
|
+
if operation.thinking is None:
|
|
483
|
+
raise ValueError("thinking must be provided; interactive selection belongs to UI")
|
|
484
|
+
|
|
485
|
+
previous = self._model_switcher.change_thinking(agent, thinking=operation.thinking)
|
|
486
|
+
current = _format_thinking_for_display(previous)
|
|
487
|
+
new_status = _format_thinking_for_display(operation.thinking)
|
|
488
|
+
|
|
489
|
+
if operation.emit_switch_message:
|
|
490
|
+
developer_item = message.DeveloperMessage(
|
|
491
|
+
parts=message.text_parts_from_str(f"Thinking changed: {current} -> {new_status}"),
|
|
492
|
+
ui_extra=model.build_command_output_extra(commands.CommandName.THINKING),
|
|
493
|
+
)
|
|
494
|
+
agent.session.append_history([developer_item])
|
|
495
|
+
await self.emit_event(events.DeveloperMessageEvent(session_id=agent.session.id, item=developer_item))
|
|
496
|
+
|
|
497
|
+
if operation.emit_welcome_event:
|
|
498
|
+
await self.emit_event(
|
|
499
|
+
events.WelcomeEvent(
|
|
500
|
+
session_id=agent.session.id,
|
|
501
|
+
work_dir=str(agent.session.work_dir),
|
|
502
|
+
llm_config=agent.profile.llm_client.get_llm_config(),
|
|
503
|
+
show_klaude_code_info=False,
|
|
504
|
+
)
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
async def handle_clear_session(self, operation: op.ClearSessionOperation) -> None:
|
|
508
|
+
await self._agent_runtime.clear_session(operation.session_id)
|
|
509
|
+
|
|
510
|
+
async def handle_resume_session(self, operation: op.ResumeSessionOperation) -> None:
|
|
511
|
+
await self._agent_runtime.resume_session(operation.target_session_id)
|
|
512
|
+
|
|
513
|
+
async def handle_export_session(self, operation: op.ExportSessionOperation) -> None:
|
|
514
|
+
agent = await self._agent_runtime.ensure_agent(operation.session_id)
|
|
515
|
+
try:
|
|
516
|
+
output_path = self._resolve_export_output_path(operation.output_path, agent.session)
|
|
517
|
+
html_doc = self._build_export_html(agent)
|
|
518
|
+
await asyncio.to_thread(output_path.parent.mkdir, parents=True, exist_ok=True)
|
|
519
|
+
await asyncio.to_thread(output_path.write_text, html_doc, "utf-8")
|
|
520
|
+
await asyncio.to_thread(self._open_file, output_path)
|
|
521
|
+
developer_item = message.DeveloperMessage(
|
|
522
|
+
parts=message.text_parts_from_str(f"Session exported and opened: {output_path}"),
|
|
523
|
+
ui_extra=model.build_command_output_extra(commands.CommandName.EXPORT),
|
|
524
|
+
)
|
|
525
|
+
agent.session.append_history([developer_item])
|
|
526
|
+
await self.emit_event(events.DeveloperMessageEvent(session_id=agent.session.id, item=developer_item))
|
|
527
|
+
except Exception as exc: # pragma: no cover
|
|
528
|
+
import traceback
|
|
529
|
+
|
|
530
|
+
developer_item = message.DeveloperMessage(
|
|
531
|
+
parts=message.text_parts_from_str(f"Failed to export session: {exc}\n{traceback.format_exc()}"),
|
|
532
|
+
ui_extra=model.build_command_output_extra(commands.CommandName.EXPORT, is_error=True),
|
|
533
|
+
)
|
|
534
|
+
agent.session.append_history([developer_item])
|
|
535
|
+
await self.emit_event(events.DeveloperMessageEvent(session_id=agent.session.id, item=developer_item))
|
|
536
|
+
|
|
391
537
|
def _resolve_export_output_path(self, raw: str | None, session: Session) -> Path:
|
|
392
538
|
trimmed = (raw or "").strip()
|
|
393
539
|
if trimmed:
|
|
@@ -432,43 +578,7 @@ class ExecutorContext:
|
|
|
432
578
|
async def handle_interrupt(self, operation: op.InterruptOperation) -> None:
|
|
433
579
|
"""Handle an interrupt by invoking agent.cancel() and cancelling tasks."""
|
|
434
580
|
|
|
435
|
-
|
|
436
|
-
if operation.target_session_id is not None:
|
|
437
|
-
session_ids: list[str] = [operation.target_session_id]
|
|
438
|
-
else:
|
|
439
|
-
agent = self._agent
|
|
440
|
-
session_ids = [agent.session.id] if agent is not None else []
|
|
441
|
-
|
|
442
|
-
# Call cancel() on each affected agent to persist an interrupt marker
|
|
443
|
-
for sid in session_ids:
|
|
444
|
-
agent = self._get_active_agent(sid)
|
|
445
|
-
if agent is not None:
|
|
446
|
-
for evt in agent.cancel():
|
|
447
|
-
await self.emit_event(evt)
|
|
448
|
-
|
|
449
|
-
# emit interrupt event
|
|
450
|
-
await self.emit_event(events.InterruptEvent(session_id=operation.target_session_id or "all"))
|
|
451
|
-
|
|
452
|
-
# Find tasks to cancel (filter by target sessions if provided)
|
|
453
|
-
if operation.target_session_id is None:
|
|
454
|
-
session_filter: set[str] | None = None
|
|
455
|
-
else:
|
|
456
|
-
session_filter = {operation.target_session_id}
|
|
457
|
-
|
|
458
|
-
tasks_to_cancel = self.task_manager.cancel_tasks_for_sessions(session_filter)
|
|
459
|
-
|
|
460
|
-
scope = operation.target_session_id or "all"
|
|
461
|
-
log_debug(
|
|
462
|
-
f"Interrupting {len(tasks_to_cancel)} task(s) for: {scope}",
|
|
463
|
-
style="yellow",
|
|
464
|
-
debug_type=DebugType.EXECUTION,
|
|
465
|
-
)
|
|
466
|
-
|
|
467
|
-
# Cancel the tasks
|
|
468
|
-
for task_id, task in tasks_to_cancel:
|
|
469
|
-
task.cancel()
|
|
470
|
-
# Remove from active tasks immediately
|
|
471
|
-
self.task_manager.remove(task_id)
|
|
581
|
+
await self._agent_runtime.interrupt(operation.target_session_id)
|
|
472
582
|
|
|
473
583
|
def get_active_task(self, submission_id: str) -> asyncio.Task[None] | None:
|
|
474
584
|
"""Return the asyncio.Task for a submission id if one is registered."""
|
|
@@ -483,16 +593,6 @@ class ExecutorContext:
|
|
|
483
593
|
|
|
484
594
|
return self.task_manager.get(submission_id) is not None
|
|
485
595
|
|
|
486
|
-
def _get_active_agent(self, session_id: str) -> Agent | None:
|
|
487
|
-
"""Return the active agent if its session id matches ``session_id``."""
|
|
488
|
-
|
|
489
|
-
agent = self._agent
|
|
490
|
-
if agent is None:
|
|
491
|
-
return None
|
|
492
|
-
if agent.session.id != session_id:
|
|
493
|
-
return None
|
|
494
|
-
return agent
|
|
495
|
-
|
|
496
596
|
|
|
497
597
|
class Executor:
|
|
498
598
|
"""
|
|
@@ -590,7 +690,11 @@ class Executor:
|
|
|
590
690
|
debug_type=DebugType.EXECUTION,
|
|
591
691
|
)
|
|
592
692
|
await self.context.emit_event(
|
|
593
|
-
events.ErrorEvent(
|
|
693
|
+
events.ErrorEvent(
|
|
694
|
+
error_message=f"Executor error: {e!s}",
|
|
695
|
+
can_retry=False,
|
|
696
|
+
session_id="__app__",
|
|
697
|
+
)
|
|
594
698
|
)
|
|
595
699
|
|
|
596
700
|
async def stop(self) -> None:
|
|
@@ -667,7 +771,16 @@ class Executor:
|
|
|
667
771
|
style="red",
|
|
668
772
|
debug_type=DebugType.EXECUTION,
|
|
669
773
|
)
|
|
670
|
-
|
|
774
|
+
session_id = getattr(submission.operation, "session_id", None) or getattr(
|
|
775
|
+
submission.operation, "target_session_id", None
|
|
776
|
+
)
|
|
777
|
+
await self.context.emit_event(
|
|
778
|
+
events.ErrorEvent(
|
|
779
|
+
error_message=f"Operation failed: {e!s}",
|
|
780
|
+
can_retry=False,
|
|
781
|
+
session_id=session_id or "__app__",
|
|
782
|
+
)
|
|
783
|
+
)
|
|
671
784
|
# Set completion event even on error to prevent wait_for_completion from hanging
|
|
672
785
|
event = self._completion_events.get(submission.id)
|
|
673
786
|
if event is not None:
|