klaude-code 1.2.17__py3-none-any.whl → 1.2.18__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 (54) hide show
  1. klaude_code/cli/config_cmd.py +1 -1
  2. klaude_code/cli/debug.py +1 -1
  3. klaude_code/cli/main.py +3 -9
  4. klaude_code/cli/runtime.py +10 -13
  5. klaude_code/command/__init__.py +4 -1
  6. klaude_code/command/clear_cmd.py +2 -7
  7. klaude_code/command/command_abc.py +33 -5
  8. klaude_code/command/debug_cmd.py +79 -0
  9. klaude_code/command/diff_cmd.py +2 -6
  10. klaude_code/command/export_cmd.py +7 -7
  11. klaude_code/command/export_online_cmd.py +1 -5
  12. klaude_code/command/help_cmd.py +4 -9
  13. klaude_code/command/model_cmd.py +10 -6
  14. klaude_code/command/prompt_command.py +2 -6
  15. klaude_code/command/refresh_cmd.py +2 -7
  16. klaude_code/command/registry.py +2 -4
  17. klaude_code/command/release_notes_cmd.py +2 -6
  18. klaude_code/command/status_cmd.py +2 -7
  19. klaude_code/command/terminal_setup_cmd.py +2 -6
  20. klaude_code/command/thinking_cmd.py +13 -8
  21. klaude_code/config/select_model.py +81 -5
  22. klaude_code/const/__init__.py +1 -1
  23. klaude_code/core/executor.py +236 -109
  24. klaude_code/core/manager/__init__.py +2 -4
  25. klaude_code/core/prompts/prompt-claude-code.md +1 -1
  26. klaude_code/core/prompts/prompt-sub-agent-web.md +8 -5
  27. klaude_code/core/reminders.py +9 -35
  28. klaude_code/core/tool/file/read_tool.py +38 -10
  29. klaude_code/core/tool/shell/bash_tool.py +22 -2
  30. klaude_code/core/tool/tool_runner.py +26 -23
  31. klaude_code/core/tool/truncation.py +23 -9
  32. klaude_code/core/tool/web/web_fetch_tool.md +1 -1
  33. klaude_code/core/tool/web/web_fetch_tool.py +36 -1
  34. klaude_code/core/turn.py +28 -0
  35. klaude_code/protocol/commands.py +1 -0
  36. klaude_code/protocol/sub_agent/web.py +3 -2
  37. klaude_code/session/session.py +2 -2
  38. klaude_code/session/templates/export_session.html +24 -13
  39. klaude_code/trace/__init__.py +20 -2
  40. klaude_code/ui/modes/repl/completers.py +19 -2
  41. klaude_code/ui/modes/repl/event_handler.py +8 -6
  42. klaude_code/ui/renderers/metadata.py +2 -4
  43. klaude_code/ui/renderers/thinking.py +24 -8
  44. klaude_code/ui/renderers/tools.py +79 -10
  45. klaude_code/ui/rich/code_panel.py +112 -0
  46. klaude_code/ui/rich/markdown.py +3 -4
  47. klaude_code/ui/rich/status.py +0 -2
  48. klaude_code/ui/rich/theme.py +10 -1
  49. {klaude_code-1.2.17.dist-info → klaude_code-1.2.18.dist-info}/METADATA +16 -6
  50. {klaude_code-1.2.17.dist-info → klaude_code-1.2.18.dist-info}/RECORD +53 -52
  51. klaude_code/core/manager/agent_manager.py +0 -132
  52. /klaude_code/{config → cli}/list_model.py +0 -0
  53. {klaude_code-1.2.17.dist-info → klaude_code-1.2.18.dist-info}/WHEEL +0 -0
  54. {klaude_code-1.2.17.dist-info → klaude_code-1.2.18.dist-info}/entry_points.txt +0 -0
@@ -1,35 +1,111 @@
1
- from klaude_code.config.config import load_config
1
+ from klaude_code.config.config import ModelConfig, load_config
2
2
  from klaude_code.trace import log
3
3
 
4
4
 
5
+ def _normalize_model_key(value: str) -> str:
6
+ """Normalize a model identifier for loose matching.
7
+
8
+ This enables aliases like:
9
+ - gpt52 -> gpt-5.2
10
+ - gpt5.2 -> gpt-5.2
11
+
12
+ Strategy: case-fold + keep only alphanumeric characters.
13
+ """
14
+
15
+ return "".join(ch for ch in value.casefold() if ch.isalnum())
16
+
17
+
5
18
  def select_model_from_config(preferred: str | None = None) -> str | None:
6
19
  """
7
20
  Interactive single-choice model selector.
8
21
  for `--select-model`
22
+
23
+ If preferred is provided:
24
+ - Exact match: return immediately
25
+ - Single partial match (case-insensitive): return immediately
26
+ - Otherwise: fall through to interactive selection
9
27
  """
10
28
  config = load_config()
11
29
  assert config is not None
12
- models = sorted(config.model_list, key=lambda m: m.model_name.lower())
30
+ models: list[ModelConfig] = sorted(config.model_list, key=lambda m: m.model_name.lower())
13
31
 
14
32
  if not models:
15
33
  raise ValueError("No models configured. Please update your config.yaml")
16
34
 
17
35
  names: list[str] = [m.model_name for m in models]
18
36
 
37
+ # Try to match preferred model name
38
+ filtered_models = models
39
+ if preferred and preferred.strip():
40
+ preferred = preferred.strip()
41
+ # Exact match
42
+ if preferred in names:
43
+ return preferred
44
+
45
+ preferred_lower = preferred.lower()
46
+ # Case-insensitive exact match (model_name or model_params.model)
47
+ exact_ci_matches = [
48
+ m
49
+ for m in models
50
+ if preferred_lower == m.model_name.lower() or preferred_lower == (m.model_params.model or "").lower()
51
+ ]
52
+ if len(exact_ci_matches) == 1:
53
+ return exact_ci_matches[0].model_name
54
+
55
+ # Normalized matching (e.g. gpt52 == gpt-5.2, gpt52 in gpt-5.2-2025-...)
56
+ preferred_norm = _normalize_model_key(preferred)
57
+ normalized_matches: list[ModelConfig] = []
58
+ if preferred_norm:
59
+ normalized_matches = [
60
+ m
61
+ for m in models
62
+ if preferred_norm == _normalize_model_key(m.model_name)
63
+ or preferred_norm == _normalize_model_key(m.model_params.model or "")
64
+ ]
65
+ if len(normalized_matches) == 1:
66
+ return normalized_matches[0].model_name
67
+
68
+ if not normalized_matches and len(preferred_norm) >= 4:
69
+ normalized_matches = [
70
+ m
71
+ for m in models
72
+ if preferred_norm in _normalize_model_key(m.model_name)
73
+ or preferred_norm in _normalize_model_key(m.model_params.model or "")
74
+ ]
75
+ if len(normalized_matches) == 1:
76
+ return normalized_matches[0].model_name
77
+
78
+ # Partial match (case-insensitive) on model_name or model_params.model.
79
+ # If normalized matching found candidates (even if multiple), prefer those as the filter set.
80
+ matches = normalized_matches or [
81
+ m
82
+ for m in models
83
+ if preferred_lower in m.model_name.lower() or preferred_lower in (m.model_params.model or "").lower()
84
+ ]
85
+ if len(matches) == 1:
86
+ return matches[0].model_name
87
+ if matches:
88
+ # Multiple matches: filter the list for interactive selection
89
+ filtered_models = matches
90
+ else:
91
+ # No matches: show all models without filter hint
92
+ preferred = None
93
+
19
94
  try:
20
95
  import questionary
21
96
 
22
97
  choices: list[questionary.Choice] = []
23
98
 
24
- max_model_name_length = max(len(m.model_name) for m in models)
25
- for m in models:
99
+ max_model_name_length = max(len(m.model_name) for m in filtered_models)
100
+ for m in filtered_models:
26
101
  star = "★ " if m.model_name == config.main_model else " "
27
102
  title = f"{star}{m.model_name:<{max_model_name_length}} → {m.model_params.model or 'N/A'} @ {m.provider}"
28
103
  choices.append(questionary.Choice(title=title, value=m.model_name))
29
104
 
30
105
  try:
106
+ message = f"Select a model (filtered by '{preferred}'):" if preferred else "Select a model:"
31
107
  result = questionary.select(
32
- message="Select a model:",
108
+ message=message,
33
109
  choices=choices,
34
110
  pointer="→",
35
111
  instruction="↑↓ to move • Enter to select",
@@ -50,7 +50,7 @@ READ_CHAR_LIMIT_PER_LINE = 2000
50
50
  READ_GLOBAL_LINE_CAP = 2000
51
51
 
52
52
  # Maximum total characters to read
53
- READ_MAX_CHARS = 60000
53
+ READ_MAX_CHARS = 50000
54
54
 
55
55
  # Maximum file size in KB for text files
56
56
  READ_MAX_KB = 256
@@ -8,15 +8,19 @@ handling operations submitted from the CLI and coordinating with agents.
8
8
  from __future__ import annotations
9
9
 
10
10
  import asyncio
11
+ from collections.abc import Awaitable, Callable
11
12
  from dataclasses import dataclass
12
13
 
13
14
  from klaude_code.command import InputAction, InputActionType, dispatch_command
15
+ from klaude_code.config import load_config
14
16
  from klaude_code.core.agent import Agent, DefaultModelProfileProvider, ModelProfileProvider
15
- from klaude_code.core.manager import AgentManager, LLMClients, SubAgentManager
17
+ from klaude_code.core.manager import LLMClients, SubAgentManager
16
18
  from klaude_code.core.tool import current_run_subtask_callback
17
- from klaude_code.protocol import events, model, op
19
+ from klaude_code.llm.registry import create_llm_client
20
+ from klaude_code.protocol import commands, events, model, op
18
21
  from klaude_code.protocol.op_handler import OperationHandler
19
22
  from klaude_code.protocol.sub_agent import SubAgentResult
23
+ from klaude_code.session.session import Session
20
24
  from klaude_code.trace import DebugType, log_debug
21
25
 
22
26
 
@@ -72,6 +76,164 @@ class TaskManager:
72
76
  self._tasks.clear()
73
77
 
74
78
 
79
+ class InputActionExecutor:
80
+ """Execute input actions returned by the command dispatcher.
81
+
82
+ This helper encapsulates the logic for running the main agent task,
83
+ applying model changes, and clearing conversations so that
84
+ :class:`ExecutorContext` stays focused on operation dispatch.
85
+ """
86
+
87
+ def __init__(
88
+ self,
89
+ task_manager: TaskManager,
90
+ sub_agent_manager: SubAgentManager,
91
+ model_profile_provider: ModelProfileProvider,
92
+ emit_event: Callable[[events.Event], Awaitable[None]],
93
+ ) -> None:
94
+ self._task_manager = task_manager
95
+ self._sub_agent_manager = sub_agent_manager
96
+ self._model_profile_provider = model_profile_provider
97
+ self._emit_event = emit_event
98
+
99
+ async def run(self, action: InputAction, operation: op.UserInputOperation, agent: Agent) -> None:
100
+ """Dispatch and execute a single input action."""
101
+
102
+ if operation.session_id is None:
103
+ raise ValueError("session_id cannot be None for input actions")
104
+
105
+ session_id = operation.session_id
106
+
107
+ if action.type == InputActionType.RUN_AGENT:
108
+ await self._run_agent_action(action, operation, agent, session_id)
109
+ return
110
+
111
+ if action.type == InputActionType.CHANGE_MODEL:
112
+ if not action.model_name:
113
+ raise ValueError("ChangeModel action requires model_name")
114
+
115
+ await self._apply_model_change(agent, action.model_name)
116
+ return
117
+
118
+ if action.type == InputActionType.CLEAR:
119
+ await self._apply_clear(agent)
120
+ return
121
+
122
+ raise ValueError(f"Unsupported input action type: {action.type}")
123
+
124
+ async def _run_agent_action(
125
+ self,
126
+ action: InputAction,
127
+ operation: op.UserInputOperation,
128
+ agent: Agent,
129
+ session_id: str,
130
+ ) -> None:
131
+ task_input = model.UserInputPayload(text=action.text, images=operation.input.images)
132
+
133
+ existing_active = self._task_manager.get(operation.id)
134
+ if existing_active is not None and not existing_active.task.done():
135
+ raise RuntimeError(f"Active task already registered for operation {operation.id}")
136
+
137
+ task: asyncio.Task[None] = asyncio.create_task(
138
+ self._run_agent_task(agent, task_input, operation.id, session_id)
139
+ )
140
+ self._task_manager.register(operation.id, task, session_id)
141
+
142
+ async def _run_agent_task(
143
+ self,
144
+ agent: Agent,
145
+ user_input: model.UserInputPayload,
146
+ task_id: str,
147
+ session_id: str,
148
+ ) -> None:
149
+ """Run the main agent task and forward events to the UI."""
150
+
151
+ try:
152
+ log_debug(
153
+ f"Starting agent task {task_id} for session {session_id}",
154
+ style="green",
155
+ debug_type=DebugType.EXECUTION,
156
+ )
157
+
158
+ async def _runner(state: model.SubAgentState) -> SubAgentResult:
159
+ return await self._sub_agent_manager.run_sub_agent(agent, state)
160
+
161
+ token = current_run_subtask_callback.set(_runner)
162
+ try:
163
+ async for event in agent.run_task(user_input):
164
+ await self._emit_event(event)
165
+ finally:
166
+ current_run_subtask_callback.reset(token)
167
+
168
+ except asyncio.CancelledError:
169
+ log_debug(
170
+ f"Agent task {task_id} was cancelled",
171
+ style="yellow",
172
+ debug_type=DebugType.EXECUTION,
173
+ )
174
+ await self._emit_event(events.TaskFinishEvent(session_id=session_id, task_result="task cancelled"))
175
+
176
+ except Exception as e:
177
+ import traceback
178
+
179
+ log_debug(
180
+ f"Agent task {task_id} failed: {e!s}",
181
+ style="red",
182
+ debug_type=DebugType.EXECUTION,
183
+ )
184
+ log_debug(traceback.format_exc(), style="red", debug_type=DebugType.EXECUTION)
185
+ await self._emit_event(
186
+ events.ErrorEvent(
187
+ error_message=f"Agent task failed: [{e.__class__.__name__}] {e!s}",
188
+ can_retry=False,
189
+ )
190
+ )
191
+
192
+ finally:
193
+ self._task_manager.remove(task_id)
194
+ log_debug(
195
+ f"Cleaned up agent task {task_id}",
196
+ style="cyan",
197
+ debug_type=DebugType.EXECUTION,
198
+ )
199
+
200
+ async def _apply_model_change(self, agent: Agent, model_name: str) -> None:
201
+ """Change the model used by the active agent and notify the UI."""
202
+
203
+ config = load_config()
204
+ if config is None:
205
+ raise ValueError("Configuration must be initialized before changing model")
206
+
207
+ llm_config = config.get_model_config(model_name)
208
+ llm_client = create_llm_client(llm_config)
209
+ agent.set_model_profile(self._model_profile_provider.build_profile(llm_client))
210
+
211
+ developer_item = model.DeveloperMessageItem(
212
+ content=f"switched to model: {model_name}",
213
+ command_output=model.CommandOutput(command_name=commands.CommandName.MODEL),
214
+ )
215
+ agent.session.append_history([developer_item])
216
+
217
+ await self._emit_event(events.DeveloperMessageEvent(session_id=agent.session.id, item=developer_item))
218
+ await self._emit_event(events.WelcomeEvent(llm_config=llm_config, work_dir=str(agent.session.work_dir)))
219
+
220
+ async def _apply_clear(self, agent: Agent) -> None:
221
+ """Start a new conversation for the agent and notify the UI."""
222
+
223
+ new_session = Session(work_dir=agent.session.work_dir)
224
+ new_session.model_name = agent.session.model_name
225
+
226
+ agent.session = new_session
227
+ agent.session.save()
228
+
229
+ developer_item = model.DeveloperMessageItem(
230
+ content="started new conversation",
231
+ command_output=model.CommandOutput(command_name=commands.CommandName.CLEAR),
232
+ )
233
+
234
+ await self._emit_event(events.DeveloperMessageEvent(session_id=agent.session.id, item=developer_item))
235
+
236
+
75
237
  class ExecutorContext:
76
238
  """
77
239
  Context object providing shared state and operations for the executor.
@@ -89,32 +251,81 @@ class ExecutorContext:
89
251
  model_profile_provider: ModelProfileProvider | None = None,
90
252
  ):
91
253
  self.event_queue: asyncio.Queue[events.Event] = event_queue
254
+ self.llm_clients: LLMClients = llm_clients
92
255
 
93
256
  resolved_profile_provider = model_profile_provider or DefaultModelProfileProvider()
94
257
  self.model_profile_provider: ModelProfileProvider = resolved_profile_provider
95
258
 
96
- # Delegate responsibilities to helper components
97
- self.agent_manager = AgentManager(event_queue, llm_clients, resolved_profile_provider)
98
259
  self.task_manager = TaskManager()
99
260
  self.sub_agent_manager = SubAgentManager(event_queue, llm_clients, resolved_profile_provider)
261
+ self._action_executor = InputActionExecutor(
262
+ task_manager=self.task_manager,
263
+ sub_agent_manager=self.sub_agent_manager,
264
+ model_profile_provider=resolved_profile_provider,
265
+ emit_event=self.emit_event,
266
+ )
267
+ self._agent: Agent | None = None
100
268
 
101
269
  async def emit_event(self, event: events.Event) -> None:
102
270
  """Emit an event to the UI display system."""
103
271
  await self.event_queue.put(event)
104
272
 
273
+ def current_session_id(self) -> str | None:
274
+ """Return the primary active session id, if any.
275
+
276
+ This is a convenience wrapper used by the CLI, which conceptually
277
+ operates on a single interactive session per process.
278
+ """
279
+
280
+ agent = self._agent
281
+ if agent is None:
282
+ return None
283
+ return agent.session.id
284
+
105
285
  @property
106
- def active_agents(self) -> dict[str, Agent]:
107
- """Expose currently active agents keyed by session id.
286
+ def current_agent(self) -> Agent | None:
287
+ """Return the currently active agent, if any."""
288
+
289
+ return self._agent
108
290
 
109
- This property preserves the previous public attribute used by the
110
- CLI status provider while delegating storage to :class:`AgentManager`.
291
+ async def _ensure_agent(self, session_id: str | None = None) -> Agent:
292
+ """Return the active agent, creating or loading a session as needed.
293
+
294
+ If ``session_id`` is ``None``, a new session is created with an
295
+ auto-generated ID. If provided, the executor attempts to resume the
296
+ session from disk or creates a new one if not found.
111
297
  """
112
298
 
113
- return self.agent_manager.all_active_agents()
299
+ # Fast-path: reuse current agent when the session id already matches.
300
+ if session_id is not None and self._agent is not None and self._agent.session.id == session_id:
301
+ return self._agent
302
+
303
+ session = Session.create() if session_id is None else Session.load(session_id)
304
+
305
+ profile = self.model_profile_provider.build_profile(self.llm_clients.main)
306
+ agent = Agent(session=session, profile=profile)
307
+
308
+ async for evt in agent.replay_history():
309
+ await self.emit_event(evt)
310
+
311
+ await self.emit_event(
312
+ events.WelcomeEvent(
313
+ work_dir=str(session.work_dir),
314
+ llm_config=self.llm_clients.main.get_llm_config(),
315
+ )
316
+ )
317
+
318
+ self._agent = agent
319
+ log_debug(
320
+ f"Initialized agent for session: {session.id}",
321
+ style="cyan",
322
+ debug_type=DebugType.EXECUTION,
323
+ )
324
+ return agent
114
325
 
115
326
  async def handle_init_agent(self, operation: op.InitAgentOperation) -> None:
116
327
  """Initialize an agent for a session and replay history to UI."""
117
- await self.agent_manager.ensure_agent(operation.session_id)
328
+ await self._ensure_agent(operation.session_id)
118
329
 
119
330
  async def handle_user_input(self, operation: op.UserInputOperation) -> None:
120
331
  """Handle a user input operation by running it through an agent."""
@@ -123,7 +334,7 @@ class ExecutorContext:
123
334
  raise ValueError("session_id cannot be None")
124
335
 
125
336
  session_id = operation.session_id
126
- agent = await self.agent_manager.ensure_agent(session_id)
337
+ agent = await self._ensure_agent(session_id)
127
338
  user_input = operation.input
128
339
 
129
340
  # emit user input event
@@ -148,39 +359,7 @@ class ExecutorContext:
148
359
  await self.emit_event(evt)
149
360
 
150
361
  for action in actions:
151
- await self._run_input_action(action, operation, agent)
152
-
153
- async def _run_input_action(self, action: InputAction, operation: op.UserInputOperation, agent: Agent) -> None:
154
- if operation.session_id is None:
155
- raise ValueError("session_id cannot be None for input actions")
156
-
157
- session_id = operation.session_id
158
-
159
- if action.type == InputActionType.RUN_AGENT:
160
- task_input = model.UserInputPayload(text=action.text, images=operation.input.images)
161
-
162
- existing_active = self.task_manager.get(operation.id)
163
- if existing_active is not None and not existing_active.task.done():
164
- raise RuntimeError(f"Active task already registered for operation {operation.id}")
165
-
166
- task: asyncio.Task[None] = asyncio.create_task(
167
- self._run_agent_task(agent, task_input, operation.id, session_id)
168
- )
169
- self.task_manager.register(operation.id, task, session_id)
170
- return
171
-
172
- if action.type == InputActionType.CHANGE_MODEL:
173
- if not action.model_name:
174
- raise ValueError("ChangeModel action requires model_name")
175
-
176
- await self.agent_manager.apply_model_change(agent, action.model_name)
177
- return
178
-
179
- if action.type == InputActionType.CLEAR:
180
- await self.agent_manager.apply_clear(agent)
181
- return
182
-
183
- raise ValueError(f"Unsupported input action type: {action.type}")
362
+ await self._action_executor.run(action, operation, agent)
184
363
 
185
364
  async def handle_interrupt(self, operation: op.InterruptOperation) -> None:
186
365
  """Handle an interrupt by invoking agent.cancel() and cancelling tasks."""
@@ -189,11 +368,12 @@ class ExecutorContext:
189
368
  if operation.target_session_id is not None:
190
369
  session_ids: list[str] = [operation.target_session_id]
191
370
  else:
192
- session_ids = self.agent_manager.active_session_ids()
371
+ agent = self._agent
372
+ session_ids = [agent.session.id] if agent is not None else []
193
373
 
194
374
  # Call cancel() on each affected agent to persist an interrupt marker
195
375
  for sid in session_ids:
196
- agent = self.agent_manager.get_active_agent(sid)
376
+ agent = self._get_active_agent(sid)
197
377
  if agent is not None:
198
378
  for evt in agent.cancel():
199
379
  await self.emit_event(evt)
@@ -222,69 +402,6 @@ class ExecutorContext:
222
402
  # Remove from active tasks immediately
223
403
  self.task_manager.remove(task_id)
224
404
 
225
- async def _run_agent_task(
226
- self, agent: Agent, user_input: model.UserInputPayload, task_id: str, session_id: str
227
- ) -> None:
228
- """
229
- Run an agent task and forward all events to the UI.
230
-
231
- This method wraps the agent's run_task method and handles any exceptions
232
- that might occur during execution.
233
- """
234
- try:
235
- log_debug(
236
- f"Starting agent task {task_id} for session {session_id}",
237
- style="green",
238
- debug_type=DebugType.EXECUTION,
239
- )
240
-
241
- # Inject subtask runner into tool context for nested Task tool usage
242
- async def _runner(state: model.SubAgentState) -> SubAgentResult:
243
- return await self.sub_agent_manager.run_sub_agent(agent, state)
244
-
245
- token = current_run_subtask_callback.set(_runner)
246
- try:
247
- # Forward all events from the agent to the UI
248
- async for event in agent.run_task(user_input):
249
- await self.emit_event(event)
250
- finally:
251
- current_run_subtask_callback.reset(token)
252
-
253
- except asyncio.CancelledError:
254
- # Task was cancelled (likely due to interrupt)
255
- log_debug(
256
- f"Agent task {task_id} was cancelled",
257
- style="yellow",
258
- debug_type=DebugType.EXECUTION,
259
- )
260
- await self.emit_event(events.TaskFinishEvent(session_id=session_id, task_result="task cancelled"))
261
-
262
- except Exception as e:
263
- # Handle any other exceptions
264
- import traceback
265
-
266
- log_debug(
267
- f"Agent task {task_id} failed: {e!s}",
268
- style="red",
269
- debug_type=DebugType.EXECUTION,
270
- )
271
- log_debug(traceback.format_exc(), style="red", debug_type=DebugType.EXECUTION)
272
- await self.emit_event(
273
- events.ErrorEvent(
274
- error_message=f"Agent task failed: [{e.__class__.__name__}] {e!s}",
275
- can_retry=False,
276
- )
277
- )
278
-
279
- finally:
280
- # Clean up the task from active tasks
281
- self.task_manager.remove(task_id)
282
- log_debug(
283
- f"Cleaned up agent task {task_id}",
284
- style="cyan",
285
- debug_type=DebugType.EXECUTION,
286
- )
287
-
288
405
  def get_active_task(self, submission_id: str) -> asyncio.Task[None] | None:
289
406
  """Return the asyncio.Task for a submission id if one is registered."""
290
407
 
@@ -298,6 +415,16 @@ class ExecutorContext:
298
415
 
299
416
  return self.task_manager.get(submission_id) is not None
300
417
 
418
+ def _get_active_agent(self, session_id: str) -> Agent | None:
419
+ """Return the active agent if its session id matches ``session_id``."""
420
+
421
+ agent = self._agent
422
+ if agent is None:
423
+ return None
424
+ if agent.session.id != session_id:
425
+ return None
426
+ return agent
427
+
301
428
 
302
429
  class Executor:
303
430
  """
@@ -1,18 +1,16 @@
1
1
  """Core runtime and state management components.
2
2
 
3
3
  Expose the manager layer via package imports to reduce module churn in
4
- callers. This keeps long-lived runtime state helpers (agents, tasks,
5
- LLM clients, sub-agents) distinct from per-session execution logic in
4
+ callers. This keeps long-lived runtime state helpers (LLM clients and
5
+ sub-agents) distinct from per-session execution logic in
6
6
  ``klaude_code.core``.
7
7
  """
8
8
 
9
- from klaude_code.core.manager.agent_manager import AgentManager
10
9
  from klaude_code.core.manager.llm_clients import LLMClients
11
10
  from klaude_code.core.manager.llm_clients_builder import build_llm_clients
12
11
  from klaude_code.core.manager.sub_agent_manager import SubAgentManager
13
12
 
14
13
  __all__ = [
15
- "AgentManager",
16
14
  "LLMClients",
17
15
  "SubAgentManager",
18
16
  "build_llm_clients",
@@ -7,7 +7,7 @@ You are an interactive CLI tool that helps users with software engineering tasks
7
7
  - NEVER create files unless they're absolutely necessary for achieving your goal. ALWAYS prefer editing an existing file to creating a new one. This includes markdown files.
8
8
 
9
9
  ## Professional objectivity
10
- Prioritize technical accuracy and truthfulness over validating the user's beliefs. Focus on facts and problem-solving, providing direct, objective technical info without any unnecessary superlatives, praise, or emotional validation. It is best for the user if Claude honestly applies the same rigorous standards to all ideas and disagrees when necessary, even if it may not be what the user wants to hear. Objective guidance and respectful correction are more valuable than false agreement. Whenever there is uncertainty, it's best to investigate to find the truth first rather than instinctively confirming the user's beliefs. Avoid using over-the-top validation or excessive praise when responding to users such as "You're absolutely right" or similar phrases.
10
+ Prioritize technical accuracy and truthfulness over validating the user's beliefs. Focus on facts and problem-solving, providing direct, objective technical info without any unnecessary superlatives, praise, or emotional validation. It is best for the user if you honestly applies the same rigorous standards to all ideas and disagrees when necessary, even if it may not be what the user wants to hear. Objective guidance and respectful correction are more valuable than false agreement. Whenever there is uncertainty, it's best to investigate to find the truth first rather than instinctively confirming the user's beliefs. Avoid using over-the-top validation or excessive praise when responding to users such as "You're absolutely right" or similar phrases.
11
11
 
12
12
  ## Planning without timelines
13
13
  When planning tasks, provide concrete implementation steps without time estimates. Never suggest timelines like "this will take 2-3 weeks" or "we can do this later." Focus on what needs to be done, not when. Break work into actionable steps and let users decide scheduling.
@@ -11,6 +11,7 @@ You are a web research agent that searches and fetches web content to provide up
11
11
  - HTML pages are automatically converted to Markdown
12
12
  - JSON responses are auto-formatted with indentation
13
13
  - Other text content returned as-is
14
+ - **Content is always saved to a local file** - check `<file_saved>` tag for the path
14
15
 
15
16
  ## Tool Usage Strategy
16
17
 
@@ -26,7 +27,7 @@ Balance efficiency with thoroughness. For open-ended questions (e.g., "recommend
26
27
  - Keep queries concise (1-6 words). Start broad, then narrow if needed
27
28
  - Avoid repeating similar queries - they won't yield new results
28
29
  - NEVER use '-', 'site:', or quotes unless explicitly asked
29
- - Include year/date for time-sensitive queries (check "Today's date" in <env>)
30
+ - Include year/date for time-sensitive queries (check "Today's date" in <env>), don't limit yourself to your knowledge cutoff date
30
31
  - Use WebFetch to get full content - search snippets are often insufficient
31
32
  - Follow relevant links on pages with WebFetch
32
33
  - If truncated results are saved to local files, use grep/read to explore
@@ -34,15 +35,17 @@ Balance efficiency with thoroughness. For open-ended questions (e.g., "recommend
34
35
  ## Response Guidelines
35
36
 
36
37
  - Only your last message is returned to the main agent
37
- - Be succinct - include only relevant information
38
+ - **DO NOT copy full web page content** - the main agent can read the saved files directly
39
+ - Provide a concise summary/analysis of key findings
40
+ - Include the file path from `<file_saved>` so the main agent can access full content if needed
38
41
  - Lead with the most recent info for evolving topics
39
42
  - Favor original sources (company blogs, papers, gov sites) over aggregators
40
43
  - Note conflicting sources when they exist
41
44
 
42
45
  ## Sources (REQUIRED)
43
46
 
44
- You MUST end every response with a "Sources:" section listing all URLs as markdown links:
47
+ You MUST end every response with a "Sources:" section listing all URLs with their saved file paths:
45
48
 
46
49
  Sources:
47
- - [Source Title](https://example.com)
48
- - [Another Source](https://example.com/page) (saved: /path/to/file)
50
+ - [Source Title](https://example.com) -> /tmp/klaude/web/example_com-123456.md
51
+ - [Another Source](https://example.com/page) -> /tmp/klaude/web/example_com_page-123456.md