klaude-code 2.9.1__py3-none-any.whl → 2.10.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. klaude_code/app/runtime.py +5 -1
  2. klaude_code/cli/cost_cmd.py +4 -4
  3. klaude_code/cli/list_model.py +1 -2
  4. klaude_code/cli/main.py +10 -0
  5. klaude_code/config/assets/builtin_config.yaml +15 -14
  6. klaude_code/const.py +4 -3
  7. klaude_code/core/agent_profile.py +23 -0
  8. klaude_code/core/bash_mode.py +276 -0
  9. klaude_code/core/executor.py +40 -7
  10. klaude_code/core/manager/llm_clients.py +1 -0
  11. klaude_code/core/manager/llm_clients_builder.py +2 -2
  12. klaude_code/core/memory.py +140 -0
  13. klaude_code/core/prompts/prompt-sub-agent-web.md +2 -2
  14. klaude_code/core/reminders.py +17 -89
  15. klaude_code/core/tool/offload.py +4 -4
  16. klaude_code/core/tool/web/web_fetch_tool.md +2 -1
  17. klaude_code/core/tool/web/web_fetch_tool.py +1 -1
  18. klaude_code/core/turn.py +9 -4
  19. klaude_code/protocol/events.py +17 -0
  20. klaude_code/protocol/op.py +12 -0
  21. klaude_code/protocol/op_handler.py +5 -0
  22. klaude_code/session/templates/mermaid_viewer.html +85 -0
  23. klaude_code/tui/command/resume_cmd.py +1 -1
  24. klaude_code/tui/commands.py +15 -0
  25. klaude_code/tui/components/command_output.py +4 -5
  26. klaude_code/tui/components/developer.py +1 -3
  27. klaude_code/tui/components/metadata.py +28 -25
  28. klaude_code/tui/components/rich/code_panel.py +31 -16
  29. klaude_code/tui/components/rich/markdown.py +56 -124
  30. klaude_code/tui/components/rich/theme.py +22 -12
  31. klaude_code/tui/components/thinking.py +0 -35
  32. klaude_code/tui/components/tools.py +4 -2
  33. klaude_code/tui/components/user_input.py +49 -59
  34. klaude_code/tui/components/welcome.py +47 -2
  35. klaude_code/tui/display.py +14 -6
  36. klaude_code/tui/input/completers.py +8 -0
  37. klaude_code/tui/input/key_bindings.py +37 -1
  38. klaude_code/tui/input/prompt_toolkit.py +57 -31
  39. klaude_code/tui/machine.py +108 -28
  40. klaude_code/tui/renderer.py +117 -19
  41. klaude_code/tui/runner.py +22 -0
  42. klaude_code/tui/terminal/notifier.py +11 -12
  43. klaude_code/tui/terminal/selector.py +1 -1
  44. klaude_code/ui/terminal/title.py +4 -2
  45. {klaude_code-2.9.1.dist-info → klaude_code-2.10.1.dist-info}/METADATA +1 -1
  46. {klaude_code-2.9.1.dist-info → klaude_code-2.10.1.dist-info}/RECORD +48 -47
  47. klaude_code/tui/components/assistant.py +0 -2
  48. {klaude_code-2.9.1.dist-info → klaude_code-2.10.1.dist-info}/WHEEL +0 -0
  49. {klaude_code-2.9.1.dist-info → klaude_code-2.10.1.dist-info}/entry_points.txt +0 -0
@@ -12,6 +12,7 @@ from klaude_code.core.agent import Agent
12
12
  from klaude_code.core.agent_profile import (
13
13
  DefaultModelProfileProvider,
14
14
  VanillaModelProfileProvider,
15
+ WebModelProfileProvider,
15
16
  )
16
17
  from klaude_code.core.executor import Executor
17
18
  from klaude_code.core.manager import build_llm_clients
@@ -27,6 +28,7 @@ class AppInitConfig:
27
28
  model: str | None
28
29
  debug: bool
29
30
  vanilla: bool
31
+ web: bool = False
30
32
  debug_filters: set[DebugType] | None = None
31
33
 
32
34
 
@@ -74,6 +76,8 @@ async def initialize_app_components(
74
76
 
75
77
  if init_config.vanilla:
76
78
  model_profile_provider = VanillaModelProfileProvider()
79
+ elif init_config.web:
80
+ model_profile_provider = WebModelProfileProvider(config=config)
77
81
  else:
78
82
  model_profile_provider = DefaultModelProfileProvider(config=config)
79
83
 
@@ -87,7 +91,7 @@ async def initialize_app_components(
87
91
  )
88
92
 
89
93
  if on_model_change is not None:
90
- on_model_change(llm_clients.main.model_name)
94
+ on_model_change(llm_clients.main_model_alias)
91
95
 
92
96
  executor_task = asyncio.create_task(executor.start())
93
97
 
@@ -343,7 +343,7 @@ def render_cost_table(daily_stats: dict[str, DailyStats]) -> Table:
343
343
  sub_list = list(group.sub_providers.values())
344
344
  for sub_idx, sub_group in enumerate(sub_list):
345
345
  is_last_sub = sub_idx == len(sub_list) - 1
346
- sub_prefix = " └─ " if is_last_sub else " ├─ "
346
+ sub_prefix = " ╰─ " if is_last_sub else " ├─ "
347
347
 
348
348
  # Sub-provider row
349
349
  add_stats_row(sub_group.total, prefix=sub_prefix, bold=True)
@@ -353,15 +353,15 @@ def render_cost_table(daily_stats: dict[str, DailyStats]) -> Table:
353
353
  is_last_model = model_idx == len(sub_group.models) - 1
354
354
  # Indent based on whether sub-provider is last
355
355
  if is_last_sub:
356
- model_prefix = " └─ " if is_last_model else " ├─ "
356
+ model_prefix = " ╰─ " if is_last_model else " ├─ "
357
357
  else:
358
- model_prefix = " │ └─ " if is_last_model else " │ ├─ "
358
+ model_prefix = " │ ╰─ " if is_last_model else " │ ├─ "
359
359
  add_stats_row(stats, prefix=model_prefix)
360
360
  else:
361
361
  # No sub-providers: render two-level tree (direct models)
362
362
  for model_idx, stats in enumerate(group.models):
363
363
  is_last_model = model_idx == len(group.models) - 1
364
- model_prefix = " └─ " if is_last_model else " ├─ "
364
+ model_prefix = " ╰─ " if is_last_model else " ├─ "
365
365
  add_stats_row(stats, prefix=model_prefix)
366
366
 
367
367
  if show_subtotal:
@@ -338,7 +338,7 @@ def _build_models_table(
338
338
  model_count = len(provider.model_list)
339
339
  for i, model in enumerate(provider.model_list):
340
340
  is_last = i == model_count - 1
341
- prefix = " └─ " if is_last else " ├─ "
341
+ prefix = " ╰─ " if is_last else " ├─ "
342
342
 
343
343
  if provider_disabled:
344
344
  name = Text.assemble(
@@ -439,7 +439,6 @@ def display_models_and_providers(config: Config, *, show_all: bool = False):
439
439
  # Provider info panel
440
440
  provider_panel = _build_provider_info_panel(provider, provider_available, disabled=provider.disabled)
441
441
  console.print(provider_panel)
442
- console.print()
443
442
 
444
443
  # Models table for this provider
445
444
  models_table = _build_models_table(provider, config)
klaude_code/cli/main.py CHANGED
@@ -200,6 +200,11 @@ def main_callback(
200
200
  help="Image generation mode (alias for --model banana)",
201
201
  rich_help_panel="LLM",
202
202
  ),
203
+ web: bool = typer.Option(
204
+ False,
205
+ "--web",
206
+ help="Enable web tools (WebFetch, WebSearch) for the main agent",
207
+ ),
203
208
  version: bool = typer.Option(
204
209
  False,
205
210
  "--version",
@@ -218,6 +223,10 @@ def main_callback(
218
223
  log(("Error: --banana cannot be combined with --vanilla", "red"))
219
224
  raise typer.Exit(2)
220
225
 
226
+ if vanilla and web:
227
+ log(("Error: --web cannot be combined with --vanilla", "red"))
228
+ raise typer.Exit(2)
229
+
221
230
  resume_by_id_value = resume_by_id.strip() if resume_by_id is not None else None
222
231
  if resume_by_id_value == "":
223
232
  log(("Error: --resume <id> cannot be empty", "red"))
@@ -347,6 +356,7 @@ def main_callback(
347
356
  model=chosen_model,
348
357
  debug=debug_enabled,
349
358
  vanilla=vanilla,
359
+ web=web,
350
360
  debug_filters=debug_filters,
351
361
  )
352
362
 
@@ -57,22 +57,14 @@ provider_list:
57
57
  reasoning_summary: concise
58
58
  cost: {input: 1.75, output: 14, cache_read: 0.17}
59
59
 
60
- - model_name: gpt-5.2-fast
61
- model_id: gpt-5.2
62
- context_limit: 400000
63
- verbosity: low
60
+ - model_name: gpt-5.2-codex
61
+ model_id: gpt-5.2-codex
64
62
  thinking:
65
- reasoning_effort: none
66
- cost: {input: 1.75, output: 14, cache_read: 0.17}
67
-
68
- - model_name: gpt-5.1-codex-max
69
- model_id: gpt-5.1-codex-max
70
- max_tokens: 128000
63
+ reasoning_effort: high
64
+ reasoning_summary: auto
71
65
  context_limit: 400000
72
- thinking:
73
- reasoning_effort: medium
74
- reasoning_summary: concise
75
- cost: {input: 1.25, output: 10, cache_read: 0.13}
66
+ max_tokens: 128000
67
+ cost: {input: 1.75, output: 14, cache_read: 0.17}
76
68
 
77
69
 
78
70
  - provider_name: openrouter
@@ -80,6 +72,15 @@ provider_list:
80
72
  api_key: ${OPENROUTER_API_KEY}
81
73
  model_list:
82
74
 
75
+ - model_name: gpt-5.2-codex
76
+ model_id: gpt-5.2-codex
77
+ thinking:
78
+ reasoning_effort: high
79
+ reasoning_summary: auto
80
+ context_limit: 400000
81
+ max_tokens: 128000
82
+ cost: {input: 1.75, output: 14, cache_read: 0.17}
83
+
83
84
  - model_name: gpt-5.2-high
84
85
  model_id: openai/gpt-5.2
85
86
  max_tokens: 128000
klaude_code/const.py CHANGED
@@ -71,7 +71,6 @@ DEFAULT_ANTHROPIC_THINKING_BUDGET_TOKENS = 2048 # Default thinking budget token
71
71
 
72
72
  TODO_REMINDER_TOOL_CALL_THRESHOLD = 10 # Tool call count threshold for todo reminder
73
73
  REMINDER_COOLDOWN_TURNS = 3 # Cooldown turns between reminder triggers
74
- MEMORY_FILE_NAMES = ["CLAUDE.md", "AGENTS.md", "AGENT.md"] # Memory file names to search for
75
74
 
76
75
 
77
76
  # =============================================================================
@@ -92,6 +91,7 @@ BINARY_CHECK_SIZE = 8192 # Bytes to check for binary file detection
92
91
 
93
92
  BASH_DEFAULT_TIMEOUT_MS = 120000 # Default timeout for bash commands (milliseconds)
94
93
  BASH_TERMINATE_TIMEOUT_SEC = 1.0 # Timeout before escalating to SIGKILL (seconds)
94
+ BASH_MODE_SESSION_OUTPUT_MAX_BYTES = 200 * 1024 * 1024 # Max command output captured for session history
95
95
 
96
96
 
97
97
  # =============================================================================
@@ -156,8 +156,8 @@ CROP_ABOVE_LIVE_REFRESH_PER_SECOND = 4.0 # CropAboveLive default refresh rate
156
156
  MARKDOWN_STREAM_LIVE_REPAINT_ENABLED = True # Enable live area for streaming markdown
157
157
  MARKDOWN_STREAM_SYNCHRONIZED_OUTPUT_ENABLED = True # Use terminal "Synchronized Output" to reduce flicker
158
158
  STREAM_MAX_HEIGHT_SHRINK_RESET_LINES = 20 # Reset stream height ceiling after this shrinkage
159
- MARKDOWN_LEFT_MARGIN = 2 # Left margin (columns) for markdown rendering
160
- MARKDOWN_RIGHT_MARGIN = 2 # Right margin (columns) for markdown rendering
159
+ MARKDOWN_LEFT_MARGIN = 0 # Left margin (columns) for markdown rendering
160
+ MARKDOWN_RIGHT_MARGIN = 0 # Right margin (columns) for markdown rendering
161
161
 
162
162
 
163
163
  # =============================================================================
@@ -171,6 +171,7 @@ STATUS_WAITING_TEXT = "Loading …"
171
171
  STATUS_THINKING_TEXT = "Thinking …"
172
172
  STATUS_COMPOSING_TEXT = "Composing"
173
173
  STATUS_COMPACTING_TEXT = "Compacting"
174
+ STATUS_RUNNING_TEXT = "Running …"
174
175
 
175
176
  # Backwards-compatible alias for the default spinner status text.
176
177
  STATUS_DEFAULT_TEXT = STATUS_WAITING_TEXT
@@ -305,3 +305,26 @@ class VanillaModelProfileProvider(ModelProfileProvider):
305
305
  if output_schema:
306
306
  return with_structured_output(profile, output_schema)
307
307
  return profile
308
+
309
+
310
+ class WebModelProfileProvider(DefaultModelProfileProvider):
311
+ """Provider that adds web tools to the main agent."""
312
+
313
+ def build_profile(
314
+ self,
315
+ llm_client: LLMClientABC,
316
+ sub_agent_type: tools.SubAgentType | None = None,
317
+ *,
318
+ output_schema: dict[str, Any] | None = None,
319
+ ) -> AgentProfile:
320
+ profile = super().build_profile(llm_client, sub_agent_type, output_schema=output_schema)
321
+ # Only add web tools for main agent (not sub-agents)
322
+ if sub_agent_type is None:
323
+ web_tools = get_tool_schemas([tools.WEB_FETCH, tools.WEB_SEARCH])
324
+ return AgentProfile(
325
+ llm_client=profile.llm_client,
326
+ system_prompt=profile.system_prompt,
327
+ tools=[*profile.tools, *web_tools],
328
+ reminders=profile.reminders,
329
+ )
330
+ return profile
@@ -0,0 +1,276 @@
1
+ """Bash-mode execution helpers.
2
+
3
+ This module provides the implementation for running non-interactive shell commands
4
+ with streaming output to the UI, plus session history recording.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import contextlib
11
+ import os
12
+ import re
13
+ import secrets
14
+ import shutil
15
+ import signal
16
+ import subprocess
17
+ import sys
18
+ from collections.abc import Awaitable, Callable
19
+ from dataclasses import dataclass
20
+ from pathlib import Path
21
+ from typing import TextIO
22
+
23
+ from klaude_code.const import BASH_MODE_SESSION_OUTPUT_MAX_BYTES, BASH_TERMINATE_TIMEOUT_SEC, TOOL_OUTPUT_TRUNCATION_DIR
24
+ from klaude_code.core.tool.offload import offload_tool_output
25
+ from klaude_code.protocol import events, message
26
+ from klaude_code.session.session import Session
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class _BashModeToolCall:
31
+ tool_name: str = "Bash"
32
+
33
+
34
+ _ANSI_ESCAPE_RE = re.compile(
35
+ r"""
36
+ \x1B
37
+ (?:
38
+ \[[0-?]*[ -/]*[@-~] | # CSI sequences
39
+ \][0-?]*.*?(?:\x07|\x1B\\) | # OSC sequences
40
+ P.*?(?:\x07|\x1B\\) | # DCS sequences
41
+ _.*?(?:\x07|\x1B\\) | # APC sequences
42
+ \^.*?(?:\x07|\x1B\\) | # PM sequences
43
+ [@-Z\\-_] # 2-char sequences
44
+ )
45
+ """,
46
+ re.VERBOSE | re.DOTALL,
47
+ )
48
+
49
+
50
+ def _format_inline_code(text: str) -> str:
51
+ if not text:
52
+ return "``"
53
+ max_run = 0
54
+ run = 0
55
+ for ch in text:
56
+ if ch == "`":
57
+ run += 1
58
+ max_run = max(max_run, run)
59
+ else:
60
+ run = 0
61
+ fence = "`" * (max_run + 1)
62
+ return f"{fence}{text}{fence}"
63
+
64
+
65
+ def _resolve_shell_command(command_text: str) -> list[str]:
66
+ # Use the user's default shell when possible.
67
+ # - macOS/Linux: $SHELL (supports bash/zsh/fish)
68
+ # - Windows: prefer pwsh/powershell
69
+ if sys.platform == "win32": # pragma: no cover
70
+ exe = "pwsh" if shutil.which("pwsh") else "powershell"
71
+ return [exe, "-NoProfile", "-Command", command_text]
72
+
73
+ shell_path = os.environ.get("SHELL")
74
+ shell_name = Path(shell_path).name.lower() if shell_path else ""
75
+ if shell_path and shell_name in {"bash", "zsh", "fish"}:
76
+ # Use -lic to load both login profile and interactive config (e.g. aliases from .zshrc)
77
+ return [shell_path, "-lic", command_text]
78
+ return ["bash", "-lic", command_text]
79
+
80
+
81
+ async def _terminate_process(proc: asyncio.subprocess.Process) -> None:
82
+ if proc.returncode is not None:
83
+ return
84
+
85
+ try:
86
+ if os.name == "posix":
87
+ os.killpg(proc.pid, signal.SIGTERM)
88
+ else: # pragma: no cover
89
+ proc.terminate()
90
+ except ProcessLookupError:
91
+ return
92
+ except OSError:
93
+ pass
94
+
95
+ with contextlib.suppress(Exception):
96
+ await asyncio.wait_for(proc.wait(), timeout=BASH_TERMINATE_TIMEOUT_SEC)
97
+ return
98
+
99
+ with contextlib.suppress(Exception):
100
+ if os.name == "posix":
101
+ os.killpg(proc.pid, signal.SIGKILL)
102
+ else: # pragma: no cover
103
+ proc.kill()
104
+ with contextlib.suppress(Exception):
105
+ await asyncio.wait_for(proc.wait(), timeout=BASH_TERMINATE_TIMEOUT_SEC)
106
+
107
+
108
+ async def _emit_clean_chunk(
109
+ *,
110
+ emit_event: Callable[[events.Event], Awaitable[None]],
111
+ session_id: str,
112
+ chunk: str,
113
+ out_file: TextIO,
114
+ ) -> None:
115
+ if not chunk:
116
+ return
117
+
118
+ cleaned = _ANSI_ESCAPE_RE.sub("", chunk)
119
+ if cleaned:
120
+ await emit_event(events.BashCommandOutputDeltaEvent(session_id=session_id, content=cleaned))
121
+ with contextlib.suppress(Exception):
122
+ out_file.write(cleaned)
123
+
124
+
125
+ async def run_bash_command(
126
+ *,
127
+ emit_event: Callable[[events.Event], Awaitable[None]],
128
+ session: Session,
129
+ session_id: str,
130
+ command: str,
131
+ ) -> None:
132
+ """Run a non-interactive bash command with streaming output to the UI.
133
+
134
+ The full (cleaned) output is appended to session history in a single UserMessage
135
+ as: `Ran <command>` plus truncated output via offload strategy.
136
+ """
137
+
138
+ await emit_event(events.BashCommandStartEvent(session_id=session_id, command=command))
139
+
140
+ # Create a log file to support large outputs without holding everything in memory.
141
+ # Use TOOL_OUTPUT_TRUNCATION_DIR (system temp) for consistency with offload.
142
+ tmp_root = Path(TOOL_OUTPUT_TRUNCATION_DIR)
143
+ tmp_root.mkdir(parents=True, exist_ok=True)
144
+ log_path = tmp_root / f"klaude-bash-mode-{secrets.token_hex(8)}.log"
145
+
146
+ env = os.environ.copy()
147
+ env.update(
148
+ {
149
+ "GIT_TERMINAL_PROMPT": "0",
150
+ "PAGER": "cat",
151
+ "GIT_PAGER": "cat",
152
+ "EDITOR": "true",
153
+ "VISUAL": "true",
154
+ "GIT_EDITOR": "true",
155
+ "JJ_EDITOR": "true",
156
+ "TERM": "dumb",
157
+ }
158
+ )
159
+
160
+ proc: asyncio.subprocess.Process | None = None
161
+ cancelled = False
162
+ exit_code: int | None = None
163
+
164
+ # Hold back any trailing ESC-started sequence to avoid leaking control codes
165
+ # when the subprocess output is chunked.
166
+ pending = ""
167
+
168
+ try:
169
+ kwargs: dict[str, object] = {
170
+ "stdin": asyncio.subprocess.DEVNULL,
171
+ "stdout": asyncio.subprocess.PIPE,
172
+ "stderr": asyncio.subprocess.STDOUT,
173
+ "env": env,
174
+ }
175
+ if os.name == "posix":
176
+ kwargs["start_new_session"] = True
177
+ elif os.name == "nt": # pragma: no cover
178
+ kwargs["creationflags"] = getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0)
179
+
180
+ shell_argv = _resolve_shell_command(command)
181
+ proc = await asyncio.create_subprocess_exec(*shell_argv, **kwargs) # type: ignore[arg-type]
182
+ assert proc.stdout is not None
183
+
184
+ with log_path.open("w", encoding="utf-8", errors="replace") as out_file:
185
+ while True:
186
+ data = await proc.stdout.read(4096)
187
+ if not data:
188
+ break
189
+ piece = data.decode(errors="replace")
190
+ pending += piece
191
+
192
+ # Keep from the last ESC onwards to avoid emitting incomplete sequences.
193
+ last_esc = pending.rfind("\x1b")
194
+ if last_esc == -1:
195
+ to_emit, pending = pending, ""
196
+ elif last_esc < len(pending) - 128:
197
+ to_emit, pending = pending[:last_esc], pending[last_esc:]
198
+ else:
199
+ # Wait for more bytes to complete the sequence.
200
+ continue
201
+
202
+ await _emit_clean_chunk(
203
+ emit_event=emit_event,
204
+ session_id=session_id,
205
+ chunk=to_emit,
206
+ out_file=out_file,
207
+ )
208
+
209
+ if pending:
210
+ await _emit_clean_chunk(
211
+ emit_event=emit_event,
212
+ session_id=session_id,
213
+ chunk=pending,
214
+ out_file=out_file,
215
+ )
216
+ pending = ""
217
+
218
+ exit_code = await proc.wait()
219
+
220
+ except asyncio.CancelledError:
221
+ cancelled = True
222
+ if proc is not None:
223
+ with contextlib.suppress(Exception):
224
+ await asyncio.shield(_terminate_process(proc))
225
+ except Exception as exc:
226
+ # Surface errors to the UI as a final line.
227
+ msg = f"Execution error: {exc.__class__.__name__} {exc}"
228
+ await emit_event(events.BashCommandOutputDeltaEvent(session_id=session_id, content=msg))
229
+ finally:
230
+ header = f"Ran {_format_inline_code(command)}"
231
+
232
+ record_lines: list[str] = [header]
233
+ if cancelled:
234
+ record_lines.append("\n(command cancelled)")
235
+ elif isinstance(exit_code, int) and exit_code != 0:
236
+ record_lines.append(f"\nCommand exited with code {exit_code}")
237
+
238
+ output_text = ""
239
+ output_note_added = False
240
+ try:
241
+ if log_path.exists() and log_path.stat().st_size > BASH_MODE_SESSION_OUTPUT_MAX_BYTES:
242
+ record_lines.append(
243
+ f"\n\n<system-reminder>Output truncated due to length. Full output saved to: {log_path} </system-reminder>"
244
+ )
245
+ output_note_added = True
246
+ else:
247
+ output_text = log_path.read_text("utf-8", errors="replace") if log_path.exists() else ""
248
+ except OSError:
249
+ output_text = ""
250
+
251
+ if output_text.strip() == "":
252
+ if not cancelled and not output_note_added:
253
+ record_lines.append("\n(no output)")
254
+ await emit_event(events.BashCommandOutputDeltaEvent(session_id=session_id, content="(no output)\n"))
255
+ else:
256
+ offloaded = offload_tool_output(output_text, _BashModeToolCall())
257
+ record_lines.append("\n\n" + offloaded.output)
258
+
259
+ # Always emit an end event so the renderer can finalize formatting.
260
+ await emit_event(
261
+ events.BashCommandEndEvent(
262
+ session_id=session_id,
263
+ exit_code=exit_code,
264
+ cancelled=cancelled,
265
+ )
266
+ )
267
+ session.append_history(
268
+ [
269
+ message.UserMessage(
270
+ parts=message.parts_from_text_and_images(
271
+ "".join(record_lines).rstrip(),
272
+ None,
273
+ )
274
+ )
275
+ ]
276
+ )
@@ -18,9 +18,11 @@ from klaude_code.config import load_config
18
18
  from klaude_code.config.sub_agent_model_helper import SubAgentModelHelper
19
19
  from klaude_code.core.agent import Agent
20
20
  from klaude_code.core.agent_profile import DefaultModelProfileProvider, ModelProfileProvider
21
+ from klaude_code.core.bash_mode import run_bash_command
21
22
  from klaude_code.core.compaction import CompactionReason, run_compaction
22
23
  from klaude_code.core.loaded_skills import get_loaded_skill_names_by_location
23
24
  from klaude_code.core.manager import LLMClients, SubAgentManager
25
+ from klaude_code.core.memory import get_existing_memory_paths_by_location
24
26
  from klaude_code.llm.registry import create_llm_client
25
27
  from klaude_code.log import DebugType, log_debug
26
28
  from klaude_code.protocol import commands, events, message, model, op
@@ -134,18 +136,19 @@ class AgentRuntime:
134
136
  compact_llm_client=self._llm_clients.compact,
135
137
  )
136
138
 
137
- async for evt in agent.replay_history():
138
- await self._emit_event(evt)
139
-
140
139
  await self._emit_event(
141
140
  events.WelcomeEvent(
142
141
  session_id=session.id,
143
142
  work_dir=str(session.work_dir),
144
143
  llm_config=self._llm_clients.main.get_llm_config(),
145
144
  loaded_skills=get_loaded_skill_names_by_location(),
145
+ loaded_memories=get_existing_memory_paths_by_location(work_dir=session.work_dir),
146
146
  )
147
147
  )
148
148
 
149
+ async for evt in agent.replay_history():
150
+ await self._emit_event(evt)
151
+
149
152
  self._agent = agent
150
153
  log_debug(
151
154
  f"Initialized agent for session: {session.id}",
@@ -179,6 +182,23 @@ class AgentRuntime:
179
182
  )
180
183
  self._task_manager.register(operation.id, task, operation.session_id)
181
184
 
185
+ async def run_bash(self, operation: op.RunBashOperation) -> None:
186
+ agent = await self.ensure_agent(operation.session_id)
187
+
188
+ existing_active = self._task_manager.get(operation.id)
189
+ if existing_active is not None and not existing_active.task.done():
190
+ raise RuntimeError(f"Active task already registered for operation {operation.id}")
191
+
192
+ task: asyncio.Task[None] = asyncio.create_task(
193
+ self._run_bash_task(
194
+ session=agent.session,
195
+ command=operation.command,
196
+ task_id=operation.id,
197
+ session_id=operation.session_id,
198
+ )
199
+ )
200
+ self._task_manager.register(operation.id, task, operation.session_id)
201
+
182
202
  async def continue_agent(self, operation: op.ContinueAgentOperation) -> None:
183
203
  """Continue agent execution without adding a new user message."""
184
204
  agent = await self.ensure_agent(operation.session_id)
@@ -230,6 +250,7 @@ class AgentRuntime:
230
250
  work_dir=str(agent.session.work_dir),
231
251
  llm_config=self._llm_clients.main.get_llm_config(),
232
252
  loaded_skills=get_loaded_skill_names_by_location(),
253
+ loaded_memories=get_existing_memory_paths_by_location(work_dir=agent.session.work_dir),
233
254
  )
234
255
  )
235
256
 
@@ -249,18 +270,19 @@ class AgentRuntime:
249
270
  compact_llm_client=self._llm_clients.compact,
250
271
  )
251
272
 
252
- async for evt in agent.replay_history():
253
- await self._emit_event(evt)
254
-
255
273
  await self._emit_event(
256
274
  events.WelcomeEvent(
257
275
  session_id=target_session.id,
258
276
  work_dir=str(target_session.work_dir),
259
277
  llm_config=self._llm_clients.main.get_llm_config(),
260
278
  loaded_skills=get_loaded_skill_names_by_location(),
279
+ loaded_memories=get_existing_memory_paths_by_location(work_dir=target_session.work_dir),
261
280
  )
262
281
  )
263
282
 
283
+ async for evt in agent.replay_history():
284
+ await self._emit_event(evt)
285
+
264
286
  self._agent = agent
265
287
  log_debug(
266
288
  f"Resumed session: {target_session.id}",
@@ -359,6 +381,14 @@ class AgentRuntime:
359
381
  debug_type=DebugType.EXECUTION,
360
382
  )
361
383
 
384
+ async def _run_bash_task(self, *, session: Session, command: str, task_id: str, session_id: str) -> None:
385
+ await run_bash_command(
386
+ emit_event=self._emit_event,
387
+ session=session,
388
+ session_id=session_id,
389
+ command=command,
390
+ )
391
+
362
392
  async def _run_compaction_task(
363
393
  self,
364
394
  agent: Agent,
@@ -467,7 +497,7 @@ class ModelSwitcher:
467
497
  config.main_model = model_name
468
498
  await config.save()
469
499
 
470
- return llm_config, llm_client.model_name
500
+ return llm_config, model_name
471
501
 
472
502
  def change_thinking(self, agent: Agent, *, thinking: Thinking) -> Thinking | None:
473
503
  """Apply thinking configuration to the agent's active LLM config and persisted session."""
@@ -540,6 +570,9 @@ class ExecutorContext:
540
570
  async def handle_run_agent(self, operation: op.RunAgentOperation) -> None:
541
571
  await self._agent_runtime.run_agent(operation)
542
572
 
573
+ async def handle_run_bash(self, operation: op.RunBashOperation) -> None:
574
+ await self._agent_runtime.run_bash(operation)
575
+
543
576
  async def handle_continue_agent(self, operation: op.ContinueAgentOperation) -> None:
544
577
  await self._agent_runtime.continue_agent(operation)
545
578
 
@@ -19,6 +19,7 @@ class LLMClients:
19
19
  """Container for LLM clients used by main agent and sub-agents."""
20
20
 
21
21
  main: LLMClientABC
22
+ main_model_alias: str = ""
22
23
  sub_clients: dict[SubAgentType, LLMClientABC] = dataclass_field(default_factory=_default_sub_clients)
23
24
  compact: LLMClientABC | None = None
24
25
 
@@ -53,7 +53,7 @@ def build_llm_clients(
53
53
  compact_client = create_llm_client(compact_llm_config)
54
54
 
55
55
  if skip_sub_agents:
56
- return LLMClients(main=main_client, compact=compact_client)
56
+ return LLMClients(main=main_client, main_model_alias=model_name, compact=compact_client)
57
57
 
58
58
  helper = SubAgentModelHelper(config)
59
59
  sub_agent_configs = helper.build_sub_agent_client_configs()
@@ -63,4 +63,4 @@ def build_llm_clients(
63
63
  sub_llm_config = config.get_model_config(sub_model_name)
64
64
  sub_clients[sub_agent_type] = create_llm_client(sub_llm_config)
65
65
 
66
- return LLMClients(main=main_client, sub_clients=sub_clients, compact=compact_client)
66
+ return LLMClients(main=main_client, main_model_alias=model_name, sub_clients=sub_clients, compact=compact_client)