klaude-code 1.2.12__py3-none-any.whl → 1.2.13__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/oauth.py +3 -3
- klaude_code/cli/main.py +5 -5
- klaude_code/cli/runtime.py +19 -27
- klaude_code/cli/session_cmd.py +6 -8
- klaude_code/command/__init__.py +6 -6
- klaude_code/command/export_cmd.py +3 -3
- klaude_code/command/registry.py +1 -1
- klaude_code/command/terminal_setup_cmd.py +2 -2
- klaude_code/command/thinking_cmd.py +8 -6
- klaude_code/config/__init__.py +1 -1
- klaude_code/config/list_model.py +1 -1
- klaude_code/core/agent.py +13 -61
- klaude_code/core/executor.py +11 -10
- klaude_code/core/manager/agent_manager.py +4 -4
- klaude_code/core/manager/llm_clients.py +10 -49
- klaude_code/core/manager/llm_clients_builder.py +8 -21
- klaude_code/core/manager/sub_agent_manager.py +3 -3
- klaude_code/core/prompt.py +2 -2
- klaude_code/core/reminders.py +1 -1
- klaude_code/core/task.py +2 -2
- klaude_code/core/tool/__init__.py +16 -25
- klaude_code/core/tool/file/_utils.py +1 -1
- klaude_code/core/tool/file/apply_patch.py +17 -25
- klaude_code/core/tool/file/apply_patch_tool.py +4 -7
- klaude_code/core/tool/file/edit_tool.py +4 -11
- klaude_code/core/tool/file/multi_edit_tool.py +2 -3
- klaude_code/core/tool/file/read_tool.py +3 -4
- klaude_code/core/tool/file/write_tool.py +2 -3
- klaude_code/core/tool/memory/memory_tool.py +2 -8
- klaude_code/core/tool/memory/skill_loader.py +3 -2
- klaude_code/core/tool/shell/command_safety.py +0 -1
- klaude_code/core/tool/tool_context.py +1 -3
- klaude_code/core/tool/tool_registry.py +2 -1
- klaude_code/core/tool/tool_runner.py +1 -1
- klaude_code/core/tool/truncation.py +2 -5
- klaude_code/core/turn.py +9 -3
- klaude_code/llm/anthropic/client.py +6 -2
- klaude_code/llm/client.py +1 -1
- klaude_code/llm/codex/client.py +2 -2
- klaude_code/llm/input_common.py +2 -2
- klaude_code/llm/openai_compatible/client.py +11 -8
- klaude_code/llm/openai_compatible/stream_processor.py +2 -1
- klaude_code/llm/openrouter/client.py +20 -8
- klaude_code/llm/openrouter/reasoning_handler.py +19 -132
- klaude_code/llm/registry.py +6 -5
- klaude_code/llm/responses/client.py +10 -5
- klaude_code/protocol/events.py +7 -0
- klaude_code/protocol/model.py +7 -1
- klaude_code/protocol/sub_agent.py +2 -1
- klaude_code/session/selector.py +2 -2
- klaude_code/session/session.py +2 -4
- klaude_code/trace/__init__.py +1 -1
- klaude_code/trace/log.py +1 -1
- klaude_code/ui/__init__.py +4 -9
- klaude_code/ui/core/stage_manager.py +7 -4
- klaude_code/ui/modes/repl/__init__.py +1 -1
- klaude_code/ui/modes/repl/completers.py +3 -4
- klaude_code/ui/modes/repl/display.py +3 -4
- klaude_code/ui/modes/repl/event_handler.py +63 -5
- klaude_code/ui/modes/repl/key_bindings.py +2 -3
- klaude_code/ui/modes/repl/renderer.py +2 -1
- klaude_code/ui/renderers/diffs.py +1 -4
- klaude_code/ui/rich/markdown.py +3 -3
- klaude_code/ui/rich/searchable_text.py +6 -6
- klaude_code/ui/rich/status.py +3 -4
- klaude_code/ui/rich/theme.py +1 -4
- klaude_code/ui/terminal/control.py +7 -16
- klaude_code/ui/terminal/notifier.py +2 -4
- klaude_code/ui/utils/common.py +1 -1
- klaude_code/ui/utils/debouncer.py +2 -2
- {klaude_code-1.2.12.dist-info → klaude_code-1.2.13.dist-info}/METADATA +1 -1
- {klaude_code-1.2.12.dist-info → klaude_code-1.2.13.dist-info}/RECORD +74 -74
- {klaude_code-1.2.12.dist-info → klaude_code-1.2.13.dist-info}/WHEEL +0 -0
- {klaude_code-1.2.12.dist-info → klaude_code-1.2.13.dist-info}/entry_points.txt +0 -0
klaude_code/auth/codex/oauth.py
CHANGED
|
@@ -78,13 +78,13 @@ class OAuthCallbackHandler(BaseHTTPRequestHandler):
|
|
|
78
78
|
self.end_headers()
|
|
79
79
|
|
|
80
80
|
if OAuthCallbackHandler.error:
|
|
81
|
-
html = """
|
|
81
|
+
html = f"""
|
|
82
82
|
<html><body style="font-family: sans-serif; text-align: center; padding: 50px;">
|
|
83
83
|
<h1>Authentication Failed</h1>
|
|
84
|
-
<p>Error: {}</p>
|
|
84
|
+
<p>Error: {OAuthCallbackHandler.error}</p>
|
|
85
85
|
<p>Please close this window and try again.</p>
|
|
86
86
|
</body></html>
|
|
87
|
-
"""
|
|
87
|
+
"""
|
|
88
88
|
else:
|
|
89
89
|
html = """
|
|
90
90
|
<html><body style="font-family: sans-serif; text-align: center; padding: 50px;">
|
klaude_code/cli/main.py
CHANGED
|
@@ -67,7 +67,7 @@ def login_command(
|
|
|
67
67
|
if state and not state.is_expired():
|
|
68
68
|
log(("You are already logged in to Codex.", "green"))
|
|
69
69
|
log(f" Account ID: {state.account_id[:8]}...")
|
|
70
|
-
expires_dt = datetime.datetime.fromtimestamp(state.expires_at, tz=datetime.
|
|
70
|
+
expires_dt = datetime.datetime.fromtimestamp(state.expires_at, tz=datetime.UTC)
|
|
71
71
|
log(f" Expires: {expires_dt.strftime('%Y-%m-%d %H:%M:%S UTC')}")
|
|
72
72
|
if not typer.confirm("Do you want to re-login?"):
|
|
73
73
|
return
|
|
@@ -80,11 +80,11 @@ def login_command(
|
|
|
80
80
|
state = oauth.login()
|
|
81
81
|
log(("Login successful!", "green"))
|
|
82
82
|
log(f" Account ID: {state.account_id[:8]}...")
|
|
83
|
-
expires_dt = datetime.datetime.fromtimestamp(state.expires_at, tz=datetime.
|
|
83
|
+
expires_dt = datetime.datetime.fromtimestamp(state.expires_at, tz=datetime.UTC)
|
|
84
84
|
log(f" Expires: {expires_dt.strftime('%Y-%m-%d %H:%M:%S UTC')}")
|
|
85
85
|
except Exception as e:
|
|
86
86
|
log((f"Login failed: {e}", "red"))
|
|
87
|
-
raise typer.Exit(1)
|
|
87
|
+
raise typer.Exit(1) from None
|
|
88
88
|
|
|
89
89
|
|
|
90
90
|
@app.command("logout")
|
|
@@ -173,11 +173,11 @@ def edit_config() -> None:
|
|
|
173
173
|
subprocess.run([editor, str(config_path)], check=True)
|
|
174
174
|
except subprocess.CalledProcessError as e:
|
|
175
175
|
log((f"Error: Failed to open editor: {e}", "red"))
|
|
176
|
-
raise typer.Exit(1)
|
|
176
|
+
raise typer.Exit(1) from None
|
|
177
177
|
except FileNotFoundError:
|
|
178
178
|
log((f"Error: Editor '{editor}' not found", "red"))
|
|
179
179
|
log("Please install a text editor or set your $EDITOR environment variable")
|
|
180
|
-
raise typer.Exit(1)
|
|
180
|
+
raise typer.Exit(1) from None
|
|
181
181
|
|
|
182
182
|
|
|
183
183
|
@app.command("exec")
|
klaude_code/cli/runtime.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import contextlib
|
|
2
3
|
import sys
|
|
3
4
|
from dataclasses import dataclass
|
|
4
5
|
from typing import Any, Protocol
|
|
@@ -169,32 +170,26 @@ async def cleanup_app_components(components: AppComponents) -> None:
|
|
|
169
170
|
await components.display_task
|
|
170
171
|
finally:
|
|
171
172
|
# Always attempt to clear Ghostty progress bar and restore cursor visibility
|
|
172
|
-
|
|
173
|
+
# Best-effort only; never fail cleanup due to OSC errors
|
|
174
|
+
with contextlib.suppress(Exception):
|
|
173
175
|
emit_osc94(OSC94States.HIDDEN)
|
|
174
|
-
except Exception:
|
|
175
|
-
# Best-effort only; never fail cleanup due to OSC errors
|
|
176
|
-
pass
|
|
177
176
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
177
|
+
# Ensure the terminal cursor is visible even if Rich's Status spinner
|
|
178
|
+
# did not get a chance to stop cleanly (e.g. on KeyboardInterrupt).
|
|
179
|
+
# If this fails the shell can still recover via `reset`/`stty sane`.
|
|
180
|
+
with contextlib.suppress(Exception):
|
|
181
181
|
stream = getattr(sys, "__stdout__", None) or sys.stdout
|
|
182
182
|
stream.write("\033[?25h")
|
|
183
183
|
stream.flush()
|
|
184
|
-
except Exception:
|
|
185
|
-
# If this fails the shell can still recover via `reset`/`stty sane`.
|
|
186
|
-
pass
|
|
187
184
|
|
|
188
185
|
|
|
189
186
|
async def _handle_keyboard_interrupt(executor: Executor) -> None:
|
|
190
187
|
"""Handle Ctrl+C by logging and sending a global interrupt."""
|
|
191
188
|
|
|
192
189
|
log("Bye!")
|
|
193
|
-
|
|
190
|
+
# Executor might already be stopping
|
|
191
|
+
with contextlib.suppress(Exception):
|
|
194
192
|
await executor.submit(op.InterruptOperation(target_session_id=None))
|
|
195
|
-
except Exception:
|
|
196
|
-
# Executor might already be stopping
|
|
197
|
-
pass
|
|
198
193
|
|
|
199
194
|
|
|
200
195
|
async def run_exec(init_config: AppInitConfig, input_content: str) -> None:
|
|
@@ -259,9 +254,12 @@ async def run_interactive(init_config: AppInitConfig, session_id: str | None = N
|
|
|
259
254
|
if isinstance(components.display, ui.REPLDisplay):
|
|
260
255
|
printer = components.display.renderer
|
|
261
256
|
# Check if it's a DebugEventDisplay wrapping a REPLDisplay
|
|
262
|
-
elif
|
|
263
|
-
|
|
264
|
-
|
|
257
|
+
elif (
|
|
258
|
+
isinstance(components.display, ui.DebugEventDisplay)
|
|
259
|
+
and components.display.wrapped_display
|
|
260
|
+
and isinstance(components.display.wrapped_display, ui.REPLDisplay)
|
|
261
|
+
):
|
|
262
|
+
printer = components.display.wrapped_display.renderer
|
|
265
263
|
|
|
266
264
|
if printer is not None:
|
|
267
265
|
printer.print(Text(f" {MSG} ", style="bold yellow reverse"))
|
|
@@ -272,10 +270,8 @@ async def run_interactive(init_config: AppInitConfig, session_id: str | None = N
|
|
|
272
270
|
print(MSG, file=sys.stderr)
|
|
273
271
|
|
|
274
272
|
def _hide_progress() -> None:
|
|
275
|
-
|
|
273
|
+
with contextlib.suppress(Exception):
|
|
276
274
|
emit_osc94(OSC94States.HIDDEN)
|
|
277
|
-
except Exception:
|
|
278
|
-
pass
|
|
279
275
|
|
|
280
276
|
restore_sigint = install_sigint_double_press_exit(_show_toast_once, _hide_progress)
|
|
281
277
|
|
|
@@ -315,17 +311,13 @@ async def run_interactive(init_config: AppInitConfig, session_id: str | None = N
|
|
|
315
311
|
finally:
|
|
316
312
|
# Stop ESC monitor and wait for it to finish cleaning up TTY
|
|
317
313
|
stop_event.set()
|
|
318
|
-
|
|
314
|
+
with contextlib.suppress(Exception):
|
|
319
315
|
await esc_task
|
|
320
|
-
except Exception:
|
|
321
|
-
pass
|
|
322
316
|
|
|
323
317
|
except KeyboardInterrupt:
|
|
324
318
|
await _handle_keyboard_interrupt(components.executor)
|
|
325
319
|
finally:
|
|
326
|
-
|
|
327
|
-
|
|
320
|
+
# Restore original SIGINT handler
|
|
321
|
+
with contextlib.suppress(Exception):
|
|
328
322
|
restore_sigint()
|
|
329
|
-
except Exception:
|
|
330
|
-
pass
|
|
331
323
|
await cleanup_app_components(components)
|
klaude_code/cli/session_cmd.py
CHANGED
|
@@ -46,10 +46,9 @@ def session_clean(
|
|
|
46
46
|
log(f"No sessions with fewer than {min_messages} messages found.")
|
|
47
47
|
return
|
|
48
48
|
|
|
49
|
-
if not yes:
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
return
|
|
49
|
+
if not yes and not _session_confirm(to_delete, "Delete these sessions?"):
|
|
50
|
+
log("Aborted.")
|
|
51
|
+
return
|
|
53
52
|
|
|
54
53
|
deleted = Session.clean_small_sessions(min_messages)
|
|
55
54
|
log(f"Deleted {deleted} session(s).")
|
|
@@ -65,10 +64,9 @@ def session_clean_all(
|
|
|
65
64
|
log("No sessions found.")
|
|
66
65
|
return
|
|
67
66
|
|
|
68
|
-
if not yes:
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
return
|
|
67
|
+
if not yes and not _session_confirm(sessions, "Delete ALL sessions? This cannot be undone."):
|
|
68
|
+
log("Aborted.")
|
|
69
|
+
return
|
|
72
70
|
|
|
73
71
|
deleted = Session.clean_all_sessions()
|
|
74
72
|
log(f"Deleted {deleted} session(s).")
|
klaude_code/command/__init__.py
CHANGED
|
@@ -28,15 +28,15 @@ def ensure_commands_loaded() -> None:
|
|
|
28
28
|
|
|
29
29
|
# Import and register commands in display order
|
|
30
30
|
from .clear_cmd import ClearCommand
|
|
31
|
-
from .model_cmd import ModelCommand
|
|
32
|
-
from .status_cmd import StatusCommand
|
|
33
31
|
from .diff_cmd import DiffCommand
|
|
34
32
|
from .export_cmd import ExportCommand
|
|
35
|
-
from .thinking_cmd import ThinkingCommand
|
|
36
33
|
from .help_cmd import HelpCommand
|
|
34
|
+
from .model_cmd import ModelCommand
|
|
37
35
|
from .refresh_cmd import RefreshTerminalCommand
|
|
38
|
-
from .terminal_setup_cmd import TerminalSetupCommand
|
|
39
36
|
from .release_notes_cmd import ReleaseNotesCommand
|
|
37
|
+
from .status_cmd import StatusCommand
|
|
38
|
+
from .terminal_setup_cmd import TerminalSetupCommand
|
|
39
|
+
from .thinking_cmd import ThinkingCommand
|
|
40
40
|
|
|
41
41
|
# Register in desired display order
|
|
42
42
|
register(ExportCommand())
|
|
@@ -86,8 +86,8 @@ __all__ = [
|
|
|
86
86
|
"InputAction",
|
|
87
87
|
"InputActionType",
|
|
88
88
|
"dispatch_command",
|
|
89
|
+
"ensure_commands_loaded",
|
|
89
90
|
"get_commands",
|
|
90
|
-
"is_slash_command_name",
|
|
91
91
|
"has_interactive_command",
|
|
92
|
-
"
|
|
92
|
+
"is_slash_command_name",
|
|
93
93
|
]
|
|
@@ -31,7 +31,7 @@ class ExportCommand(CommandABC):
|
|
|
31
31
|
def is_interactive(self) -> bool:
|
|
32
32
|
return False
|
|
33
33
|
|
|
34
|
-
async def run(self, raw: str, agent:
|
|
34
|
+
async def run(self, raw: str, agent: Agent) -> CommandResult:
|
|
35
35
|
try:
|
|
36
36
|
output_path = self._resolve_output_path(raw, agent)
|
|
37
37
|
html_doc = self._build_html(agent)
|
|
@@ -58,7 +58,7 @@ class ExportCommand(CommandABC):
|
|
|
58
58
|
)
|
|
59
59
|
return CommandResult(events=[event])
|
|
60
60
|
|
|
61
|
-
def _resolve_output_path(self, raw: str, agent:
|
|
61
|
+
def _resolve_output_path(self, raw: str, agent: Agent) -> Path:
|
|
62
62
|
trimmed = raw.strip()
|
|
63
63
|
if trimmed:
|
|
64
64
|
candidate = Path(trimmed).expanduser()
|
|
@@ -79,7 +79,7 @@ class ExportCommand(CommandABC):
|
|
|
79
79
|
msg = f"Failed to open HTML with `open`: {exc}"
|
|
80
80
|
raise RuntimeError(msg) from exc
|
|
81
81
|
|
|
82
|
-
def _build_html(self, agent:
|
|
82
|
+
def _build_html(self, agent: Agent) -> str:
|
|
83
83
|
profile = agent.profile
|
|
84
84
|
system_prompt = (profile.system_prompt if profile else "") or ""
|
|
85
85
|
tools = profile.tools if profile else []
|
klaude_code/command/registry.py
CHANGED
|
@@ -94,7 +94,7 @@ async def dispatch_command(raw: str, agent: "Agent") -> CommandResult:
|
|
|
94
94
|
events.DeveloperMessageEvent(
|
|
95
95
|
session_id=agent.session.id,
|
|
96
96
|
item=model.DeveloperMessageItem(
|
|
97
|
-
content=f"Command {command_identifier} error: [{e.__class__.__name__}] {
|
|
97
|
+
content=f"Command {command_identifier} error: [{e.__class__.__name__}] {e!s}",
|
|
98
98
|
command_output=command_output,
|
|
99
99
|
),
|
|
100
100
|
)
|
|
@@ -43,7 +43,7 @@ class TerminalSetupCommand(CommandABC):
|
|
|
43
43
|
return self._create_success_result(agent, message)
|
|
44
44
|
|
|
45
45
|
except Exception as e:
|
|
46
|
-
return self._create_error_result(agent, f"Error configuring terminal: {
|
|
46
|
+
return self._create_error_result(agent, f"Error configuring terminal: {e!s}")
|
|
47
47
|
|
|
48
48
|
def _setup_ghostty(self) -> str:
|
|
49
49
|
"""Configure shift+enter newline for Ghostty terminal"""
|
|
@@ -113,7 +113,7 @@ class TerminalSetupCommand(CommandABC):
|
|
|
113
113
|
)
|
|
114
114
|
|
|
115
115
|
except Exception as e:
|
|
116
|
-
raise Exception(f"Error configuring iTerm: {
|
|
116
|
+
raise Exception(f"Error configuring iTerm: {e!s}") from e
|
|
117
117
|
|
|
118
118
|
def _setup_vscode_family(self) -> str:
|
|
119
119
|
"""Configure shift+enter newline for VS Code family terminals (VS Code, Windsurf, Cursor).
|
|
@@ -95,12 +95,14 @@ def _format_current_thinking(config: llm_param.LLMConfigParameter) -> str:
|
|
|
95
95
|
return "unknown protocol"
|
|
96
96
|
|
|
97
97
|
|
|
98
|
-
SELECT_STYLE = questionary.Style(
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
98
|
+
SELECT_STYLE = questionary.Style(
|
|
99
|
+
[
|
|
100
|
+
("instruction", "ansibrightblack"),
|
|
101
|
+
("pointer", "ansicyan"),
|
|
102
|
+
("highlighted", "ansicyan"),
|
|
103
|
+
("text", "ansibrightblack"),
|
|
104
|
+
]
|
|
105
|
+
)
|
|
104
106
|
|
|
105
107
|
|
|
106
108
|
def _select_responses_thinking_sync(model_name: str | None) -> llm_param.Thinking | None:
|
klaude_code/config/__init__.py
CHANGED
klaude_code/config/list_model.py
CHANGED
|
@@ -34,7 +34,7 @@ def _display_codex_status(console: Console) -> None:
|
|
|
34
34
|
)
|
|
35
35
|
)
|
|
36
36
|
else:
|
|
37
|
-
expires_dt = datetime.datetime.fromtimestamp(state.expires_at, tz=datetime.
|
|
37
|
+
expires_dt = datetime.datetime.fromtimestamp(state.expires_at, tz=datetime.UTC)
|
|
38
38
|
console.print(
|
|
39
39
|
Text.assemble(
|
|
40
40
|
("Codex Status: ", "bold"),
|
klaude_code/core/agent.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from collections.abc import AsyncGenerator,
|
|
3
|
+
from collections.abc import AsyncGenerator, Iterable
|
|
4
4
|
from dataclasses import dataclass
|
|
5
|
-
from typing import
|
|
5
|
+
from typing import Protocol
|
|
6
6
|
|
|
7
7
|
from klaude_code.core.prompt import load_system_prompt
|
|
8
8
|
from klaude_code.core.reminders import Reminder, load_agent_reminders
|
|
@@ -14,38 +14,21 @@ from klaude_code.protocol.model import UserInputPayload
|
|
|
14
14
|
from klaude_code.session import Session
|
|
15
15
|
from klaude_code.trace import DebugType, log_debug
|
|
16
16
|
|
|
17
|
-
if TYPE_CHECKING:
|
|
18
|
-
from klaude_code.core.manager.llm_clients import LLMClients
|
|
19
|
-
|
|
20
17
|
|
|
21
18
|
@dataclass(frozen=True)
|
|
22
19
|
class AgentProfile:
|
|
23
20
|
"""Encapsulates the active LLM client plus prompts/tools/reminders."""
|
|
24
21
|
|
|
25
|
-
|
|
22
|
+
llm_client: LLMClientABC
|
|
26
23
|
system_prompt: str | None
|
|
27
24
|
tools: list[llm_param.ToolSchema]
|
|
28
25
|
reminders: list[Reminder]
|
|
29
26
|
|
|
30
|
-
_llm_client: LLMClientABC | None = None
|
|
31
|
-
|
|
32
|
-
@property
|
|
33
|
-
def llm_client(self) -> LLMClientABC:
|
|
34
|
-
if self._llm_client is None:
|
|
35
|
-
object.__setattr__(self, "_llm_client", self.llm_client_factory())
|
|
36
|
-
return self._llm_client # type: ignore[return-value]
|
|
37
|
-
|
|
38
27
|
|
|
39
28
|
class ModelProfileProvider(Protocol):
|
|
40
29
|
"""Strategy interface for constructing agent profiles."""
|
|
41
30
|
|
|
42
31
|
def build_profile(
|
|
43
|
-
self,
|
|
44
|
-
llm_clients: LLMClients,
|
|
45
|
-
sub_agent_type: tools.SubAgentType | None = None,
|
|
46
|
-
) -> AgentProfile: ...
|
|
47
|
-
|
|
48
|
-
def build_profile_eager(
|
|
49
32
|
self,
|
|
50
33
|
llm_client: LLMClientABC,
|
|
51
34
|
sub_agent_type: tools.SubAgentType | None = None,
|
|
@@ -56,26 +39,13 @@ class DefaultModelProfileProvider(ModelProfileProvider):
|
|
|
56
39
|
"""Default provider backed by global prompts/tool/reminder registries."""
|
|
57
40
|
|
|
58
41
|
def build_profile(
|
|
59
|
-
self,
|
|
60
|
-
llm_clients: LLMClients,
|
|
61
|
-
sub_agent_type: tools.SubAgentType | None = None,
|
|
62
|
-
) -> AgentProfile:
|
|
63
|
-
model_name = llm_clients.main_model_name
|
|
64
|
-
return AgentProfile(
|
|
65
|
-
llm_client_factory=lambda: llm_clients.main,
|
|
66
|
-
system_prompt=load_system_prompt(model_name, sub_agent_type),
|
|
67
|
-
tools=load_agent_tools(model_name, sub_agent_type),
|
|
68
|
-
reminders=load_agent_reminders(model_name, sub_agent_type),
|
|
69
|
-
)
|
|
70
|
-
|
|
71
|
-
def build_profile_eager(
|
|
72
42
|
self,
|
|
73
43
|
llm_client: LLMClientABC,
|
|
74
44
|
sub_agent_type: tools.SubAgentType | None = None,
|
|
75
45
|
) -> AgentProfile:
|
|
76
46
|
model_name = llm_client.model_name
|
|
77
47
|
return AgentProfile(
|
|
78
|
-
|
|
48
|
+
llm_client=llm_client,
|
|
79
49
|
system_prompt=load_system_prompt(model_name, sub_agent_type),
|
|
80
50
|
tools=load_agent_tools(model_name, sub_agent_type),
|
|
81
51
|
reminders=load_agent_reminders(model_name, sub_agent_type),
|
|
@@ -86,26 +56,13 @@ class VanillaModelProfileProvider(ModelProfileProvider):
|
|
|
86
56
|
"""Provider that strips prompts, reminders, and tools for vanilla mode."""
|
|
87
57
|
|
|
88
58
|
def build_profile(
|
|
89
|
-
self,
|
|
90
|
-
llm_clients: LLMClients,
|
|
91
|
-
sub_agent_type: tools.SubAgentType | None = None,
|
|
92
|
-
) -> AgentProfile:
|
|
93
|
-
model_name = llm_clients.main_model_name
|
|
94
|
-
return AgentProfile(
|
|
95
|
-
llm_client_factory=lambda: llm_clients.main,
|
|
96
|
-
system_prompt=None,
|
|
97
|
-
tools=load_agent_tools(model_name, vanilla=True),
|
|
98
|
-
reminders=load_agent_reminders(model_name, vanilla=True),
|
|
99
|
-
)
|
|
100
|
-
|
|
101
|
-
def build_profile_eager(
|
|
102
59
|
self,
|
|
103
60
|
llm_client: LLMClientABC,
|
|
104
61
|
sub_agent_type: tools.SubAgentType | None = None,
|
|
105
62
|
) -> AgentProfile:
|
|
106
63
|
model_name = llm_client.model_name
|
|
107
64
|
return AgentProfile(
|
|
108
|
-
|
|
65
|
+
llm_client=llm_client,
|
|
109
66
|
system_prompt=None,
|
|
110
67
|
tools=load_agent_tools(model_name, vanilla=True),
|
|
111
68
|
reminders=load_agent_reminders(model_name, vanilla=True),
|
|
@@ -117,13 +74,12 @@ class Agent:
|
|
|
117
74
|
self,
|
|
118
75
|
session: Session,
|
|
119
76
|
profile: AgentProfile,
|
|
120
|
-
model_name: str | None = None,
|
|
121
77
|
):
|
|
122
78
|
self.session: Session = session
|
|
123
79
|
self.profile: AgentProfile = profile
|
|
124
80
|
self._current_task: TaskExecutor | None = None
|
|
125
|
-
if not self.session.model_name
|
|
126
|
-
self.session.model_name = model_name
|
|
81
|
+
if not self.session.model_name:
|
|
82
|
+
self.session.model_name = profile.llm_client.model_name
|
|
127
83
|
|
|
128
84
|
def cancel(self) -> Iterable[events.Event]:
|
|
129
85
|
"""Handle agent cancellation and persist an interrupt marker and tool cancellations.
|
|
@@ -136,8 +92,7 @@ class Agent:
|
|
|
136
92
|
"""
|
|
137
93
|
# First, cancel any running task so it stops emitting events.
|
|
138
94
|
if self._current_task is not None:
|
|
139
|
-
|
|
140
|
-
yield ui_event
|
|
95
|
+
yield from self._current_task.cancel()
|
|
141
96
|
self._current_task = None
|
|
142
97
|
|
|
143
98
|
# Record an interrupt marker in the session history
|
|
@@ -148,7 +103,7 @@ class Agent:
|
|
|
148
103
|
debug_type=DebugType.EXECUTION,
|
|
149
104
|
)
|
|
150
105
|
|
|
151
|
-
async def run_task(self, user_input: UserInputPayload) -> AsyncGenerator[events.Event
|
|
106
|
+
async def run_task(self, user_input: UserInputPayload) -> AsyncGenerator[events.Event]:
|
|
152
107
|
session_ctx = SessionContext(
|
|
153
108
|
session_id=self.session.id,
|
|
154
109
|
get_conversation_history=lambda: self.session.conversation_history,
|
|
@@ -173,7 +128,7 @@ class Agent:
|
|
|
173
128
|
finally:
|
|
174
129
|
self._current_task = None
|
|
175
130
|
|
|
176
|
-
async def replay_history(self) -> AsyncGenerator[events.Event
|
|
131
|
+
async def replay_history(self) -> AsyncGenerator[events.Event]:
|
|
177
132
|
"""Yield UI events reconstructed from saved conversation history."""
|
|
178
133
|
|
|
179
134
|
if len(self.session.conversation_history) == 0:
|
|
@@ -185,21 +140,18 @@ class Agent:
|
|
|
185
140
|
session_id=self.session.id,
|
|
186
141
|
)
|
|
187
142
|
|
|
188
|
-
async def _process_reminder(self, reminder: Reminder) -> AsyncGenerator[events.DeveloperMessageEvent
|
|
143
|
+
async def _process_reminder(self, reminder: Reminder) -> AsyncGenerator[events.DeveloperMessageEvent]:
|
|
189
144
|
"""Process a single reminder and yield events if it produces output."""
|
|
190
145
|
item = await reminder(self.session)
|
|
191
146
|
if item is not None:
|
|
192
147
|
self.session.append_history([item])
|
|
193
148
|
yield events.DeveloperMessageEvent(session_id=self.session.id, item=item)
|
|
194
149
|
|
|
195
|
-
def set_model_profile(self, profile: AgentProfile
|
|
150
|
+
def set_model_profile(self, profile: AgentProfile) -> None:
|
|
196
151
|
"""Apply a fully constructed profile to the agent."""
|
|
197
152
|
|
|
198
153
|
self.profile = profile
|
|
199
|
-
|
|
200
|
-
self.session.model_name = model_name
|
|
201
|
-
elif not self.session.model_name:
|
|
202
|
-
self.session.model_name = profile.llm_client.model_name
|
|
154
|
+
self.session.model_name = profile.llm_client.model_name
|
|
203
155
|
|
|
204
156
|
def get_llm_client(self) -> LLMClientABC:
|
|
205
157
|
return self.profile.llm_client
|
klaude_code/core/executor.py
CHANGED
|
@@ -264,14 +264,14 @@ class ExecutorContext:
|
|
|
264
264
|
import traceback
|
|
265
265
|
|
|
266
266
|
log_debug(
|
|
267
|
-
f"Agent task {task_id} failed: {
|
|
267
|
+
f"Agent task {task_id} failed: {e!s}",
|
|
268
268
|
style="red",
|
|
269
269
|
debug_type=DebugType.EXECUTION,
|
|
270
270
|
)
|
|
271
271
|
log_debug(traceback.format_exc(), style="red", debug_type=DebugType.EXECUTION)
|
|
272
272
|
await self.emit_event(
|
|
273
273
|
events.ErrorEvent(
|
|
274
|
-
error_message=f"Agent task failed: [{e.__class__.__name__}] {
|
|
274
|
+
error_message=f"Agent task failed: [{e.__class__.__name__}] {e!s}",
|
|
275
275
|
can_retry=False,
|
|
276
276
|
)
|
|
277
277
|
)
|
|
@@ -317,6 +317,7 @@ class Executor:
|
|
|
317
317
|
self.submission_queue: asyncio.Queue[op.Submission] = asyncio.Queue()
|
|
318
318
|
# Track completion events for all submissions (not just those with ActiveTask)
|
|
319
319
|
self._completion_events: dict[str, asyncio.Event] = {}
|
|
320
|
+
self._background_tasks: set[asyncio.Task[None]] = set()
|
|
320
321
|
|
|
321
322
|
async def submit(self, operation: op.Operation) -> str:
|
|
322
323
|
"""
|
|
@@ -388,12 +389,12 @@ class Executor:
|
|
|
388
389
|
except Exception as e:
|
|
389
390
|
# Handle unexpected errors
|
|
390
391
|
log_debug(
|
|
391
|
-
f"Executor error: {
|
|
392
|
+
f"Executor error: {e!s}",
|
|
392
393
|
style="red",
|
|
393
394
|
debug_type=DebugType.EXECUTION,
|
|
394
395
|
)
|
|
395
396
|
await self.context.emit_event(
|
|
396
|
-
events.ErrorEvent(error_message=f"Executor error: {
|
|
397
|
+
events.ErrorEvent(error_message=f"Executor error: {e!s}", can_retry=False)
|
|
397
398
|
)
|
|
398
399
|
|
|
399
400
|
async def stop(self) -> None:
|
|
@@ -420,7 +421,7 @@ class Executor:
|
|
|
420
421
|
await self.submission_queue.put(submission)
|
|
421
422
|
except Exception as e:
|
|
422
423
|
log_debug(
|
|
423
|
-
f"Failed to send EndOperation: {
|
|
424
|
+
f"Failed to send EndOperation: {e!s}",
|
|
424
425
|
style="red",
|
|
425
426
|
debug_type=DebugType.EXECUTION,
|
|
426
427
|
)
|
|
@@ -460,17 +461,17 @@ class Executor:
|
|
|
460
461
|
event.set()
|
|
461
462
|
else:
|
|
462
463
|
# Run in background so the submission loop can continue (e.g., to handle interrupts)
|
|
463
|
-
asyncio.create_task(_await_agent_and_complete(task))
|
|
464
|
+
background_task = asyncio.create_task(_await_agent_and_complete(task))
|
|
465
|
+
self._background_tasks.add(background_task)
|
|
466
|
+
background_task.add_done_callback(self._background_tasks.discard)
|
|
464
467
|
|
|
465
468
|
except Exception as e:
|
|
466
469
|
log_debug(
|
|
467
|
-
f"Failed to handle submission {submission.id}: {
|
|
470
|
+
f"Failed to handle submission {submission.id}: {e!s}",
|
|
468
471
|
style="red",
|
|
469
472
|
debug_type=DebugType.EXECUTION,
|
|
470
473
|
)
|
|
471
|
-
await self.context.emit_event(
|
|
472
|
-
events.ErrorEvent(error_message=f"Operation failed: {str(e)}", can_retry=False)
|
|
473
|
-
)
|
|
474
|
+
await self.context.emit_event(events.ErrorEvent(error_message=f"Operation failed: {e!s}", can_retry=False))
|
|
474
475
|
# Set completion event even on error to prevent wait_for_completion from hanging
|
|
475
476
|
event = self._completion_events.get(submission.id)
|
|
476
477
|
if event is not None:
|
|
@@ -51,8 +51,8 @@ class AgentManager:
|
|
|
51
51
|
if agent is not None:
|
|
52
52
|
return agent
|
|
53
53
|
session = Session.load(session_id)
|
|
54
|
-
profile = self._model_profile_provider.build_profile(self._llm_clients)
|
|
55
|
-
agent = Agent(session=session, profile=profile
|
|
54
|
+
profile = self._model_profile_provider.build_profile(self._llm_clients.main)
|
|
55
|
+
agent = Agent(session=session, profile=profile)
|
|
56
56
|
|
|
57
57
|
async for evt in agent.replay_history():
|
|
58
58
|
await self.emit_event(evt)
|
|
@@ -60,7 +60,7 @@ class AgentManager:
|
|
|
60
60
|
await self.emit_event(
|
|
61
61
|
events.WelcomeEvent(
|
|
62
62
|
work_dir=str(session.work_dir),
|
|
63
|
-
llm_config=self._llm_clients.get_llm_config(),
|
|
63
|
+
llm_config=self._llm_clients.main.get_llm_config(),
|
|
64
64
|
)
|
|
65
65
|
)
|
|
66
66
|
|
|
@@ -81,7 +81,7 @@ class AgentManager:
|
|
|
81
81
|
|
|
82
82
|
llm_config = config.get_model_config(model_name)
|
|
83
83
|
llm_client = create_llm_client(llm_config)
|
|
84
|
-
agent.set_model_profile(self._model_profile_provider.
|
|
84
|
+
agent.set_model_profile(self._model_profile_provider.build_profile(llm_client))
|
|
85
85
|
|
|
86
86
|
developer_item = model.DeveloperMessageItem(
|
|
87
87
|
content=f"switched to model: {model_name}",
|
|
@@ -2,66 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from dataclasses import field as dataclass_field
|
|
6
7
|
|
|
7
8
|
from klaude_code.llm.client import LLMClientABC
|
|
8
|
-
from klaude_code.protocol import llm_param
|
|
9
9
|
from klaude_code.protocol.tools import SubAgentType
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def __init__(
|
|
16
|
-
self,
|
|
17
|
-
main_factory: Callable[[], LLMClientABC],
|
|
18
|
-
main_model_name: str,
|
|
19
|
-
main_llm_config: llm_param.LLMConfigParameter,
|
|
20
|
-
) -> None:
|
|
21
|
-
self._main_factory: Callable[[], LLMClientABC] | None = main_factory
|
|
22
|
-
self._main_client: LLMClientABC | None = None
|
|
23
|
-
self._main_model_name: str = main_model_name
|
|
24
|
-
self._main_llm_config: llm_param.LLMConfigParameter = main_llm_config
|
|
25
|
-
self._sub_clients: dict[SubAgentType, LLMClientABC] = {}
|
|
26
|
-
self._sub_factories: dict[SubAgentType, Callable[[], LLMClientABC]] = {}
|
|
12
|
+
def _default_sub_clients() -> dict[SubAgentType, LLMClientABC]:
|
|
13
|
+
return {}
|
|
27
14
|
|
|
28
|
-
@property
|
|
29
|
-
def main_model_name(self) -> str:
|
|
30
|
-
return self._main_model_name
|
|
31
15
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
@property
|
|
36
|
-
def main(self) -> LLMClientABC:
|
|
37
|
-
if self._main_client is None:
|
|
38
|
-
if self._main_factory is None:
|
|
39
|
-
raise RuntimeError("Main client factory not set")
|
|
40
|
-
self._main_client = self._main_factory()
|
|
41
|
-
self._main_factory = None
|
|
42
|
-
return self._main_client
|
|
16
|
+
@dataclass
|
|
17
|
+
class LLMClients:
|
|
18
|
+
"""Container for LLM clients used by main agent and sub-agents."""
|
|
43
19
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
sub_agent_type: SubAgentType,
|
|
47
|
-
factory: Callable[[], LLMClientABC],
|
|
48
|
-
) -> None:
|
|
49
|
-
self._sub_factories[sub_agent_type] = factory
|
|
20
|
+
main: LLMClientABC
|
|
21
|
+
sub_clients: dict[SubAgentType, LLMClientABC] = dataclass_field(default_factory=_default_sub_clients)
|
|
50
22
|
|
|
51
23
|
def get_client(self, sub_agent_type: SubAgentType | None = None) -> LLMClientABC:
|
|
52
24
|
"""Return client for a sub-agent type or the main client."""
|
|
53
25
|
|
|
54
26
|
if sub_agent_type is None:
|
|
55
27
|
return self.main
|
|
56
|
-
|
|
57
|
-
existing = self._sub_clients.get(sub_agent_type)
|
|
58
|
-
if existing is not None:
|
|
59
|
-
return existing
|
|
60
|
-
|
|
61
|
-
factory = self._sub_factories.get(sub_agent_type)
|
|
62
|
-
if factory is None:
|
|
63
|
-
return self.main
|
|
64
|
-
|
|
65
|
-
client = factory()
|
|
66
|
-
self._sub_clients[sub_agent_type] = client
|
|
67
|
-
return client
|
|
28
|
+
return self.sub_clients.get(sub_agent_type) or self.main
|