klaude-code 1.2.17__py3-none-any.whl → 1.2.19__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 +45 -31
  4. klaude_code/cli/runtime.py +49 -13
  5. klaude_code/{version.py → cli/self_update.py} +110 -2
  6. klaude_code/command/__init__.py +4 -1
  7. klaude_code/command/clear_cmd.py +2 -7
  8. klaude_code/command/command_abc.py +33 -5
  9. klaude_code/command/debug_cmd.py +79 -0
  10. klaude_code/command/diff_cmd.py +2 -6
  11. klaude_code/command/export_cmd.py +7 -7
  12. klaude_code/command/export_online_cmd.py +9 -8
  13. klaude_code/command/help_cmd.py +4 -9
  14. klaude_code/command/model_cmd.py +10 -6
  15. klaude_code/command/prompt_command.py +2 -6
  16. klaude_code/command/refresh_cmd.py +2 -7
  17. klaude_code/command/registry.py +69 -26
  18. klaude_code/command/release_notes_cmd.py +2 -6
  19. klaude_code/command/status_cmd.py +2 -7
  20. klaude_code/command/terminal_setup_cmd.py +2 -6
  21. klaude_code/command/thinking_cmd.py +16 -10
  22. klaude_code/config/select_model.py +81 -5
  23. klaude_code/const/__init__.py +1 -1
  24. klaude_code/core/executor.py +257 -110
  25. klaude_code/core/manager/__init__.py +2 -4
  26. klaude_code/core/prompts/prompt-claude-code.md +1 -1
  27. klaude_code/core/prompts/prompt-sub-agent-explore.md +14 -2
  28. klaude_code/core/prompts/prompt-sub-agent-web.md +8 -5
  29. klaude_code/core/reminders.py +9 -35
  30. klaude_code/core/task.py +9 -7
  31. klaude_code/core/tool/file/read_tool.md +1 -1
  32. klaude_code/core/tool/file/read_tool.py +41 -12
  33. klaude_code/core/tool/memory/skill_loader.py +12 -10
  34. klaude_code/core/tool/shell/bash_tool.py +22 -2
  35. klaude_code/core/tool/tool_registry.py +1 -1
  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/turn.py +28 -0
  41. klaude_code/llm/anthropic/client.py +25 -9
  42. klaude_code/llm/openai_compatible/client.py +5 -2
  43. klaude_code/llm/openrouter/client.py +7 -3
  44. klaude_code/llm/responses/client.py +6 -1
  45. klaude_code/protocol/commands.py +1 -0
  46. klaude_code/protocol/sub_agent/web.py +3 -2
  47. klaude_code/session/session.py +35 -15
  48. klaude_code/session/templates/export_session.html +45 -32
  49. klaude_code/trace/__init__.py +20 -2
  50. klaude_code/ui/modes/repl/completers.py +231 -73
  51. klaude_code/ui/modes/repl/event_handler.py +8 -6
  52. klaude_code/ui/modes/repl/input_prompt_toolkit.py +1 -1
  53. klaude_code/ui/modes/repl/renderer.py +2 -2
  54. klaude_code/ui/renderers/common.py +54 -0
  55. klaude_code/ui/renderers/developer.py +2 -3
  56. klaude_code/ui/renderers/errors.py +1 -1
  57. klaude_code/ui/renderers/metadata.py +12 -5
  58. klaude_code/ui/renderers/thinking.py +24 -8
  59. klaude_code/ui/renderers/tools.py +82 -14
  60. klaude_code/ui/rich/code_panel.py +112 -0
  61. klaude_code/ui/rich/markdown.py +3 -4
  62. klaude_code/ui/rich/status.py +0 -2
  63. klaude_code/ui/rich/theme.py +10 -1
  64. klaude_code/ui/utils/common.py +0 -18
  65. {klaude_code-1.2.17.dist-info → klaude_code-1.2.19.dist-info}/METADATA +32 -7
  66. {klaude_code-1.2.17.dist-info → klaude_code-1.2.19.dist-info}/RECORD +69 -68
  67. klaude_code/core/manager/agent_manager.py +0 -132
  68. /klaude_code/{config → cli}/list_model.py +0 -0
  69. {klaude_code-1.2.17.dist-info → klaude_code-1.2.19.dist-info}/WHEEL +0 -0
  70. {klaude_code-1.2.17.dist-info → klaude_code-1.2.19.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,174 @@ 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
+ on_model_change: Callable[[str], None] | None = None,
94
+ ) -> None:
95
+ self._task_manager = task_manager
96
+ self._sub_agent_manager = sub_agent_manager
97
+ self._model_profile_provider = model_profile_provider
98
+ self._emit_event = emit_event
99
+ self._on_model_change = on_model_change
100
+
101
+ async def run(self, action: InputAction, operation: op.UserInputOperation, agent: Agent) -> None:
102
+ """Dispatch and execute a single input action."""
103
+
104
+ if operation.session_id is None:
105
+ raise ValueError("session_id cannot be None for input actions")
106
+
107
+ session_id = operation.session_id
108
+
109
+ if action.type == InputActionType.RUN_AGENT:
110
+ await self._run_agent_action(action, operation, agent, session_id)
111
+ return
112
+
113
+ if action.type == InputActionType.CHANGE_MODEL:
114
+ if not action.model_name:
115
+ raise ValueError("ChangeModel action requires model_name")
116
+
117
+ await self._apply_model_change(agent, action.model_name)
118
+ return
119
+
120
+ if action.type == InputActionType.CLEAR:
121
+ await self._apply_clear(agent)
122
+ return
123
+
124
+ raise ValueError(f"Unsupported input action type: {action.type}")
125
+
126
+ async def _run_agent_action(
127
+ self,
128
+ action: InputAction,
129
+ operation: op.UserInputOperation,
130
+ agent: Agent,
131
+ session_id: str,
132
+ ) -> None:
133
+ task_input = model.UserInputPayload(text=action.text, images=operation.input.images)
134
+
135
+ existing_active = self._task_manager.get(operation.id)
136
+ if existing_active is not None and not existing_active.task.done():
137
+ raise RuntimeError(f"Active task already registered for operation {operation.id}")
138
+
139
+ task: asyncio.Task[None] = asyncio.create_task(
140
+ self._run_agent_task(agent, task_input, operation.id, session_id)
141
+ )
142
+ self._task_manager.register(operation.id, task, session_id)
143
+
144
+ async def _run_agent_task(
145
+ self,
146
+ agent: Agent,
147
+ user_input: model.UserInputPayload,
148
+ task_id: str,
149
+ session_id: str,
150
+ ) -> None:
151
+ """Run the main agent task and forward events to the UI."""
152
+
153
+ try:
154
+ log_debug(
155
+ f"Starting agent task {task_id} for session {session_id}",
156
+ style="green",
157
+ debug_type=DebugType.EXECUTION,
158
+ )
159
+
160
+ async def _runner(state: model.SubAgentState) -> SubAgentResult:
161
+ return await self._sub_agent_manager.run_sub_agent(agent, state)
162
+
163
+ token = current_run_subtask_callback.set(_runner)
164
+ try:
165
+ async for event in agent.run_task(user_input):
166
+ await self._emit_event(event)
167
+ finally:
168
+ current_run_subtask_callback.reset(token)
169
+
170
+ except asyncio.CancelledError:
171
+ log_debug(
172
+ f"Agent task {task_id} was cancelled",
173
+ style="yellow",
174
+ debug_type=DebugType.EXECUTION,
175
+ )
176
+ await self._emit_event(events.TaskFinishEvent(session_id=session_id, task_result="task cancelled"))
177
+
178
+ except Exception as e:
179
+ import traceback
180
+
181
+ log_debug(
182
+ f"Agent task {task_id} failed: {e!s}",
183
+ style="red",
184
+ debug_type=DebugType.EXECUTION,
185
+ )
186
+ log_debug(traceback.format_exc(), style="red", debug_type=DebugType.EXECUTION)
187
+ await self._emit_event(
188
+ events.ErrorEvent(
189
+ error_message=f"Agent task failed: [{e.__class__.__name__}] {e!s}",
190
+ can_retry=False,
191
+ )
192
+ )
193
+
194
+ finally:
195
+ self._task_manager.remove(task_id)
196
+ log_debug(
197
+ f"Cleaned up agent task {task_id}",
198
+ style="cyan",
199
+ debug_type=DebugType.EXECUTION,
200
+ )
201
+
202
+ async def _apply_model_change(self, agent: Agent, model_name: str) -> None:
203
+ """Change the model used by the active agent and notify the UI."""
204
+
205
+ config = load_config()
206
+ if config is None:
207
+ raise ValueError("Configuration must be initialized before changing model")
208
+
209
+ llm_config = config.get_model_config(model_name)
210
+ llm_client = create_llm_client(llm_config)
211
+ agent.set_model_profile(self._model_profile_provider.build_profile(llm_client))
212
+
213
+ agent.session.model_config_name = model_name
214
+ agent.session.model_thinking = llm_config.thinking
215
+
216
+ developer_item = model.DeveloperMessageItem(
217
+ content=f"switched to model: {model_name}",
218
+ command_output=model.CommandOutput(command_name=commands.CommandName.MODEL),
219
+ )
220
+ agent.session.append_history([developer_item])
221
+
222
+ await self._emit_event(events.DeveloperMessageEvent(session_id=agent.session.id, item=developer_item))
223
+ await self._emit_event(events.WelcomeEvent(llm_config=llm_config, work_dir=str(agent.session.work_dir)))
224
+
225
+ if self._on_model_change is not None:
226
+ self._on_model_change(llm_client.model_name)
227
+
228
+ async def _apply_clear(self, agent: Agent) -> None:
229
+ """Start a new conversation for the agent and notify the UI."""
230
+
231
+ new_session = Session(work_dir=agent.session.work_dir)
232
+ new_session.model_name = agent.session.model_name
233
+ new_session.model_config_name = agent.session.model_config_name
234
+ new_session.model_thinking = agent.session.model_thinking
235
+
236
+ agent.session = new_session
237
+ agent.session.save()
238
+
239
+ developer_item = model.DeveloperMessageItem(
240
+ content="started new conversation",
241
+ command_output=model.CommandOutput(command_name=commands.CommandName.CLEAR),
242
+ )
243
+
244
+ await self._emit_event(events.DeveloperMessageEvent(session_id=agent.session.id, item=developer_item))
245
+
246
+
75
247
  class ExecutorContext:
76
248
  """
77
249
  Context object providing shared state and operations for the executor.
@@ -87,34 +259,92 @@ class ExecutorContext:
87
259
  event_queue: asyncio.Queue[events.Event],
88
260
  llm_clients: LLMClients,
89
261
  model_profile_provider: ModelProfileProvider | None = None,
262
+ on_model_change: Callable[[str], None] | None = None,
90
263
  ):
91
264
  self.event_queue: asyncio.Queue[events.Event] = event_queue
265
+ self.llm_clients: LLMClients = llm_clients
92
266
 
93
267
  resolved_profile_provider = model_profile_provider or DefaultModelProfileProvider()
94
268
  self.model_profile_provider: ModelProfileProvider = resolved_profile_provider
95
269
 
96
- # Delegate responsibilities to helper components
97
- self.agent_manager = AgentManager(event_queue, llm_clients, resolved_profile_provider)
98
270
  self.task_manager = TaskManager()
99
271
  self.sub_agent_manager = SubAgentManager(event_queue, llm_clients, resolved_profile_provider)
272
+ self._action_executor = InputActionExecutor(
273
+ task_manager=self.task_manager,
274
+ sub_agent_manager=self.sub_agent_manager,
275
+ model_profile_provider=resolved_profile_provider,
276
+ emit_event=self.emit_event,
277
+ on_model_change=on_model_change,
278
+ )
279
+ self._agent: Agent | None = None
100
280
 
101
281
  async def emit_event(self, event: events.Event) -> None:
102
282
  """Emit an event to the UI display system."""
103
283
  await self.event_queue.put(event)
104
284
 
285
+ def current_session_id(self) -> str | None:
286
+ """Return the primary active session id, if any.
287
+
288
+ This is a convenience wrapper used by the CLI, which conceptually
289
+ operates on a single interactive session per process.
290
+ """
291
+
292
+ agent = self._agent
293
+ if agent is None:
294
+ return None
295
+ return agent.session.id
296
+
105
297
  @property
106
- def active_agents(self) -> dict[str, Agent]:
107
- """Expose currently active agents keyed by session id.
298
+ def current_agent(self) -> Agent | None:
299
+ """Return the currently active agent, if any."""
108
300
 
109
- This property preserves the previous public attribute used by the
110
- CLI status provider while delegating storage to :class:`AgentManager`.
301
+ return self._agent
302
+
303
+ async def _ensure_agent(self, session_id: str | None = None) -> Agent:
304
+ """Return the active agent, creating or loading a session as needed.
305
+
306
+ If ``session_id`` is ``None``, a new session is created with an
307
+ auto-generated ID. If provided, the executor attempts to resume the
308
+ session from disk or creates a new one if not found.
111
309
  """
112
310
 
113
- return self.agent_manager.all_active_agents()
311
+ # Fast-path: reuse current agent when the session id already matches.
312
+ if session_id is not None and self._agent is not None and self._agent.session.id == session_id:
313
+ return self._agent
314
+
315
+ session = Session.create() if session_id is None else Session.load(session_id)
316
+
317
+ if (
318
+ session.model_thinking is not None
319
+ and session.model_name
320
+ and session.model_name == self.llm_clients.main.model_name
321
+ ):
322
+ self.llm_clients.main.get_llm_config().thinking = session.model_thinking
323
+
324
+ profile = self.model_profile_provider.build_profile(self.llm_clients.main)
325
+ agent = Agent(session=session, profile=profile)
326
+
327
+ async for evt in agent.replay_history():
328
+ await self.emit_event(evt)
329
+
330
+ await self.emit_event(
331
+ events.WelcomeEvent(
332
+ work_dir=str(session.work_dir),
333
+ llm_config=self.llm_clients.main.get_llm_config(),
334
+ )
335
+ )
336
+
337
+ self._agent = agent
338
+ log_debug(
339
+ f"Initialized agent for session: {session.id}",
340
+ style="cyan",
341
+ debug_type=DebugType.EXECUTION,
342
+ )
343
+ return agent
114
344
 
115
345
  async def handle_init_agent(self, operation: op.InitAgentOperation) -> None:
116
346
  """Initialize an agent for a session and replay history to UI."""
117
- await self.agent_manager.ensure_agent(operation.session_id)
347
+ await self._ensure_agent(operation.session_id)
118
348
 
119
349
  async def handle_user_input(self, operation: op.UserInputOperation) -> None:
120
350
  """Handle a user input operation by running it through an agent."""
@@ -123,7 +353,7 @@ class ExecutorContext:
123
353
  raise ValueError("session_id cannot be None")
124
354
 
125
355
  session_id = operation.session_id
126
- agent = await self.agent_manager.ensure_agent(session_id)
356
+ agent = await self._ensure_agent(session_id)
127
357
  user_input = operation.input
128
358
 
129
359
  # emit user input event
@@ -148,39 +378,7 @@ class ExecutorContext:
148
378
  await self.emit_event(evt)
149
379
 
150
380
  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}")
381
+ await self._action_executor.run(action, operation, agent)
184
382
 
185
383
  async def handle_interrupt(self, operation: op.InterruptOperation) -> None:
186
384
  """Handle an interrupt by invoking agent.cancel() and cancelling tasks."""
@@ -189,11 +387,12 @@ class ExecutorContext:
189
387
  if operation.target_session_id is not None:
190
388
  session_ids: list[str] = [operation.target_session_id]
191
389
  else:
192
- session_ids = self.agent_manager.active_session_ids()
390
+ agent = self._agent
391
+ session_ids = [agent.session.id] if agent is not None else []
193
392
 
194
393
  # Call cancel() on each affected agent to persist an interrupt marker
195
394
  for sid in session_ids:
196
- agent = self.agent_manager.get_active_agent(sid)
395
+ agent = self._get_active_agent(sid)
197
396
  if agent is not None:
198
397
  for evt in agent.cancel():
199
398
  await self.emit_event(evt)
@@ -222,69 +421,6 @@ class ExecutorContext:
222
421
  # Remove from active tasks immediately
223
422
  self.task_manager.remove(task_id)
224
423
 
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
424
  def get_active_task(self, submission_id: str) -> asyncio.Task[None] | None:
289
425
  """Return the asyncio.Task for a submission id if one is registered."""
290
426
 
@@ -298,6 +434,16 @@ class ExecutorContext:
298
434
 
299
435
  return self.task_manager.get(submission_id) is not None
300
436
 
437
+ def _get_active_agent(self, session_id: str) -> Agent | None:
438
+ """Return the active agent if its session id matches ``session_id``."""
439
+
440
+ agent = self._agent
441
+ if agent is None:
442
+ return None
443
+ if agent.session.id != session_id:
444
+ return None
445
+ return agent
446
+
301
447
 
302
448
  class Executor:
303
449
  """
@@ -312,8 +458,9 @@ class Executor:
312
458
  event_queue: asyncio.Queue[events.Event],
313
459
  llm_clients: LLMClients,
314
460
  model_profile_provider: ModelProfileProvider | None = None,
461
+ on_model_change: Callable[[str], None] | None = None,
315
462
  ):
316
- self.context = ExecutorContext(event_queue, llm_clients, model_profile_provider)
463
+ self.context = ExecutorContext(event_queue, llm_clients, model_profile_provider, on_model_change)
317
464
  self.submission_queue: asyncio.Queue[op.Submission] = asyncio.Queue()
318
465
  # Track completion events for all submissions (not just those with ActiveTask)
319
466
  self._completion_events: dict[str, asyncio.Event] = {}
@@ -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.
@@ -1,6 +1,14 @@
1
1
  You are a powerful code search agent.
2
2
 
3
- CRITICAL: This is a READ-ONLY exploration task. You MUST NOT create, write, or modify any files under any circumstances. Your role is strictly to search and analyze existing code.
3
+ === CRITICAL: READ-ONLY MODE - NO FILE MODIFICATIONS ===
4
+ This is a READ-ONLY exploration task. You are STRICTLY PROHIBITED from:
5
+ - Creating new files (no Write, touch, or file creation of any kind)
6
+ - Modifying existing files (no Edit operations)
7
+ - Deleting files (no rm or deletion)
8
+ - Moving or copying files (no mv or cp)
9
+ - Creating temporary files anywhere, including /tmp
10
+ - Using redirect operators (>, >>, |) or heredocs to write to files
11
+ - Running ANY commands that change system state
4
12
 
5
13
  Your strengths:
6
14
  - Rapidly finding files using glob patterns
@@ -14,12 +22,16 @@ Guidelines:
14
22
  - Use Bash ONLY for read-only operations (ls, git status, git log, git diff, find, cat, head, tail). NEVER use it for file creation, modification, or commands that change system state (mkdir, touch, rm, cp, mv, git add, git commit, npm install, pip install). NEVER use redirect operators (>, >>, |) or heredocs to create files
15
23
  - Adapt your search approach based on the thoroughness level specified by the caller
16
24
  - quick = scan obvious targets; medium = cover all related modules; very thorough = exhaustive sweep with validation
17
- - For maximum efficiency, whenever you need to perform multiple independent operations, invoke all relevant tools simultaneously rather than sequentially.
18
25
  - Only your last message is surfaced back to the agent as the final answer.
19
26
  - Return file paths as absolute paths in your final response
20
27
  - For clear communication, avoid using emojis
21
28
  - Do not create any files, or run bash commands that modify the user's system state in any way (This includes temporary files in the /tmp folder. Never create these files, instead communicate your final report directly as a regular message)
22
29
 
30
+ NOTE: You are meant to be a fast agent that returns output as quickly as possible. In order to achieve this you must:
31
+ - Make efficient use of the tools that you have at your disposal: be smart about how you search for files and implementations
32
+ - Wherever possible you should try to spawn multiple parallel tool calls for grepping and reading files
33
+
34
+
23
35
  Complete the user's search request efficiently and report your findings clearly.
24
36
 
25
37
  Notes:
@@ -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
@@ -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
@@ -29,6 +29,8 @@ class MetadataAccumulator:
29
29
  self._sub_agent_metadata: list[model.TaskMetadata] = []
30
30
  self._throughput_weighted_sum: float = 0.0
31
31
  self._throughput_tracked_tokens: int = 0
32
+ self._first_token_latency_sum: float = 0.0
33
+ self._first_token_latency_count: int = 0
32
34
  self._turn_count: int = 0
33
35
 
34
36
  def add(self, turn_metadata: model.ResponseMetadataItem) -> None:
@@ -51,13 +53,8 @@ class MetadataAccumulator:
51
53
  acc_usage.context_limit = usage.context_limit
52
54
 
53
55
  if usage.first_token_latency_ms is not None:
54
- if acc_usage.first_token_latency_ms is None:
55
- acc_usage.first_token_latency_ms = usage.first_token_latency_ms
56
- else:
57
- acc_usage.first_token_latency_ms = min(
58
- acc_usage.first_token_latency_ms,
59
- usage.first_token_latency_ms,
60
- )
56
+ self._first_token_latency_sum += usage.first_token_latency_ms
57
+ self._first_token_latency_count += 1
61
58
 
62
59
  if usage.throughput_tps is not None:
63
60
  current_output = usage.output_tokens
@@ -83,6 +80,11 @@ class MetadataAccumulator:
83
80
  else:
84
81
  main.usage.throughput_tps = None
85
82
 
83
+ if self._first_token_latency_count > 0:
84
+ main.usage.first_token_latency_ms = self._first_token_latency_sum / self._first_token_latency_count
85
+ else:
86
+ main.usage.first_token_latency_ms = None
87
+
86
88
  main.task_duration_s = task_duration_s
87
89
  main.turn_count = self._turn_count
88
90
  return model.TaskMetadataItem(main=main, sub_agent_task_metadata=self._sub_agent_metadata)