klaude-code 1.2.16__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 (70) 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 +20 -13
  5. klaude_code/command/__init__.py +7 -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 +145 -0
  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/config.py +16 -17
  22. klaude_code/config/select_model.py +81 -5
  23. klaude_code/const/__init__.py +1 -1
  24. klaude_code/core/executor.py +236 -109
  25. klaude_code/core/manager/__init__.py +2 -4
  26. klaude_code/core/manager/sub_agent_manager.py +1 -1
  27. klaude_code/core/prompts/prompt-claude-code.md +1 -1
  28. klaude_code/core/prompts/prompt-sub-agent-oracle.md +0 -1
  29. klaude_code/core/prompts/prompt-sub-agent-web.md +51 -0
  30. klaude_code/core/reminders.py +9 -35
  31. klaude_code/core/task.py +8 -0
  32. klaude_code/core/tool/__init__.py +2 -0
  33. klaude_code/core/tool/file/read_tool.py +38 -10
  34. klaude_code/core/tool/report_back_tool.py +28 -2
  35. klaude_code/core/tool/shell/bash_tool.py +22 -2
  36. klaude_code/core/tool/tool_runner.py +26 -23
  37. klaude_code/core/tool/truncation.py +23 -9
  38. klaude_code/core/tool/web/web_fetch_tool.md +1 -1
  39. klaude_code/core/tool/web/web_fetch_tool.py +36 -1
  40. klaude_code/core/tool/web/web_search_tool.md +23 -0
  41. klaude_code/core/tool/web/web_search_tool.py +126 -0
  42. klaude_code/core/turn.py +28 -0
  43. klaude_code/protocol/commands.py +2 -0
  44. klaude_code/protocol/events.py +8 -0
  45. klaude_code/protocol/sub_agent/__init__.py +1 -1
  46. klaude_code/protocol/sub_agent/explore.py +1 -1
  47. klaude_code/protocol/sub_agent/web.py +79 -0
  48. klaude_code/protocol/tools.py +1 -0
  49. klaude_code/session/session.py +2 -2
  50. klaude_code/session/templates/export_session.html +123 -37
  51. klaude_code/trace/__init__.py +20 -2
  52. klaude_code/ui/modes/repl/completers.py +19 -2
  53. klaude_code/ui/modes/repl/event_handler.py +44 -15
  54. klaude_code/ui/modes/repl/renderer.py +3 -3
  55. klaude_code/ui/renderers/metadata.py +2 -4
  56. klaude_code/ui/renderers/sub_agent.py +14 -10
  57. klaude_code/ui/renderers/thinking.py +24 -8
  58. klaude_code/ui/renderers/tools.py +83 -20
  59. klaude_code/ui/rich/code_panel.py +112 -0
  60. klaude_code/ui/rich/markdown.py +3 -4
  61. klaude_code/ui/rich/status.py +30 -6
  62. klaude_code/ui/rich/theme.py +10 -1
  63. {klaude_code-1.2.16.dist-info → klaude_code-1.2.18.dist-info}/METADATA +126 -25
  64. {klaude_code-1.2.16.dist-info → klaude_code-1.2.18.dist-info}/RECORD +67 -63
  65. klaude_code/core/manager/agent_manager.py +0 -132
  66. klaude_code/core/prompts/prompt-sub-agent-webfetch.md +0 -46
  67. klaude_code/protocol/sub_agent/web_fetch.py +0 -74
  68. /klaude_code/{config → cli}/list_model.py +0 -0
  69. {klaude_code-1.2.16.dist-info → klaude_code-1.2.18.dist-info}/WHEEL +0 -0
  70. {klaude_code-1.2.16.dist-info → klaude_code-1.2.18.dist-info}/entry_points.txt +0 -0
@@ -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",
@@ -57,7 +57,7 @@ class SubAgentManager:
57
57
  # Structured Output
58
58
  You have a `report_back` tool available. When you complete the task,\
59
59
  you MUST call `report_back` with the structured result matching the required schema.\
60
- This will end the task and return the structured data to the caller.
60
+ Only the content passed to `report_back` will be returned to user.\
61
61
  """
62
62
  base_prompt = child_profile.system_prompt or ""
63
63
  child_profile = AgentProfile(
@@ -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.
@@ -3,7 +3,6 @@ You are the Oracle - an expert AI advisor with advanced reasoning capabilities
3
3
  Your role is to provide high-quality technical guidance, code reviews, architectural advice, and strategic planning for software engineering tasks.
4
4
  You are running inside an AI coding system in which you act as a sub-agent that's used when the main agent needs a smarter, more capable model to help out.
5
5
 
6
-
7
6
  Key responsibilities:
8
7
  - Analyze code and architecture patterns
9
8
  - Provide detailed technical reviews and recommendations
@@ -0,0 +1,51 @@
1
+ You are a web research agent that searches and fetches web content to provide up-to-date information.
2
+
3
+ ## Available Tools
4
+
5
+ **WebSearch**: Search the web via DuckDuckGo
6
+ - Returns: title, URL, and snippet for each result
7
+ - Parameter `max_results`: control result count (default: 10, max: 20)
8
+ - Snippets are brief summaries - use WebFetch for full content
9
+
10
+ **WebFetch**: Fetch and process web page content
11
+ - HTML pages are automatically converted to Markdown
12
+ - JSON responses are auto-formatted with indentation
13
+ - Other text content returned as-is
14
+ - **Content is always saved to a local file** - check `<file_saved>` tag for the path
15
+
16
+ ## Tool Usage Strategy
17
+
18
+ Scale tool calls to query complexity:
19
+ - Simple facts: 1-2 calls
20
+ - Medium research: 3-5 calls
21
+ - Deep research/comparisons: 5-10 calls
22
+
23
+ Balance efficiency with thoroughness. For open-ended questions (e.g., "recommendations for video games" or "recent developments in RL"), use more calls for comprehensive answers.
24
+
25
+ ## Search Guidelines
26
+
27
+ - Keep queries concise (1-6 words). Start broad, then narrow if needed
28
+ - Avoid repeating similar queries - they won't yield new results
29
+ - NEVER use '-', 'site:', or quotes unless explicitly asked
30
+ - Include year/date for time-sensitive queries (check "Today's date" in <env>), don't limit yourself to your knowledge cutoff date
31
+ - Use WebFetch to get full content - search snippets are often insufficient
32
+ - Follow relevant links on pages with WebFetch
33
+ - If truncated results are saved to local files, use grep/read to explore
34
+
35
+ ## Response Guidelines
36
+
37
+ - Only your last message is returned to the main agent
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
41
+ - Lead with the most recent info for evolving topics
42
+ - Favor original sources (company blogs, papers, gov sites) over aggregators
43
+ - Note conflicting sources when they exist
44
+
45
+ ## Sources (REQUIRED)
46
+
47
+ You MUST end every response with a "Sources:" section listing all URLs with their saved file paths:
48
+
49
+ Sources:
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
@@ -1,4 +1,3 @@
1
- import json
2
1
  import re
3
2
  import shlex
4
3
  from collections.abc import Awaitable, Callable
@@ -282,7 +281,6 @@ def get_memory_paths() -> list[tuple[Path, str]]:
282
281
  "user's private global instructions for all projects",
283
282
  ),
284
283
  (Path.cwd() / "AGENTS.md", "project instructions, checked into the codebase"),
285
- (Path.cwd() / "AGENT.md", "project instructions, checked into the codebase"),
286
284
  (Path.cwd() / "CLAUDE.md", "project instructions, checked into the codebase"),
287
285
  ]
288
286
 
@@ -351,46 +349,22 @@ IMPORTANT: this context may or may not be relevant to your tasks. You should not
351
349
  return None
352
350
 
353
351
 
354
- def get_last_turn_tool_call(session: Session) -> list[model.ToolCallItem]:
355
- tool_calls: list[model.ToolCallItem] = []
356
- for item in reversed(session.conversation_history):
357
- if isinstance(item, model.ToolCallItem):
358
- tool_calls.append(item)
359
- if isinstance(
360
- item,
361
- (
362
- model.ReasoningEncryptedItem,
363
- model.ReasoningTextItem,
364
- model.AssistantMessageItem,
365
- ),
366
- ):
367
- break
368
- return tool_calls
369
-
370
-
371
352
  MEMORY_FILE_NAMES = ["CLAUDE.md", "AGENTS.md", "AGENT.md"]
372
353
 
373
354
 
374
355
  async def last_path_memory_reminder(
375
356
  session: Session,
376
357
  ) -> model.DeveloperMessageItem | None:
377
- """When last turn tool call entered a directory (or parent directory) with CLAUDE.md AGENTS.md"""
378
- tool_calls = get_last_turn_tool_call(session)
379
- if len(tool_calls) == 0:
358
+ """Load CLAUDE.md/AGENTS.md from directories containing files in file_tracker.
359
+
360
+ Uses session.file_tracker to detect accessed paths (works for both tool calls
361
+ and @ file references). Uses session.loaded_memory to avoid duplicate loading.
362
+ """
363
+ if not session.file_tracker:
380
364
  return None
381
- paths: list[str] = []
382
- for tool_call in tool_calls:
383
- if tool_call.name in (tools.READ, tools.EDIT, tools.MULTI_EDIT, tools.WRITE):
384
- try:
385
- json_dict = json.loads(tool_call.arguments)
386
- if path := json_dict.get("file_path", ""):
387
- paths.append(path)
388
- except json.JSONDecodeError:
389
- continue
390
- paths = list(set(paths))
365
+
366
+ paths = list(session.file_tracker.keys())
391
367
  memories: list[Memory] = []
392
- if len(paths) == 0:
393
- return None
394
368
 
395
369
  cwd = Path.cwd().resolve()
396
370
  loaded_set: set[str] = set(session.loaded_memory)
@@ -484,8 +458,8 @@ def load_agent_reminders(
484
458
  reminders.extend(
485
459
  [
486
460
  memory_reminder,
487
- last_path_memory_reminder,
488
461
  at_file_reader_reminder,
462
+ last_path_memory_reminder,
489
463
  file_changed_externally_reminder,
490
464
  image_reminder,
491
465
  ]
klaude_code/core/task.py CHANGED
@@ -182,6 +182,14 @@ class TaskExecutor:
182
182
  yield am
183
183
  case events.ResponseMetadataEvent() as e:
184
184
  metadata_accumulator.add(e.metadata)
185
+ # Emit context usage event if available
186
+ if e.metadata.usage is not None:
187
+ context_percent = e.metadata.usage.context_usage_percent
188
+ if context_percent is not None:
189
+ yield events.ContextUsageEvent(
190
+ session_id=session_ctx.session_id,
191
+ context_percent=context_percent,
192
+ )
185
193
  case events.ToolResultEvent() as e:
186
194
  # Collect sub-agent task metadata from tool results
187
195
  if e.task_metadata is not None:
@@ -28,6 +28,7 @@ from .tool_runner import run_tool
28
28
  from .truncation import SimpleTruncationStrategy, TruncationStrategy, get_truncation_strategy, set_truncation_strategy
29
29
  from .web.mermaid_tool import MermaidTool
30
30
  from .web.web_fetch_tool import WebFetchTool
31
+ from .web.web_search_tool import WebSearchTool
31
32
 
32
33
  __all__ = [
33
34
  "MEMORY_DIR_NAME",
@@ -53,6 +54,7 @@ __all__ = [
53
54
  "TruncationStrategy",
54
55
  "UpdatePlanTool",
55
56
  "WebFetchTool",
57
+ "WebSearchTool",
56
58
  "WriteTool",
57
59
  "build_todo_context",
58
60
  "current_run_subtask_callback",