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.
- klaude_code/cli/config_cmd.py +1 -1
- klaude_code/cli/debug.py +1 -1
- klaude_code/cli/main.py +3 -9
- klaude_code/cli/runtime.py +20 -13
- klaude_code/command/__init__.py +7 -1
- klaude_code/command/clear_cmd.py +2 -7
- klaude_code/command/command_abc.py +33 -5
- klaude_code/command/debug_cmd.py +79 -0
- klaude_code/command/diff_cmd.py +2 -6
- klaude_code/command/export_cmd.py +7 -7
- klaude_code/command/export_online_cmd.py +145 -0
- klaude_code/command/help_cmd.py +4 -9
- klaude_code/command/model_cmd.py +10 -6
- klaude_code/command/prompt_command.py +2 -6
- klaude_code/command/refresh_cmd.py +2 -7
- klaude_code/command/registry.py +2 -4
- klaude_code/command/release_notes_cmd.py +2 -6
- klaude_code/command/status_cmd.py +2 -7
- klaude_code/command/terminal_setup_cmd.py +2 -6
- klaude_code/command/thinking_cmd.py +13 -8
- klaude_code/config/config.py +16 -17
- klaude_code/config/select_model.py +81 -5
- klaude_code/const/__init__.py +1 -1
- klaude_code/core/executor.py +236 -109
- klaude_code/core/manager/__init__.py +2 -4
- klaude_code/core/manager/sub_agent_manager.py +1 -1
- klaude_code/core/prompts/prompt-claude-code.md +1 -1
- klaude_code/core/prompts/prompt-sub-agent-oracle.md +0 -1
- klaude_code/core/prompts/prompt-sub-agent-web.md +51 -0
- klaude_code/core/reminders.py +9 -35
- klaude_code/core/task.py +8 -0
- klaude_code/core/tool/__init__.py +2 -0
- klaude_code/core/tool/file/read_tool.py +38 -10
- klaude_code/core/tool/report_back_tool.py +28 -2
- klaude_code/core/tool/shell/bash_tool.py +22 -2
- klaude_code/core/tool/tool_runner.py +26 -23
- klaude_code/core/tool/truncation.py +23 -9
- klaude_code/core/tool/web/web_fetch_tool.md +1 -1
- klaude_code/core/tool/web/web_fetch_tool.py +36 -1
- klaude_code/core/tool/web/web_search_tool.md +23 -0
- klaude_code/core/tool/web/web_search_tool.py +126 -0
- klaude_code/core/turn.py +28 -0
- klaude_code/protocol/commands.py +2 -0
- klaude_code/protocol/events.py +8 -0
- klaude_code/protocol/sub_agent/__init__.py +1 -1
- klaude_code/protocol/sub_agent/explore.py +1 -1
- klaude_code/protocol/sub_agent/web.py +79 -0
- klaude_code/protocol/tools.py +1 -0
- klaude_code/session/session.py +2 -2
- klaude_code/session/templates/export_session.html +123 -37
- klaude_code/trace/__init__.py +20 -2
- klaude_code/ui/modes/repl/completers.py +19 -2
- klaude_code/ui/modes/repl/event_handler.py +44 -15
- klaude_code/ui/modes/repl/renderer.py +3 -3
- klaude_code/ui/renderers/metadata.py +2 -4
- klaude_code/ui/renderers/sub_agent.py +14 -10
- klaude_code/ui/renderers/thinking.py +24 -8
- klaude_code/ui/renderers/tools.py +83 -20
- klaude_code/ui/rich/code_panel.py +112 -0
- klaude_code/ui/rich/markdown.py +3 -4
- klaude_code/ui/rich/status.py +30 -6
- klaude_code/ui/rich/theme.py +10 -1
- {klaude_code-1.2.16.dist-info → klaude_code-1.2.18.dist-info}/METADATA +126 -25
- {klaude_code-1.2.16.dist-info → klaude_code-1.2.18.dist-info}/RECORD +67 -63
- klaude_code/core/manager/agent_manager.py +0 -132
- klaude_code/core/prompts/prompt-sub-agent-webfetch.md +0 -46
- klaude_code/protocol/sub_agent/web_fetch.py +0 -74
- /klaude_code/{config → cli}/list_model.py +0 -0
- {klaude_code-1.2.16.dist-info → klaude_code-1.2.18.dist-info}/WHEEL +0 -0
- {klaude_code-1.2.16.dist-info → klaude_code-1.2.18.dist-info}/entry_points.txt +0 -0
klaude_code/core/executor.py
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
|
107
|
-
"""
|
|
286
|
+
def current_agent(self) -> Agent | None:
|
|
287
|
+
"""Return the currently active agent, if any."""
|
|
288
|
+
|
|
289
|
+
return self._agent
|
|
108
290
|
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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 (
|
|
5
|
-
|
|
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
|
-
|
|
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
|
|
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
|
klaude_code/core/reminders.py
CHANGED
|
@@ -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
|
-
"""
|
|
378
|
-
|
|
379
|
-
|
|
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
|
-
|
|
382
|
-
|
|
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",
|