klaude-code 1.2.15__py3-none-any.whl → 1.2.17__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 (59) hide show
  1. klaude_code/cli/main.py +66 -42
  2. klaude_code/cli/runtime.py +34 -13
  3. klaude_code/command/__init__.py +3 -0
  4. klaude_code/command/export_cmd.py +2 -2
  5. klaude_code/command/export_online_cmd.py +149 -0
  6. klaude_code/command/prompt-handoff.md +33 -0
  7. klaude_code/command/thinking_cmd.py +5 -1
  8. klaude_code/config/config.py +20 -21
  9. klaude_code/config/list_model.py +1 -1
  10. klaude_code/const/__init__.py +3 -0
  11. klaude_code/core/executor.py +2 -2
  12. klaude_code/core/manager/llm_clients_builder.py +1 -1
  13. klaude_code/core/manager/sub_agent_manager.py +30 -6
  14. klaude_code/core/prompt.py +15 -13
  15. klaude_code/core/prompts/{prompt-subagent-explore.md → prompt-sub-agent-explore.md} +0 -1
  16. klaude_code/core/prompts/{prompt-subagent-oracle.md → prompt-sub-agent-oracle.md} +1 -2
  17. klaude_code/core/prompts/prompt-sub-agent-web.md +48 -0
  18. klaude_code/core/reminders.py +75 -32
  19. klaude_code/core/task.py +18 -22
  20. klaude_code/core/tool/__init__.py +4 -0
  21. klaude_code/core/tool/report_back_tool.py +84 -0
  22. klaude_code/core/tool/sub_agent_tool.py +6 -0
  23. klaude_code/core/tool/tool_runner.py +9 -1
  24. klaude_code/core/tool/web/web_search_tool.md +23 -0
  25. klaude_code/core/tool/web/web_search_tool.py +126 -0
  26. klaude_code/core/turn.py +45 -4
  27. klaude_code/llm/anthropic/input.py +14 -5
  28. klaude_code/llm/openrouter/input.py +14 -3
  29. klaude_code/llm/responses/input.py +19 -0
  30. klaude_code/protocol/commands.py +1 -0
  31. klaude_code/protocol/events.py +9 -0
  32. klaude_code/protocol/model.py +24 -14
  33. klaude_code/protocol/sub_agent/__init__.py +117 -0
  34. klaude_code/protocol/sub_agent/explore.py +63 -0
  35. klaude_code/protocol/sub_agent/oracle.py +91 -0
  36. klaude_code/protocol/sub_agent/task.py +61 -0
  37. klaude_code/protocol/sub_agent/web.py +78 -0
  38. klaude_code/protocol/tools.py +2 -0
  39. klaude_code/session/export.py +12 -6
  40. klaude_code/session/session.py +12 -2
  41. klaude_code/session/templates/export_session.html +111 -36
  42. klaude_code/ui/modes/repl/completers.py +1 -1
  43. klaude_code/ui/modes/repl/event_handler.py +65 -8
  44. klaude_code/ui/modes/repl/renderer.py +11 -9
  45. klaude_code/ui/renderers/developer.py +18 -7
  46. klaude_code/ui/renderers/metadata.py +24 -12
  47. klaude_code/ui/renderers/sub_agent.py +63 -3
  48. klaude_code/ui/renderers/thinking.py +1 -1
  49. klaude_code/ui/renderers/tools.py +24 -37
  50. klaude_code/ui/rich/markdown.py +20 -48
  51. klaude_code/ui/rich/status.py +61 -17
  52. klaude_code/ui/rich/theme.py +8 -7
  53. {klaude_code-1.2.15.dist-info → klaude_code-1.2.17.dist-info}/METADATA +114 -22
  54. {klaude_code-1.2.15.dist-info → klaude_code-1.2.17.dist-info}/RECORD +57 -48
  55. klaude_code/core/prompts/prompt-subagent-webfetch.md +0 -46
  56. klaude_code/protocol/sub_agent.py +0 -354
  57. /klaude_code/core/prompts/{prompt-subagent.md → prompt-sub-agent.md} +0 -0
  58. {klaude_code-1.2.15.dist-info → klaude_code-1.2.17.dist-info}/WHEEL +0 -0
  59. {klaude_code-1.2.15.dist-info → klaude_code-1.2.17.dist-info}/entry_points.txt +0 -0
klaude_code/cli/main.py CHANGED
@@ -13,7 +13,7 @@ from klaude_code.cli.debug import DEBUG_FILTER_HELP, open_log_file_in_editor, re
13
13
  from klaude_code.cli.session_cmd import register_session_commands
14
14
  from klaude_code.config import load_config
15
15
  from klaude_code.session import Session, resume_select_session
16
- from klaude_code.trace import prepare_debug_log_file
16
+ from klaude_code.trace import DebugType, prepare_debug_log_file
17
17
 
18
18
 
19
19
  def set_terminal_title(title: str) -> None:
@@ -22,6 +22,63 @@ def set_terminal_title(title: str) -> None:
22
22
  sys.stdout.flush()
23
23
 
24
24
 
25
+ def setup_terminal_title() -> None:
26
+ """Set terminal title to current folder name."""
27
+ folder_name = os.path.basename(os.getcwd())
28
+ set_terminal_title(f"{folder_name}: klaude")
29
+
30
+
31
+ def prepare_debug_logging(debug: bool, debug_filter: str | None) -> tuple[bool, set[DebugType] | None, Path | None]:
32
+ """Resolve debug settings and prepare log file if debugging is enabled.
33
+
34
+ Returns:
35
+ A tuple of (debug_enabled, debug_filters, log_path).
36
+ log_path is None if debugging is disabled.
37
+ """
38
+ debug_enabled, debug_filters = resolve_debug_settings(debug, debug_filter)
39
+ log_path: Path | None = None
40
+ if debug_enabled:
41
+ log_path = prepare_debug_log_file()
42
+ return debug_enabled, debug_filters, log_path
43
+
44
+
45
+ def read_input_content(cli_argument: str) -> str | None:
46
+ """Read and merge input from stdin and CLI argument.
47
+
48
+ Args:
49
+ cli_argument: The input content passed as CLI argument.
50
+
51
+ Returns:
52
+ The merged input content, or None if no input was provided.
53
+ """
54
+ from klaude_code.trace import log
55
+
56
+ parts: list[str] = []
57
+
58
+ # Handle stdin input
59
+ if not sys.stdin.isatty():
60
+ try:
61
+ stdin = sys.stdin.read().rstrip("\n")
62
+ if stdin:
63
+ parts.append(stdin)
64
+ except (OSError, ValueError) as e:
65
+ # Expected I/O-related errors when reading from stdin (e.g. broken pipe, closed stream).
66
+ log((f"Error reading from stdin: {e}", "red"))
67
+ except Exception as e:
68
+ # Unexpected errors are still reported but kept from crashing the CLI.
69
+ log((f"Unexpected error reading from stdin: {e}", "red"))
70
+
71
+ if cli_argument:
72
+ parts.append(cli_argument)
73
+
74
+ content = "\n".join(parts)
75
+ if len(content) == 0:
76
+ log(("Error: No input content provided", "red"))
77
+ return None
78
+
79
+ return content
80
+
81
+
25
82
  def _version_callback(value: bool) -> None:
26
83
  """Show version and exit."""
27
84
  if value:
@@ -90,33 +147,10 @@ def exec_command(
90
147
  ),
91
148
  ) -> None:
92
149
  """Execute non-interactively with provided input."""
93
- from klaude_code.trace import log
94
-
95
- # Set terminal title with current folder name
96
- folder_name = os.path.basename(os.getcwd())
97
- set_terminal_title(f"{folder_name}: klaude")
98
-
99
- parts: list[str] = []
150
+ setup_terminal_title()
100
151
 
101
- # Handle stdin input
102
- if not sys.stdin.isatty():
103
- try:
104
- stdin = sys.stdin.read().rstrip("\n")
105
- if stdin:
106
- parts.append(stdin)
107
- except (OSError, ValueError) as e:
108
- # Expected I/O-related errors when reading from stdin (e.g. broken pipe, closed stream).
109
- log((f"Error reading from stdin: {e}", "red"))
110
- except Exception as e:
111
- # Unexpected errors are still reported but kept from crashing the CLI.
112
- log((f"Unexpected error reading from stdin: {e}", "red"))
113
-
114
- if input_content:
115
- parts.append(input_content)
116
-
117
- input_content = "\n".join(parts)
118
- if len(input_content) == 0:
119
- log(("Error: No input content provided", "red"))
152
+ merged_input = read_input_content(input_content)
153
+ if merged_input is None:
120
154
  raise typer.Exit(1)
121
155
 
122
156
  from klaude_code.cli.runtime import AppInitConfig, run_exec
@@ -133,11 +167,7 @@ def exec_command(
133
167
  if chosen_model is None:
134
168
  return
135
169
 
136
- debug_enabled, debug_filters = resolve_debug_settings(debug, debug_filter)
137
-
138
- log_path: Path | None = None
139
- if debug_enabled:
140
- log_path = prepare_debug_log_file()
170
+ debug_enabled, debug_filters, log_path = prepare_debug_logging(debug, debug_filter)
141
171
 
142
172
  init_config = AppInitConfig(
143
173
  model=chosen_model,
@@ -154,7 +184,7 @@ def exec_command(
154
184
  asyncio.run(
155
185
  run_exec(
156
186
  init_config=init_config,
157
- input_content=input_content,
187
+ input_content=merged_input,
158
188
  )
159
189
  )
160
190
 
@@ -210,10 +240,8 @@ def main_callback(
210
240
  from klaude_code.cli.runtime import AppInitConfig, run_interactive
211
241
  from klaude_code.config.select_model import select_model_from_config
212
242
 
213
- # Set terminal title with current folder name
214
- folder_name = os.path.basename(os.getcwd())
215
- set_terminal_title(f"{folder_name}: klaude")
216
- # Interactive mode
243
+ setup_terminal_title()
244
+
217
245
  chosen_model = model
218
246
  if select_model:
219
247
  chosen_model = select_model_from_config(preferred=model)
@@ -232,11 +260,7 @@ def main_callback(
232
260
  session_id = Session.most_recent_session_id()
233
261
  # If still no session_id, leave as None to create a new session
234
262
 
235
- debug_enabled, debug_filters = resolve_debug_settings(debug, debug_filter)
236
-
237
- log_path: Path | None = None
238
- if debug_enabled:
239
- log_path = prepare_debug_log_file()
263
+ debug_enabled, debug_filters, log_path = prepare_debug_logging(debug, debug_filter)
240
264
 
241
265
  init_config = AppInitConfig(
242
266
  model=chosen_model,
@@ -127,6 +127,28 @@ async def initialize_app_components(init_config: AppInitConfig) -> AppComponents
127
127
  )
128
128
 
129
129
 
130
+ async def initialize_session(
131
+ executor: Executor,
132
+ event_queue: asyncio.Queue[events.Event],
133
+ session_id: str | None = None,
134
+ ) -> str | None:
135
+ """Initialize a session and return the active session ID.
136
+
137
+ Args:
138
+ executor: The executor to submit operations to.
139
+ event_queue: The event queue for synchronization.
140
+ session_id: Optional session ID to resume. If None, creates a new session.
141
+
142
+ Returns:
143
+ The active session ID, or None if no session is active.
144
+ """
145
+ await executor.submit_and_wait(op.InitAgentOperation(session_id=session_id))
146
+ await event_queue.join()
147
+
148
+ active_session_ids = executor.context.agent_manager.active_session_ids()
149
+ return active_session_ids[0] if active_session_ids else session_id
150
+
151
+
130
152
  async def cleanup_app_components(components: AppComponents) -> None:
131
153
  """Clean up all application components."""
132
154
  try:
@@ -167,13 +189,7 @@ async def run_exec(init_config: AppInitConfig, input_content: str) -> None:
167
189
  components = await initialize_app_components(init_config)
168
190
 
169
191
  try:
170
- # Initialize a new session (session_id=None means create new)
171
- await components.executor.submit_and_wait(op.InitAgentOperation())
172
- await components.event_queue.join()
173
-
174
- # Get the session_id from the newly created agent
175
- session_ids = components.executor.context.agent_manager.active_session_ids()
176
- session_id = session_ids[0] if session_ids else None
192
+ session_id = await initialize_session(components.executor, components.event_queue)
177
193
 
178
194
  # Submit the input content directly
179
195
  await components.executor.submit_and_wait(
@@ -245,12 +261,15 @@ async def run_interactive(init_config: AppInitConfig, session_id: str | None = N
245
261
  restore_sigint = install_sigint_double_press_exit(_show_toast_once, _hide_progress)
246
262
 
247
263
  try:
248
- await components.executor.submit_and_wait(op.InitAgentOperation(session_id=session_id))
249
- await components.event_queue.join()
264
+ await initialize_session(components.executor, components.event_queue, session_id=session_id)
265
+
266
+ def _get_active_session_id() -> str | None:
267
+ """Get the current active session ID dynamically.
250
268
 
251
- # Get the actual session_id (may have been auto-generated if None was passed)
252
- active_session_ids = components.executor.context.agent_manager.active_session_ids()
253
- active_session_id = active_session_ids[0] if active_session_ids else session_id
269
+ This is necessary because /clear command creates a new session with a different ID.
270
+ """
271
+ active_ids = components.executor.context.agent_manager.active_session_ids()
272
+ return active_ids[0] if active_ids else None
254
273
 
255
274
  # Input
256
275
  await input_provider.start()
@@ -261,6 +280,8 @@ async def run_interactive(init_config: AppInitConfig, session_id: str | None = N
261
280
  elif user_input.text.strip() == "":
262
281
  continue
263
282
  # Submit user input operation - directly use the payload from iter_inputs
283
+ # Use dynamic session_id lookup to handle /clear creating new sessions
284
+ active_session_id = _get_active_session_id()
264
285
  submission_id = await components.executor.submit(
265
286
  op.UserInputOperation(input=user_input, session_id=active_session_id)
266
287
  )
@@ -271,7 +292,7 @@ async def run_interactive(init_config: AppInitConfig, session_id: str | None = N
271
292
  else:
272
293
  # Esc monitor for long-running, interruptible operations
273
294
  async def _on_esc_interrupt() -> None:
274
- await components.executor.submit(op.InterruptOperation(target_session_id=active_session_id))
295
+ await components.executor.submit(op.InterruptOperation(target_session_id=_get_active_session_id()))
275
296
 
276
297
  stop_event, esc_task = start_esc_interrupt_monitor(_on_esc_interrupt)
277
298
  # Wait for this specific task to complete before accepting next input
@@ -30,6 +30,7 @@ def ensure_commands_loaded() -> None:
30
30
  from .clear_cmd import ClearCommand
31
31
  from .diff_cmd import DiffCommand
32
32
  from .export_cmd import ExportCommand
33
+ from .export_online_cmd import ExportOnlineCommand
33
34
  from .help_cmd import HelpCommand
34
35
  from .model_cmd import ModelCommand
35
36
  from .refresh_cmd import RefreshTerminalCommand
@@ -40,6 +41,7 @@ def ensure_commands_loaded() -> None:
40
41
 
41
42
  # Register in desired display order
42
43
  register(ExportCommand())
44
+ register(ExportOnlineCommand())
43
45
  register(RefreshTerminalCommand())
44
46
  register(ThinkingCommand())
45
47
  register(ModelCommand())
@@ -60,6 +62,7 @@ def __getattr__(name: str) -> object:
60
62
  "ClearCommand": "clear_cmd",
61
63
  "DiffCommand": "diff_cmd",
62
64
  "ExportCommand": "export_cmd",
65
+ "ExportOnlineCommand": "export_online_cmd",
63
66
  "HelpCommand": "help_cmd",
64
67
  "ModelCommand": "model_cmd",
65
68
  "RefreshTerminalCommand": "refresh_cmd",
@@ -25,7 +25,7 @@ class ExportCommand(CommandABC):
25
25
 
26
26
  @property
27
27
  def support_addition_params(self) -> bool:
28
- return True
28
+ return False
29
29
 
30
30
  @property
31
31
  def is_interactive(self) -> bool:
@@ -33,7 +33,7 @@ class ExportCommand(CommandABC):
33
33
 
34
34
  async def run(self, raw: str, agent: Agent) -> CommandResult:
35
35
  try:
36
- output_path = self._resolve_output_path(raw, agent)
36
+ output_path = self._resolve_output_path("", agent)
37
37
  html_doc = self._build_html(agent)
38
38
  output_path.parent.mkdir(parents=True, exist_ok=True)
39
39
  output_path.write_text(html_doc, encoding="utf-8")
@@ -0,0 +1,149 @@
1
+ from __future__ import annotations
2
+
3
+ import secrets
4
+ import shutil
5
+ import subprocess
6
+ import tempfile
7
+ from pathlib import Path
8
+ from typing import TYPE_CHECKING
9
+
10
+ from klaude_code.command.command_abc import CommandABC, CommandResult
11
+ from klaude_code.protocol import commands, events, model
12
+ from klaude_code.session.export import build_export_html
13
+
14
+ if TYPE_CHECKING:
15
+ from klaude_code.core.agent import Agent
16
+
17
+
18
+ class ExportOnlineCommand(CommandABC):
19
+ """Export and deploy the current session to surge.sh as a static webpage."""
20
+
21
+ @property
22
+ def name(self) -> commands.CommandName:
23
+ return commands.CommandName.EXPORT_ONLINE
24
+
25
+ @property
26
+ def summary(self) -> str:
27
+ return "Export and deploy session to surge.sh"
28
+
29
+ @property
30
+ def support_addition_params(self) -> bool:
31
+ return False
32
+
33
+ @property
34
+ def is_interactive(self) -> bool:
35
+ return False
36
+
37
+ async def run(self, raw: str, agent: Agent) -> CommandResult:
38
+ # Check if npx or surge is available
39
+ surge_cmd = self._get_surge_command()
40
+ if not surge_cmd:
41
+ event = events.DeveloperMessageEvent(
42
+ session_id=agent.session.id,
43
+ item=model.DeveloperMessageItem(
44
+ content="surge.sh CLI not found. Install with: npm install -g surge",
45
+ command_output=model.CommandOutput(command_name=self.name, is_error=True),
46
+ ),
47
+ )
48
+ return CommandResult(events=[event])
49
+
50
+ # Check if user is logged in to surge
51
+ if not self._is_surge_logged_in(surge_cmd):
52
+ login_cmd = " ".join([*surge_cmd, "login"])
53
+ event = events.DeveloperMessageEvent(
54
+ session_id=agent.session.id,
55
+ item=model.DeveloperMessageItem(
56
+ content=f"Not logged in to surge.sh. Please run: {login_cmd}",
57
+ command_output=model.CommandOutput(command_name=self.name, is_error=True),
58
+ ),
59
+ )
60
+ return CommandResult(events=[event])
61
+
62
+ try:
63
+ html_doc = self._build_html(agent)
64
+ domain = self._generate_domain()
65
+ url = self._deploy_to_surge(surge_cmd, html_doc, domain)
66
+
67
+ event = events.DeveloperMessageEvent(
68
+ session_id=agent.session.id,
69
+ item=model.DeveloperMessageItem(
70
+ content=f"Session deployed to: {url}",
71
+ command_output=model.CommandOutput(command_name=self.name),
72
+ ),
73
+ )
74
+ return CommandResult(events=[event])
75
+ except Exception as exc:
76
+ import traceback
77
+
78
+ event = events.DeveloperMessageEvent(
79
+ session_id=agent.session.id,
80
+ item=model.DeveloperMessageItem(
81
+ content=f"Failed to deploy session: {exc}\n{traceback.format_exc()}",
82
+ command_output=model.CommandOutput(command_name=self.name, is_error=True),
83
+ ),
84
+ )
85
+ return CommandResult(events=[event])
86
+
87
+ def _get_surge_command(self) -> list[str] | None:
88
+ """Check if surge CLI is available, prefer npx if available."""
89
+ # Check for npx first (more common)
90
+ if shutil.which("npx"):
91
+ return ["npx", "surge"]
92
+ # Check for globally installed surge
93
+ if shutil.which("surge"):
94
+ return ["surge"]
95
+ return None
96
+
97
+ def _is_surge_logged_in(self, surge_cmd: list[str]) -> bool:
98
+ """Check if user is logged in to surge.sh via 'surge whoami'."""
99
+ try:
100
+ cmd = [*surge_cmd, "whoami"]
101
+ result = subprocess.run(
102
+ cmd,
103
+ capture_output=True,
104
+ text=True,
105
+ timeout=30,
106
+ )
107
+ # If logged in, whoami returns 0 and prints the email
108
+ # If not logged in, it returns non-zero or prints "Not Authenticated"
109
+ if result.returncode != 0:
110
+ return False
111
+ output = (result.stdout + result.stderr).lower()
112
+ if "not authenticated" in output or "not logged in" in output:
113
+ return False
114
+ return bool(result.stdout.strip())
115
+ except (subprocess.TimeoutExpired, OSError):
116
+ return False
117
+
118
+ def _generate_domain(self) -> str:
119
+ """Generate a random subdomain for surge.sh."""
120
+ random_suffix = secrets.token_hex(4)
121
+ return f"klaude-session-{random_suffix}.surge.sh"
122
+
123
+ def _deploy_to_surge(self, surge_cmd: list[str], html_content: str, domain: str) -> str:
124
+ """Deploy HTML content to surge.sh and return the URL."""
125
+ with tempfile.TemporaryDirectory() as tmpdir:
126
+ html_path = Path(tmpdir) / "index.html"
127
+ html_path.write_text(html_content, encoding="utf-8")
128
+
129
+ # Run surge with --domain flag
130
+ cmd = [*surge_cmd, tmpdir, "--domain", domain]
131
+ result = subprocess.run(
132
+ cmd,
133
+ capture_output=True,
134
+ text=True,
135
+ timeout=60,
136
+ )
137
+
138
+ if result.returncode != 0:
139
+ error_msg = result.stderr or result.stdout or "Unknown error"
140
+ raise RuntimeError(f"Surge deployment failed: {error_msg}")
141
+
142
+ return f"https://{domain}"
143
+
144
+ def _build_html(self, agent: Agent) -> str:
145
+ profile = agent.profile
146
+ system_prompt = (profile.system_prompt if profile else "") or ""
147
+ tools = profile.tools if profile else []
148
+ model_name = profile.llm_client.model_name if profile else "unknown"
149
+ return build_export_html(agent.session, system_prompt, tools, model_name)
@@ -0,0 +1,33 @@
1
+ ---
2
+ description: Write a HANDOFF.md for another agent to continue the conversation
3
+ from: amp-cli https://ampcode.com/manual#handoff
4
+ ---
5
+
6
+ Write a HANDOFF.md file in the current working directory for another agent to continue this conversation.
7
+
8
+ Extract relevant context from the conversation above to facilitate continuing this work. Write from my perspective (first person: "I did...", "I told you...").
9
+
10
+ Consider what would be useful to know based on my request below. Relevant questions include:
11
+ - What did I just do or implement?
12
+ - What instructions did I give you that are still relevant (e.g., follow patterns in the codebase)?
13
+ - Did I provide a plan or spec that should be included?
14
+ - What important information did I share (certain libraries, patterns, constraints, preferences)?
15
+ - What key technical details did I discover (APIs, methods, patterns)?
16
+ - What caveats, limitations, or open questions remain?
17
+
18
+ Extract only what matters for the specific request below. Skip irrelevant questions. Choose an appropriate length based on the complexity of the request.
19
+
20
+ Focus on capabilities and behavior, not file-by-file changes. Avoid excessive implementation details (variable names, storage keys, constants) unless critical.
21
+
22
+ Format: Plain text with bullets. No markdown headers, no bold/italic, no code fences. Use workspace-relative paths for files.
23
+
24
+ List file or directory paths (workspace-relative) relevant to accomplishing the goal in the following format:
25
+ <example>
26
+ @src/project/main.py
27
+ @src/project/llm/
28
+ </example>
29
+
30
+ My request:
31
+ $ARGUMENTS
32
+
33
+ <system>If the request section is empty, ask for clarification about the goal</system>
@@ -211,7 +211,11 @@ class ThinkingCommand(CommandABC):
211
211
  content=f"Thinking changed: {current} -> {new_status}",
212
212
  command_output=model.CommandOutput(command_name=self.name),
213
213
  ),
214
- )
214
+ ),
215
+ events.WelcomeEvent(
216
+ work_dir=str(agent.session.work_dir),
217
+ llm_config=config,
218
+ ),
215
219
  ]
216
220
  )
217
221
 
@@ -23,20 +23,20 @@ class Config(BaseModel):
23
23
  provider_list: list[llm_param.LLMConfigProviderParameter]
24
24
  model_list: list[ModelConfig]
25
25
  main_model: str
26
- subagent_models: dict[str, str] = Field(default_factory=dict)
26
+ sub_agent_models: dict[str, str] = Field(default_factory=dict)
27
27
  theme: str | None = None
28
28
 
29
29
  @model_validator(mode="before")
30
30
  @classmethod
31
- def _normalize_subagent_models(cls, data: dict[str, Any]) -> dict[str, Any]:
32
- raw_val: Any = data.get("subagent_models") or {}
31
+ def _normalize_sub_agent_models(cls, data: dict[str, Any]) -> dict[str, Any]:
32
+ raw_val: Any = data.get("sub_agent_models") or {}
33
33
  raw_models: dict[str, Any] = cast(dict[str, Any], raw_val) if isinstance(raw_val, dict) else {}
34
34
  normalized: dict[str, str] = {}
35
35
  key_map = {p.name.lower(): p.name for p in iter_sub_agent_profiles()}
36
36
  for key, value in dict(raw_models).items():
37
37
  canonical = key_map.get(str(key).lower(), str(key))
38
38
  normalized[canonical] = str(value)
39
- data["subagent_models"] = normalized
39
+ data["sub_agent_models"] = normalized
40
40
  return data
41
41
 
42
42
  def get_main_model_config(self) -> llm_param.LLMConfigParameter:
@@ -79,8 +79,8 @@ class Config(BaseModel):
79
79
 
80
80
  def get_example_config() -> Config:
81
81
  return Config(
82
- main_model="gpt-5.1",
83
- subagent_models={"explore": "haiku", "oracle": "gpt-5.1-high"},
82
+ main_model="opus",
83
+ sub_agent_models={"explore": "haiku", "oracle": "gpt-5.1", "webagent": "haiku", "task": "opus"},
84
84
  provider_list=[
85
85
  llm_param.LLMConfigProviderParameter(
86
86
  provider_name="openai",
@@ -93,6 +93,11 @@ def get_example_config() -> Config:
93
93
  protocol=llm_param.LLMClientProtocol.OPENROUTER,
94
94
  api_key="your-openrouter-api-key",
95
95
  ),
96
+ llm_param.LLMConfigProviderParameter(
97
+ provider_name="anthropic",
98
+ protocol=llm_param.LLMClientProtocol.ANTHROPIC,
99
+ api_key="your-anthropic-api-key",
100
+ ),
96
101
  ],
97
102
  model_list=[
98
103
  ModelConfig(
@@ -100,31 +105,25 @@ def get_example_config() -> Config:
100
105
  provider="openai",
101
106
  model_params=llm_param.LLMConfigModelParameter(
102
107
  model="gpt-5.1-2025-11-13",
103
- max_tokens=32000,
104
108
  verbosity="medium",
105
109
  thinking=llm_param.Thinking(
106
- reasoning_effort="medium",
110
+ reasoning_effort="high",
107
111
  reasoning_summary="auto",
108
- type="enabled",
109
- budget_tokens=None,
110
112
  ),
111
- context_limit=368000,
113
+ context_limit=400000,
112
114
  ),
113
115
  ),
114
116
  ModelConfig(
115
- model_name="gpt-5.1-high",
116
- provider="openai",
117
+ model_name="opus",
118
+ provider="anthropic",
117
119
  model_params=llm_param.LLMConfigModelParameter(
118
- model="gpt-5.1-2025-11-13",
119
- max_tokens=32000,
120
- verbosity="medium",
120
+ model="claude-opus-4-5-20251101",
121
+ verbosity="high",
121
122
  thinking=llm_param.Thinking(
122
- reasoning_effort="high",
123
- reasoning_summary="auto",
124
123
  type="enabled",
125
- budget_tokens=None,
124
+ budget_tokens=31999,
126
125
  ),
127
- context_limit=368000,
126
+ context_limit=200000,
128
127
  ),
129
128
  ),
130
129
  ModelConfig(
@@ -136,7 +135,7 @@ def get_example_config() -> Config:
136
135
  provider_routing=llm_param.OpenRouterProviderRouting(
137
136
  sort="throughput",
138
137
  ),
139
- context_limit=168000,
138
+ context_limit=200000,
140
139
  ),
141
140
  ),
142
141
  ],
@@ -198,7 +198,7 @@ def display_models_and_providers(config: Config):
198
198
  )
199
199
 
200
200
  for profile in iter_sub_agent_profiles():
201
- sub_model_name = config.subagent_models.get(profile.name)
201
+ sub_model_name = config.sub_agent_models.get(profile.name)
202
202
  if not sub_model_name:
203
203
  continue
204
204
  console.print(
@@ -105,6 +105,9 @@ UI_REFRESH_RATE_FPS = 20
105
105
  # Number of lines to keep visible at bottom of markdown streaming window
106
106
  MARKDOWN_STREAM_LIVE_WINDOW = 20
107
107
 
108
+ # Status hint text shown after spinner status
109
+ STATUS_HINT_TEXT = " (esc to interrupt)"
110
+
108
111
  # Status shimmer animation
109
112
  # Horizontal padding used when computing shimmer band position
110
113
  STATUS_SHIMMER_PADDING = 10
@@ -96,7 +96,7 @@ class ExecutorContext:
96
96
  # Delegate responsibilities to helper components
97
97
  self.agent_manager = AgentManager(event_queue, llm_clients, resolved_profile_provider)
98
98
  self.task_manager = TaskManager()
99
- self.subagent_manager = SubAgentManager(event_queue, llm_clients, resolved_profile_provider)
99
+ self.sub_agent_manager = SubAgentManager(event_queue, llm_clients, resolved_profile_provider)
100
100
 
101
101
  async def emit_event(self, event: events.Event) -> None:
102
102
  """Emit an event to the UI display system."""
@@ -240,7 +240,7 @@ class ExecutorContext:
240
240
 
241
241
  # Inject subtask runner into tool context for nested Task tool usage
242
242
  async def _runner(state: model.SubAgentState) -> SubAgentResult:
243
- return await self.subagent_manager.run_subagent(agent, state)
243
+ return await self.sub_agent_manager.run_sub_agent(agent, state)
244
244
 
245
245
  token = current_run_subtask_callback.set(_runner)
246
246
  try:
@@ -32,7 +32,7 @@ def build_llm_clients(
32
32
  sub_clients: dict[SubAgentType, LLMClientABC] = {}
33
33
 
34
34
  for profile in iter_sub_agent_profiles():
35
- model_name = config.subagent_models.get(profile.name)
35
+ model_name = config.sub_agent_models.get(profile.name)
36
36
  if not model_name:
37
37
  continue
38
38