klaude-code 1.8.0__py3-none-any.whl → 2.0.0__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/auth/base.py +97 -0
- klaude_code/auth/claude/__init__.py +6 -0
- klaude_code/auth/claude/exceptions.py +9 -0
- klaude_code/auth/claude/oauth.py +172 -0
- klaude_code/auth/claude/token_manager.py +26 -0
- klaude_code/auth/codex/token_manager.py +10 -50
- klaude_code/cli/auth_cmd.py +127 -46
- klaude_code/cli/config_cmd.py +4 -2
- klaude_code/cli/cost_cmd.py +14 -9
- klaude_code/cli/list_model.py +248 -200
- klaude_code/cli/main.py +1 -1
- klaude_code/cli/runtime.py +7 -5
- klaude_code/cli/self_update.py +1 -1
- klaude_code/cli/session_cmd.py +1 -1
- klaude_code/command/clear_cmd.py +6 -2
- klaude_code/command/command_abc.py +2 -2
- klaude_code/command/debug_cmd.py +4 -4
- klaude_code/command/export_cmd.py +2 -2
- klaude_code/command/export_online_cmd.py +12 -12
- klaude_code/command/fork_session_cmd.py +29 -23
- klaude_code/command/help_cmd.py +4 -4
- klaude_code/command/model_cmd.py +4 -4
- klaude_code/command/model_select.py +1 -1
- klaude_code/command/prompt-commit.md +82 -0
- klaude_code/command/prompt_command.py +3 -3
- klaude_code/command/refresh_cmd.py +2 -2
- klaude_code/command/registry.py +7 -5
- klaude_code/command/release_notes_cmd.py +4 -4
- klaude_code/command/resume_cmd.py +15 -11
- klaude_code/command/status_cmd.py +4 -4
- klaude_code/command/terminal_setup_cmd.py +8 -8
- klaude_code/command/thinking_cmd.py +4 -4
- klaude_code/config/assets/builtin_config.yaml +52 -3
- klaude_code/config/builtin_config.py +16 -5
- klaude_code/config/config.py +31 -7
- klaude_code/config/thinking.py +4 -4
- klaude_code/const.py +146 -91
- klaude_code/core/agent.py +3 -12
- klaude_code/core/executor.py +21 -13
- klaude_code/core/manager/sub_agent_manager.py +71 -7
- klaude_code/core/prompt.py +1 -1
- klaude_code/core/prompts/prompt-sub-agent-image-gen.md +1 -0
- klaude_code/core/prompts/prompt-sub-agent-web.md +27 -1
- klaude_code/core/reminders.py +88 -69
- klaude_code/core/task.py +44 -45
- klaude_code/core/tool/file/apply_patch_tool.py +9 -9
- klaude_code/core/tool/file/diff_builder.py +3 -5
- klaude_code/core/tool/file/edit_tool.py +23 -23
- klaude_code/core/tool/file/move_tool.py +43 -43
- klaude_code/core/tool/file/read_tool.py +44 -39
- klaude_code/core/tool/file/write_tool.py +14 -14
- klaude_code/core/tool/report_back_tool.py +4 -4
- klaude_code/core/tool/shell/bash_tool.py +23 -23
- klaude_code/core/tool/skill/skill_tool.py +7 -7
- klaude_code/core/tool/sub_agent_tool.py +38 -9
- klaude_code/core/tool/todo/todo_write_tool.py +8 -8
- klaude_code/core/tool/todo/update_plan_tool.py +6 -6
- klaude_code/core/tool/tool_abc.py +2 -2
- klaude_code/core/tool/tool_context.py +27 -0
- klaude_code/core/tool/tool_runner.py +88 -42
- klaude_code/core/tool/truncation.py +38 -20
- klaude_code/core/tool/web/mermaid_tool.py +6 -7
- klaude_code/core/tool/web/web_fetch_tool.py +68 -30
- klaude_code/core/tool/web/web_search_tool.py +15 -17
- klaude_code/core/turn.py +120 -73
- klaude_code/llm/anthropic/client.py +104 -44
- klaude_code/llm/anthropic/input.py +116 -108
- klaude_code/llm/bedrock/client.py +8 -5
- klaude_code/llm/claude/__init__.py +3 -0
- klaude_code/llm/claude/client.py +105 -0
- klaude_code/llm/client.py +4 -3
- klaude_code/llm/codex/client.py +16 -10
- klaude_code/llm/google/client.py +122 -60
- klaude_code/llm/google/input.py +94 -108
- klaude_code/llm/image.py +123 -0
- klaude_code/llm/input_common.py +136 -189
- klaude_code/llm/openai_compatible/client.py +17 -7
- klaude_code/llm/openai_compatible/input.py +36 -66
- klaude_code/llm/openai_compatible/stream.py +119 -67
- klaude_code/llm/openai_compatible/tool_call_accumulator.py +23 -11
- klaude_code/llm/openrouter/client.py +34 -9
- klaude_code/llm/openrouter/input.py +63 -64
- klaude_code/llm/openrouter/reasoning.py +22 -24
- klaude_code/llm/registry.py +20 -15
- klaude_code/llm/responses/client.py +107 -45
- klaude_code/llm/responses/input.py +115 -98
- klaude_code/llm/usage.py +52 -25
- klaude_code/protocol/__init__.py +1 -0
- klaude_code/protocol/events.py +16 -12
- klaude_code/protocol/llm_param.py +22 -3
- klaude_code/protocol/message.py +250 -0
- klaude_code/protocol/model.py +94 -281
- klaude_code/protocol/op.py +2 -2
- klaude_code/protocol/sub_agent/__init__.py +2 -2
- klaude_code/protocol/sub_agent/explore.py +10 -0
- klaude_code/protocol/sub_agent/image_gen.py +119 -0
- klaude_code/protocol/sub_agent/task.py +10 -0
- klaude_code/protocol/sub_agent/web.py +10 -0
- klaude_code/session/codec.py +6 -6
- klaude_code/session/export.py +261 -62
- klaude_code/session/selector.py +7 -24
- klaude_code/session/session.py +125 -53
- klaude_code/session/store.py +5 -32
- klaude_code/session/templates/export_session.html +1 -1
- klaude_code/session/templates/mermaid_viewer.html +1 -1
- klaude_code/trace/log.py +11 -6
- klaude_code/ui/core/input.py +1 -1
- klaude_code/ui/core/stage_manager.py +1 -8
- klaude_code/ui/modes/debug/display.py +2 -2
- klaude_code/ui/modes/repl/clipboard.py +2 -2
- klaude_code/ui/modes/repl/completers.py +18 -10
- klaude_code/ui/modes/repl/event_handler.py +136 -127
- klaude_code/ui/modes/repl/input_prompt_toolkit.py +1 -1
- klaude_code/ui/modes/repl/key_bindings.py +1 -1
- klaude_code/ui/modes/repl/renderer.py +107 -15
- klaude_code/ui/renderers/assistant.py +2 -2
- klaude_code/ui/renderers/common.py +65 -7
- klaude_code/ui/renderers/developer.py +7 -6
- klaude_code/ui/renderers/diffs.py +11 -11
- klaude_code/ui/renderers/mermaid_viewer.py +49 -2
- klaude_code/ui/renderers/metadata.py +39 -31
- klaude_code/ui/renderers/sub_agent.py +57 -16
- klaude_code/ui/renderers/thinking.py +37 -2
- klaude_code/ui/renderers/tools.py +180 -165
- klaude_code/ui/rich/live.py +3 -1
- klaude_code/ui/rich/markdown.py +39 -7
- klaude_code/ui/rich/quote.py +76 -1
- klaude_code/ui/rich/status.py +14 -8
- klaude_code/ui/rich/theme.py +13 -6
- klaude_code/ui/terminal/image.py +34 -0
- klaude_code/ui/terminal/notifier.py +2 -1
- klaude_code/ui/terminal/progress_bar.py +4 -4
- klaude_code/ui/terminal/selector.py +22 -4
- klaude_code/ui/utils/common.py +55 -0
- {klaude_code-1.8.0.dist-info → klaude_code-2.0.0.dist-info}/METADATA +28 -6
- klaude_code-2.0.0.dist-info/RECORD +229 -0
- klaude_code/command/prompt-jj-describe.md +0 -32
- klaude_code/core/prompts/prompt-sub-agent-oracle.md +0 -22
- klaude_code/protocol/sub_agent/oracle.py +0 -91
- klaude_code-1.8.0.dist-info/RECORD +0 -219
- {klaude_code-1.8.0.dist-info → klaude_code-2.0.0.dist-info}/WHEEL +0 -0
- {klaude_code-1.8.0.dist-info → klaude_code-2.0.0.dist-info}/entry_points.txt +0 -0
klaude_code/core/executor.py
CHANGED
|
@@ -19,7 +19,7 @@ from klaude_code.core.agent import Agent, DefaultModelProfileProvider, ModelProf
|
|
|
19
19
|
from klaude_code.core.manager import LLMClients, SubAgentManager
|
|
20
20
|
from klaude_code.core.tool import current_run_subtask_callback
|
|
21
21
|
from klaude_code.llm.registry import create_llm_client
|
|
22
|
-
from klaude_code.protocol import commands, events, model, op
|
|
22
|
+
from klaude_code.protocol import commands, events, message, model, op
|
|
23
23
|
from klaude_code.protocol.llm_param import Thinking
|
|
24
24
|
from klaude_code.protocol.op_handler import OperationHandler
|
|
25
25
|
from klaude_code.protocol.sub_agent import SubAgentResult
|
|
@@ -193,7 +193,9 @@ class ExecutorContext:
|
|
|
193
193
|
await self.emit_event(
|
|
194
194
|
events.UserMessageEvent(content=user_input.text, session_id=session_id, images=user_input.images)
|
|
195
195
|
)
|
|
196
|
-
agent.session.append_history(
|
|
196
|
+
agent.session.append_history(
|
|
197
|
+
[message.UserMessage(parts=message.parts_from_text_and_images(user_input.text, user_input.images))]
|
|
198
|
+
)
|
|
197
199
|
|
|
198
200
|
await self.handle_run_agent(
|
|
199
201
|
op.RunAgentOperation(
|
|
@@ -230,8 +232,8 @@ class ExecutorContext:
|
|
|
230
232
|
|
|
231
233
|
if operation.emit_switch_message:
|
|
232
234
|
default_note = " (saved as default)" if operation.save_as_default else ""
|
|
233
|
-
developer_item =
|
|
234
|
-
|
|
235
|
+
developer_item = message.DeveloperMessage(
|
|
236
|
+
parts=message.text_parts_from_str(f"Switched to: {llm_config.model}{default_note}"),
|
|
235
237
|
command_output=model.CommandOutput(command_name=commands.CommandName.MODEL),
|
|
236
238
|
)
|
|
237
239
|
agent.session.append_history([developer_item])
|
|
@@ -275,8 +277,8 @@ class ExecutorContext:
|
|
|
275
277
|
new_status = _format_thinking_for_display(config.thinking)
|
|
276
278
|
|
|
277
279
|
if operation.emit_switch_message:
|
|
278
|
-
developer_item =
|
|
279
|
-
|
|
280
|
+
developer_item = message.DeveloperMessage(
|
|
281
|
+
parts=message.text_parts_from_str(f"Thinking changed: {current} -> {new_status}"),
|
|
280
282
|
command_output=model.CommandOutput(command_name=commands.CommandName.THINKING),
|
|
281
283
|
)
|
|
282
284
|
agent.session.append_history([developer_item])
|
|
@@ -293,11 +295,17 @@ class ExecutorContext:
|
|
|
293
295
|
new_session.model_thinking = agent.session.model_thinking
|
|
294
296
|
agent.session = new_session
|
|
295
297
|
|
|
296
|
-
developer_item =
|
|
297
|
-
|
|
298
|
+
developer_item = message.DeveloperMessage(
|
|
299
|
+
parts=message.text_parts_from_str("started new conversation"),
|
|
298
300
|
command_output=model.CommandOutput(command_name=commands.CommandName.CLEAR),
|
|
299
301
|
)
|
|
300
302
|
await self.emit_event(events.DeveloperMessageEvent(session_id=agent.session.id, item=developer_item))
|
|
303
|
+
await self.emit_event(
|
|
304
|
+
events.WelcomeEvent(
|
|
305
|
+
work_dir=str(agent.session.work_dir),
|
|
306
|
+
llm_config=self.llm_clients.main.get_llm_config(),
|
|
307
|
+
)
|
|
308
|
+
)
|
|
301
309
|
|
|
302
310
|
async def handle_resume_session(self, operation: op.ResumeSessionOperation) -> None:
|
|
303
311
|
target_session = Session.load(operation.target_session_id)
|
|
@@ -338,8 +346,8 @@ class ExecutorContext:
|
|
|
338
346
|
await asyncio.to_thread(output_path.parent.mkdir, parents=True, exist_ok=True)
|
|
339
347
|
await asyncio.to_thread(output_path.write_text, html_doc, "utf-8")
|
|
340
348
|
await asyncio.to_thread(self._open_file, output_path)
|
|
341
|
-
developer_item =
|
|
342
|
-
|
|
349
|
+
developer_item = message.DeveloperMessage(
|
|
350
|
+
parts=message.text_parts_from_str(f"Session exported and opened: {output_path}"),
|
|
343
351
|
command_output=model.CommandOutput(command_name=commands.CommandName.EXPORT),
|
|
344
352
|
)
|
|
345
353
|
agent.session.append_history([developer_item])
|
|
@@ -347,8 +355,8 @@ class ExecutorContext:
|
|
|
347
355
|
except Exception as exc: # pragma: no cover
|
|
348
356
|
import traceback
|
|
349
357
|
|
|
350
|
-
developer_item =
|
|
351
|
-
|
|
358
|
+
developer_item = message.DeveloperMessage(
|
|
359
|
+
parts=message.text_parts_from_str(f"Failed to export session: {exc}\n{traceback.format_exc()}"),
|
|
352
360
|
command_output=model.CommandOutput(command_name=commands.CommandName.EXPORT, is_error=True),
|
|
353
361
|
)
|
|
354
362
|
agent.session.append_history([developer_item])
|
|
@@ -357,7 +365,7 @@ class ExecutorContext:
|
|
|
357
365
|
async def _run_agent_task(
|
|
358
366
|
self,
|
|
359
367
|
agent: Agent,
|
|
360
|
-
user_input:
|
|
368
|
+
user_input: message.UserInputPayload,
|
|
361
369
|
task_id: str,
|
|
362
370
|
session_id: str,
|
|
363
371
|
) -> None:
|
|
@@ -8,11 +8,13 @@ their events back to the shared event queue.
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
import asyncio
|
|
11
|
+
import json
|
|
11
12
|
|
|
12
13
|
from klaude_code.core.agent import Agent, AgentProfile, ModelProfileProvider
|
|
13
14
|
from klaude_code.core.manager.llm_clients import LLMClients
|
|
14
15
|
from klaude_code.core.tool import ReportBackTool
|
|
15
|
-
from klaude_code.
|
|
16
|
+
from klaude_code.core.tool.tool_context import record_sub_agent_session_id
|
|
17
|
+
from klaude_code.protocol import events, message, model
|
|
16
18
|
from klaude_code.protocol.sub_agent import SubAgentResult
|
|
17
19
|
from klaude_code.session.session import Session
|
|
18
20
|
from klaude_code.trace import DebugType, log_debug
|
|
@@ -39,10 +41,60 @@ class SubAgentManager:
|
|
|
39
41
|
async def run_sub_agent(self, parent_agent: Agent, state: model.SubAgentState) -> SubAgentResult:
|
|
40
42
|
"""Run a nested sub-agent task and return its result."""
|
|
41
43
|
|
|
42
|
-
# Create a child session under the same workdir
|
|
43
44
|
parent_session = parent_agent.session
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
resume_session_id = state.resume
|
|
46
|
+
|
|
47
|
+
def _append_agent_id(task_result: str, session_id: str) -> str:
|
|
48
|
+
trimmed = (task_result or "").rstrip()
|
|
49
|
+
footer = f"agentId: {session_id} (for resuming to continue this agent's work if needed)"
|
|
50
|
+
if trimmed.strip():
|
|
51
|
+
return f"{trimmed}\n\n{footer}"
|
|
52
|
+
return footer
|
|
53
|
+
|
|
54
|
+
if resume_session_id:
|
|
55
|
+
try:
|
|
56
|
+
child_session = Session.load(resume_session_id)
|
|
57
|
+
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
58
|
+
return SubAgentResult(
|
|
59
|
+
task_result=f"Failed to resume sub-agent session '{resume_session_id}': {exc}",
|
|
60
|
+
session_id="",
|
|
61
|
+
error=True,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
if child_session.sub_agent_state is None:
|
|
65
|
+
return SubAgentResult(
|
|
66
|
+
task_result=(f"Invalid resume id '{resume_session_id}': target session is not a sub-agent session"),
|
|
67
|
+
session_id="",
|
|
68
|
+
error=True,
|
|
69
|
+
)
|
|
70
|
+
if child_session.sub_agent_state.sub_agent_type != state.sub_agent_type:
|
|
71
|
+
return SubAgentResult(
|
|
72
|
+
task_result=(
|
|
73
|
+
"Invalid resume id: sub-agent type mismatch. "
|
|
74
|
+
f"Expected '{state.sub_agent_type}', got '{child_session.sub_agent_state.sub_agent_type}'."
|
|
75
|
+
),
|
|
76
|
+
session_id="",
|
|
77
|
+
error=True,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Expose the session id immediately so ToolExecutor.cancel() can attach
|
|
81
|
+
# it to the synthesized cancellation ToolResult.
|
|
82
|
+
record_sub_agent_session_id(child_session.id)
|
|
83
|
+
|
|
84
|
+
# Update persisted sub-agent state to reflect the current invocation.
|
|
85
|
+
child_session.sub_agent_state.sub_agent_desc = state.sub_agent_desc
|
|
86
|
+
child_session.sub_agent_state.sub_agent_prompt = state.sub_agent_prompt
|
|
87
|
+
child_session.sub_agent_state.resume = resume_session_id
|
|
88
|
+
child_session.sub_agent_state.output_schema = state.output_schema
|
|
89
|
+
child_session.sub_agent_state.generation = state.generation
|
|
90
|
+
else:
|
|
91
|
+
# Create a new child session under the same workdir
|
|
92
|
+
child_session = Session(work_dir=parent_session.work_dir)
|
|
93
|
+
child_session.sub_agent_state = state
|
|
94
|
+
|
|
95
|
+
# Expose the new session id immediately so ToolExecutor.cancel() can attach
|
|
96
|
+
# it to the synthesized cancellation ToolResult.
|
|
97
|
+
record_sub_agent_session_id(child_session.id)
|
|
46
98
|
|
|
47
99
|
child_profile = self._model_profile_provider.build_profile(
|
|
48
100
|
self._llm_clients.get_client(state.sub_agent_type),
|
|
@@ -79,18 +131,30 @@ Only the content passed to `report_back` will be returned to user.\
|
|
|
79
131
|
# Not emit the subtask's user input since task tool call is already rendered
|
|
80
132
|
result: str = ""
|
|
81
133
|
task_metadata: model.TaskMetadata | None = None
|
|
82
|
-
sub_agent_input =
|
|
134
|
+
sub_agent_input = message.UserInputPayload(text=state.sub_agent_prompt, images=None)
|
|
83
135
|
child_session.append_history(
|
|
84
|
-
[
|
|
136
|
+
[
|
|
137
|
+
message.UserMessage(
|
|
138
|
+
parts=message.parts_from_text_and_images(sub_agent_input.text, sub_agent_input.images)
|
|
139
|
+
)
|
|
140
|
+
]
|
|
85
141
|
)
|
|
86
142
|
async for event in child_agent.run_task(sub_agent_input):
|
|
87
143
|
# Capture TaskFinishEvent content for return
|
|
88
144
|
if isinstance(event, events.TaskFinishEvent):
|
|
89
|
-
result = event.task_result
|
|
145
|
+
result = _append_agent_id(event.task_result, child_session.id)
|
|
146
|
+
event = events.TaskFinishEvent(
|
|
147
|
+
session_id=event.session_id,
|
|
148
|
+
task_result=result,
|
|
149
|
+
has_structured_output=event.has_structured_output,
|
|
150
|
+
)
|
|
90
151
|
# Capture TaskMetadataEvent for metadata propagation
|
|
91
152
|
elif isinstance(event, events.TaskMetadataEvent):
|
|
92
153
|
task_metadata = event.metadata.main_agent
|
|
93
154
|
await self.emit_event(event)
|
|
155
|
+
|
|
156
|
+
# Ensure the sub-agent session is persisted before returning its id for resume.
|
|
157
|
+
await child_session.wait_for_flush()
|
|
94
158
|
return SubAgentResult(
|
|
95
159
|
task_result=result,
|
|
96
160
|
session_id=child_session.id,
|
klaude_code/core/prompt.py
CHANGED
|
@@ -101,7 +101,7 @@ def load_system_prompt(
|
|
|
101
101
|
file_key = _get_file_key(model_name, protocol)
|
|
102
102
|
base_prompt = _load_base_prompt(file_key)
|
|
103
103
|
|
|
104
|
-
if protocol == llm_param.LLMClientProtocol.
|
|
104
|
+
if protocol == llm_param.LLMClientProtocol.CODEX_OAUTH:
|
|
105
105
|
# Do not append environment info for Codex protocol
|
|
106
106
|
return base_prompt
|
|
107
107
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
You are an image generation agent. Generate images based on the user's prompt.
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
You are a web research subagent that searches and fetches web content to provide up-to-date information as part of team.
|
|
2
2
|
|
|
3
|
+
## Core Principles
|
|
4
|
+
|
|
5
|
+
- **Never invent facts**. If you cannot verify something, say so clearly and explain what you did find.
|
|
6
|
+
- If evidence is thin, keep searching rather than guessing.
|
|
7
|
+
- When sources conflict, actively resolve contradictions by finding additional authoritative sources.
|
|
8
|
+
|
|
3
9
|
## Available Tools
|
|
4
10
|
|
|
5
11
|
**WebSearch**: Search the web via DuckDuckGo
|
|
@@ -32,6 +38,19 @@ Balance efficiency with thoroughness. For open-ended questions (e.g., "recommend
|
|
|
32
38
|
- Follow relevant links on pages with WebFetch
|
|
33
39
|
- If truncated results are saved to local files, use grep/read to explore
|
|
34
40
|
|
|
41
|
+
### Research Strategy
|
|
42
|
+
|
|
43
|
+
- Start with multiple targeted searches. Use parallel searches when helpful. Never rely on a single query.
|
|
44
|
+
- Begin broad enough to capture the main answer, then add targeted follow-ups to fill gaps or confirm claims.
|
|
45
|
+
- If the topic is time-sensitive, explicitly check for recent updates.
|
|
46
|
+
- If the query implies comparisons or recommendations, gather enough coverage to make tradeoffs clear.
|
|
47
|
+
- Keep iterating until additional searching is unlikely to materially change the answer.
|
|
48
|
+
|
|
49
|
+
### Handling Ambiguity
|
|
50
|
+
|
|
51
|
+
- Do not ask clarifying questions - you cannot interact with the user.
|
|
52
|
+
- If the query is ambiguous, state your interpretation plainly, then comprehensively cover all plausible intents.
|
|
53
|
+
|
|
35
54
|
## Response Guidelines
|
|
36
55
|
|
|
37
56
|
- Only your last message is returned to the main agent
|
|
@@ -40,7 +59,14 @@ Balance efficiency with thoroughness. For open-ended questions (e.g., "recommend
|
|
|
40
59
|
- Include the file path from `<file_saved>` so the main agent can access full content if needed
|
|
41
60
|
- Lead with the most recent info for evolving topics
|
|
42
61
|
- Favor original sources (company blogs, papers, gov sites) over aggregators
|
|
43
|
-
-
|
|
62
|
+
- When sources conflict, explain the discrepancy and which source is more authoritative
|
|
63
|
+
|
|
64
|
+
### Before Finalizing
|
|
65
|
+
|
|
66
|
+
Stop only when all are true:
|
|
67
|
+
1. You answered the query and every subpart
|
|
68
|
+
2. You found sufficient sources for core claims
|
|
69
|
+
3. You resolved any contradictions between sources
|
|
44
70
|
|
|
45
71
|
## Sources (REQUIRED)
|
|
46
72
|
|
klaude_code/core/reminders.py
CHANGED
|
@@ -7,14 +7,14 @@ from pathlib import Path
|
|
|
7
7
|
|
|
8
8
|
from pydantic import BaseModel
|
|
9
9
|
|
|
10
|
-
from klaude_code import
|
|
10
|
+
from klaude_code.const import MEMORY_FILE_NAMES, REMINDER_COOLDOWN_TURNS, TODO_REMINDER_TOOL_CALL_THRESHOLD
|
|
11
11
|
from klaude_code.core.tool import BashTool, ReadTool, reset_tool_context, set_tool_context_from_session
|
|
12
12
|
from klaude_code.core.tool.file._utils import hash_text_sha256
|
|
13
|
-
from klaude_code.protocol import model, tools
|
|
13
|
+
from klaude_code.protocol import message, model, tools
|
|
14
14
|
from klaude_code.session import Session
|
|
15
15
|
from klaude_code.skill import get_skill
|
|
16
16
|
|
|
17
|
-
type Reminder = Callable[[Session], Awaitable[
|
|
17
|
+
type Reminder = Callable[[Session], Awaitable[message.DeveloperMessage | None]]
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
# Match @ preceded by whitespace, start of line, or → (ReadTool line number arrow)
|
|
@@ -28,13 +28,13 @@ def get_last_new_user_input(session: Session) -> str | None:
|
|
|
28
28
|
"""Get last user input & developer message (CLAUDE.md) from conversation history. if there's a tool result after user input, return None"""
|
|
29
29
|
result: list[str] = []
|
|
30
30
|
for item in reversed(session.conversation_history):
|
|
31
|
-
if isinstance(item,
|
|
31
|
+
if isinstance(item, message.ToolResultMessage):
|
|
32
32
|
return None
|
|
33
|
-
if isinstance(item,
|
|
34
|
-
result.append(item.
|
|
33
|
+
if isinstance(item, message.UserMessage):
|
|
34
|
+
result.append(message.join_text_parts(item.parts))
|
|
35
35
|
break
|
|
36
|
-
if isinstance(item,
|
|
37
|
-
result.append(item.
|
|
36
|
+
if isinstance(item, message.DeveloperMessage):
|
|
37
|
+
result.append(message.join_text_parts(item.parts))
|
|
38
38
|
return "\n\n".join(result)
|
|
39
39
|
|
|
40
40
|
|
|
@@ -62,16 +62,16 @@ def get_at_patterns_with_source(session: Session) -> list[AtPatternSource]:
|
|
|
62
62
|
patterns: list[AtPatternSource] = []
|
|
63
63
|
|
|
64
64
|
for item in reversed(session.conversation_history):
|
|
65
|
-
if isinstance(item,
|
|
65
|
+
if isinstance(item, message.ToolResultMessage):
|
|
66
66
|
break
|
|
67
67
|
|
|
68
|
-
if isinstance(item,
|
|
69
|
-
content = item.
|
|
68
|
+
if isinstance(item, message.UserMessage):
|
|
69
|
+
content = message.join_text_parts(item.parts)
|
|
70
70
|
for path_str in _extract_at_patterns(content):
|
|
71
71
|
patterns.append(AtPatternSource(pattern=path_str, mentioned_in=None))
|
|
72
72
|
break
|
|
73
73
|
|
|
74
|
-
if isinstance(item,
|
|
74
|
+
if isinstance(item, message.DeveloperMessage) and item.memory_mentioned:
|
|
75
75
|
for memory_path, mentioned_patterns in item.memory_mentioned.items():
|
|
76
76
|
for pattern in mentioned_patterns:
|
|
77
77
|
patterns.append(AtPatternSource(pattern=pattern, mentioned_in=memory_path))
|
|
@@ -81,10 +81,10 @@ def get_at_patterns_with_source(session: Session) -> list[AtPatternSource]:
|
|
|
81
81
|
def get_skill_from_user_input(session: Session) -> str | None:
|
|
82
82
|
"""Get $skill reference from the first line of last user input."""
|
|
83
83
|
for item in reversed(session.conversation_history):
|
|
84
|
-
if isinstance(item,
|
|
84
|
+
if isinstance(item, message.ToolResultMessage):
|
|
85
85
|
return None
|
|
86
|
-
if isinstance(item,
|
|
87
|
-
content = item.
|
|
86
|
+
if isinstance(item, message.UserMessage):
|
|
87
|
+
content = message.join_text_parts(item.parts)
|
|
88
88
|
first_line = content.split("\n", 1)[0]
|
|
89
89
|
m = SKILL_PATTERN.match(first_line)
|
|
90
90
|
if m:
|
|
@@ -114,7 +114,7 @@ async def _load_at_file_recursive(
|
|
|
114
114
|
session: Session,
|
|
115
115
|
pattern: str,
|
|
116
116
|
at_files: dict[str, model.AtPatternParseResult],
|
|
117
|
-
collected_images: list[
|
|
117
|
+
collected_images: list[message.ImageURLPart],
|
|
118
118
|
visited: set[str],
|
|
119
119
|
base_dir: Path | None = None,
|
|
120
120
|
mentioned_in: str | None = None,
|
|
@@ -134,20 +134,20 @@ async def _load_at_file_recursive(
|
|
|
134
134
|
return
|
|
135
135
|
args = ReadTool.ReadArguments(file_path=path_str)
|
|
136
136
|
tool_result = await ReadTool.call_with_args(args)
|
|
137
|
+
images = [part for part in tool_result.parts if isinstance(part, message.ImageURLPart)]
|
|
137
138
|
at_files[path_str] = model.AtPatternParseResult(
|
|
138
139
|
path=path_str,
|
|
139
140
|
tool_name=tools.READ,
|
|
140
|
-
result=tool_result.
|
|
141
|
+
result=tool_result.output_text,
|
|
141
142
|
tool_args=args.model_dump_json(exclude_none=True),
|
|
142
143
|
operation="Read",
|
|
143
|
-
images=tool_result.images,
|
|
144
144
|
mentioned_in=mentioned_in,
|
|
145
145
|
)
|
|
146
|
-
if
|
|
147
|
-
collected_images.extend(
|
|
146
|
+
if images:
|
|
147
|
+
collected_images.extend(images)
|
|
148
148
|
|
|
149
149
|
# Recursively parse @ references from ReadTool output
|
|
150
|
-
output = tool_result.
|
|
150
|
+
output = tool_result.output_text
|
|
151
151
|
if "@" in output:
|
|
152
152
|
for match in AT_FILE_PATTERN.finditer(output):
|
|
153
153
|
nested = match.group("quoted") or match.group("plain")
|
|
@@ -168,7 +168,7 @@ async def _load_at_file_recursive(
|
|
|
168
168
|
at_files[path_str] = model.AtPatternParseResult(
|
|
169
169
|
path=path_str + "/",
|
|
170
170
|
tool_name=tools.BASH,
|
|
171
|
-
result=tool_result.
|
|
171
|
+
result=tool_result.output_text,
|
|
172
172
|
tool_args=args.model_dump_json(exclude_none=True),
|
|
173
173
|
operation="List",
|
|
174
174
|
)
|
|
@@ -178,14 +178,14 @@ async def _load_at_file_recursive(
|
|
|
178
178
|
|
|
179
179
|
async def at_file_reader_reminder(
|
|
180
180
|
session: Session,
|
|
181
|
-
) ->
|
|
181
|
+
) -> message.DeveloperMessage | None:
|
|
182
182
|
"""Parse @foo/bar to read, with recursive loading of nested @ references"""
|
|
183
183
|
at_pattern_sources = get_at_patterns_with_source(session)
|
|
184
184
|
if not at_pattern_sources:
|
|
185
185
|
return None
|
|
186
186
|
|
|
187
187
|
at_files: dict[str, model.AtPatternParseResult] = {} # path -> content
|
|
188
|
-
collected_images: list[
|
|
188
|
+
collected_images: list[message.ImageURLPart] = []
|
|
189
189
|
visited: set[str] = set()
|
|
190
190
|
|
|
191
191
|
for source in at_pattern_sources:
|
|
@@ -210,14 +210,16 @@ Result of calling the {result.tool_name} tool:
|
|
|
210
210
|
for result in at_files.values()
|
|
211
211
|
]
|
|
212
212
|
)
|
|
213
|
-
return
|
|
214
|
-
|
|
213
|
+
return message.DeveloperMessage(
|
|
214
|
+
parts=message.parts_from_text_and_images(
|
|
215
|
+
f"""<system-reminder>{at_files_str}\n</system-reminder>""",
|
|
216
|
+
collected_images or None,
|
|
217
|
+
),
|
|
215
218
|
at_files=list(at_files.values()),
|
|
216
|
-
images=collected_images or None,
|
|
217
219
|
)
|
|
218
220
|
|
|
219
221
|
|
|
220
|
-
async def empty_todo_reminder(session: Session) ->
|
|
222
|
+
async def empty_todo_reminder(session: Session) -> message.DeveloperMessage | None:
|
|
221
223
|
"""Remind agent to use TodoWrite tool when todos are empty/all completed.
|
|
222
224
|
|
|
223
225
|
Behavior:
|
|
@@ -233,9 +235,11 @@ async def empty_todo_reminder(session: Session) -> model.DeveloperMessageItem |
|
|
|
233
235
|
return None
|
|
234
236
|
|
|
235
237
|
if session.need_todo_empty_cooldown_counter == 0:
|
|
236
|
-
session.need_todo_empty_cooldown_counter =
|
|
237
|
-
return
|
|
238
|
-
|
|
238
|
+
session.need_todo_empty_cooldown_counter = REMINDER_COOLDOWN_TURNS
|
|
239
|
+
return message.DeveloperMessage(
|
|
240
|
+
parts=message.text_parts_from_str(
|
|
241
|
+
"<system-reminder>This is a reminder that your todo list is currently empty. DO NOT mention this to the user explicitly because they are already aware. If you are working on tasks that would benefit from a todo list please use the TodoWrite tool to create one. If not, please feel free to ignore. Again do not mention this message to the user.</system-reminder>"
|
|
242
|
+
)
|
|
239
243
|
)
|
|
240
244
|
|
|
241
245
|
if session.need_todo_empty_cooldown_counter > 0:
|
|
@@ -245,7 +249,7 @@ async def empty_todo_reminder(session: Session) -> model.DeveloperMessageItem |
|
|
|
245
249
|
|
|
246
250
|
async def todo_not_used_recently_reminder(
|
|
247
251
|
session: Session,
|
|
248
|
-
) ->
|
|
252
|
+
) -> message.DeveloperMessage | None:
|
|
249
253
|
"""Remind agent to use TodoWrite tool if it hasn't been used recently (>=10 other tool calls), with cooldown.
|
|
250
254
|
|
|
251
255
|
Cooldown behavior:
|
|
@@ -264,28 +268,37 @@ async def todo_not_used_recently_reminder(
|
|
|
264
268
|
# Count non-todo tool calls since the last TodoWrite
|
|
265
269
|
other_tool_call_count_before_last_todo = 0
|
|
266
270
|
for item in reversed(session.conversation_history):
|
|
267
|
-
if isinstance(item,
|
|
268
|
-
|
|
271
|
+
if not isinstance(item, message.AssistantMessage):
|
|
272
|
+
continue
|
|
273
|
+
for part in reversed(item.parts):
|
|
274
|
+
if not isinstance(part, message.ToolCallPart):
|
|
275
|
+
continue
|
|
276
|
+
if part.tool_name in (tools.TODO_WRITE, tools.UPDATE_PLAN):
|
|
277
|
+
other_tool_call_count_before_last_todo = 0
|
|
269
278
|
break
|
|
270
279
|
other_tool_call_count_before_last_todo += 1
|
|
271
|
-
if other_tool_call_count_before_last_todo >=
|
|
280
|
+
if other_tool_call_count_before_last_todo >= TODO_REMINDER_TOOL_CALL_THRESHOLD:
|
|
272
281
|
break
|
|
282
|
+
if other_tool_call_count_before_last_todo == 0:
|
|
283
|
+
break
|
|
273
284
|
|
|
274
|
-
not_used_recently = other_tool_call_count_before_last_todo >=
|
|
285
|
+
not_used_recently = other_tool_call_count_before_last_todo >= TODO_REMINDER_TOOL_CALL_THRESHOLD
|
|
275
286
|
|
|
276
287
|
if not not_used_recently:
|
|
277
288
|
return None
|
|
278
289
|
|
|
279
290
|
if session.need_todo_not_used_cooldown_counter == 0:
|
|
280
|
-
session.need_todo_not_used_cooldown_counter =
|
|
281
|
-
return
|
|
282
|
-
|
|
291
|
+
session.need_todo_not_used_cooldown_counter = REMINDER_COOLDOWN_TURNS
|
|
292
|
+
return message.DeveloperMessage(
|
|
293
|
+
parts=message.text_parts_from_str(
|
|
294
|
+
f"""<system-reminder>
|
|
283
295
|
The TodoWrite tool hasn't been used recently. If you're working on tasks that would benefit from tracking progress, consider using the TodoWrite tool to track progress. Also consider cleaning up the todo list if has become stale and no longer matches what you are working on. Only use it if it's relevant to the current work. This is just a gentle reminder - ignore if not applicable.
|
|
284
296
|
|
|
285
297
|
|
|
286
298
|
Here are the existing contents of your todo list:
|
|
287
299
|
|
|
288
|
-
{model.todo_list_str(session.todos)}</system-reminder>"""
|
|
300
|
+
{model.todo_list_str(session.todos)}</system-reminder>"""
|
|
301
|
+
),
|
|
289
302
|
todo_use=True,
|
|
290
303
|
)
|
|
291
304
|
|
|
@@ -296,10 +309,10 @@ Here are the existing contents of your todo list:
|
|
|
296
309
|
|
|
297
310
|
async def file_changed_externally_reminder(
|
|
298
311
|
session: Session,
|
|
299
|
-
) ->
|
|
312
|
+
) -> message.DeveloperMessage | None:
|
|
300
313
|
"""Remind agent about user/linter' changes to the files in FileTracker, provding the newest content of the file."""
|
|
301
|
-
changed_files: list[tuple[str, str, list[
|
|
302
|
-
collected_images: list[
|
|
314
|
+
changed_files: list[tuple[str, str, list[message.ImageURLPart] | None]] = []
|
|
315
|
+
collected_images: list[message.ImageURLPart] = []
|
|
303
316
|
if session.file_tracker and len(session.file_tracker) > 0:
|
|
304
317
|
for path, status in session.file_tracker.items():
|
|
305
318
|
try:
|
|
@@ -320,9 +333,10 @@ async def file_changed_externally_reminder(
|
|
|
320
333
|
ReadTool.ReadArguments(file_path=path)
|
|
321
334
|
) # This tool will update file tracker
|
|
322
335
|
if tool_result.status == "success":
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
336
|
+
images = [part for part in tool_result.parts if isinstance(part, message.ImageURLPart)]
|
|
337
|
+
changed_files.append((path, tool_result.output_text, images or None))
|
|
338
|
+
if images:
|
|
339
|
+
collected_images.extend(images)
|
|
326
340
|
finally:
|
|
327
341
|
reset_tool_context(context_token)
|
|
328
342
|
except (
|
|
@@ -341,10 +355,12 @@ async def file_changed_externally_reminder(
|
|
|
341
355
|
for file_path, file_content, _ in changed_files
|
|
342
356
|
]
|
|
343
357
|
)
|
|
344
|
-
return
|
|
345
|
-
|
|
358
|
+
return message.DeveloperMessage(
|
|
359
|
+
parts=message.parts_from_text_and_images(
|
|
360
|
+
f"""<system-reminder>{changed_files_str}""",
|
|
361
|
+
collected_images or None,
|
|
362
|
+
),
|
|
346
363
|
external_file_changes=[file_path for file_path, _, _ in changed_files],
|
|
347
|
-
images=collected_images or None,
|
|
348
364
|
)
|
|
349
365
|
|
|
350
366
|
return None
|
|
@@ -393,26 +409,28 @@ class Memory(BaseModel):
|
|
|
393
409
|
def get_last_user_message_image_count(session: Session) -> int:
|
|
394
410
|
"""Get image count from the last user message in conversation history."""
|
|
395
411
|
for item in reversed(session.conversation_history):
|
|
396
|
-
if isinstance(item,
|
|
412
|
+
if isinstance(item, message.ToolResultMessage):
|
|
397
413
|
return 0
|
|
398
|
-
if isinstance(item,
|
|
399
|
-
return len(item.
|
|
414
|
+
if isinstance(item, message.UserMessage):
|
|
415
|
+
return len([part for part in item.parts if isinstance(part, message.ImageURLPart)])
|
|
400
416
|
return 0
|
|
401
417
|
|
|
402
418
|
|
|
403
|
-
async def image_reminder(session: Session) ->
|
|
419
|
+
async def image_reminder(session: Session) -> message.DeveloperMessage | None:
|
|
404
420
|
"""Remind agent about images attached by user in the last message."""
|
|
405
421
|
image_count = get_last_user_message_image_count(session)
|
|
406
422
|
if image_count == 0:
|
|
407
423
|
return None
|
|
408
424
|
|
|
409
|
-
return
|
|
410
|
-
|
|
425
|
+
return message.DeveloperMessage(
|
|
426
|
+
parts=message.text_parts_from_str(
|
|
427
|
+
f"<system-reminder>User attached {image_count} image{'s' if image_count > 1 else ''} in their message. Make sure to analyze and reference these images as needed.</system-reminder>"
|
|
428
|
+
),
|
|
411
429
|
user_image_count=image_count,
|
|
412
430
|
)
|
|
413
431
|
|
|
414
432
|
|
|
415
|
-
async def skill_reminder(session: Session) ->
|
|
433
|
+
async def skill_reminder(session: Session) -> message.DeveloperMessage | None:
|
|
416
434
|
"""Load skill content when user references a skill with $skill syntax."""
|
|
417
435
|
skill_name = get_skill_from_user_input(session)
|
|
418
436
|
if not skill_name:
|
|
@@ -436,8 +454,8 @@ async def skill_reminder(session: Session) -> model.DeveloperMessageItem | None:
|
|
|
436
454
|
</skill>
|
|
437
455
|
</system-reminder>"""
|
|
438
456
|
|
|
439
|
-
return
|
|
440
|
-
|
|
457
|
+
return message.DeveloperMessage(
|
|
458
|
+
parts=message.text_parts_from_str(content),
|
|
441
459
|
skill_name=skill.name,
|
|
442
460
|
)
|
|
443
461
|
|
|
@@ -461,7 +479,7 @@ def _mark_memory_loaded(session: Session, path: str) -> None:
|
|
|
461
479
|
session.file_tracker[path] = model.FileStatus(mtime=mtime, content_sha256=content_sha256, is_memory=True)
|
|
462
480
|
|
|
463
481
|
|
|
464
|
-
async def memory_reminder(session: Session) ->
|
|
482
|
+
async def memory_reminder(session: Session) -> message.DeveloperMessage | None:
|
|
465
483
|
"""CLAUDE.md AGENTS.md"""
|
|
466
484
|
memory_paths = get_memory_paths()
|
|
467
485
|
memories: list[Memory] = []
|
|
@@ -485,8 +503,9 @@ async def memory_reminder(session: Session) -> model.DeveloperMessageItem | None
|
|
|
485
503
|
if patterns:
|
|
486
504
|
memory_mentioned[memory.path] = patterns
|
|
487
505
|
|
|
488
|
-
return
|
|
489
|
-
|
|
506
|
+
return message.DeveloperMessage(
|
|
507
|
+
parts=message.text_parts_from_str(
|
|
508
|
+
f"""<system-reminder>As you answer the user's questions, you can use the following context:
|
|
490
509
|
|
|
491
510
|
# claudeMd
|
|
492
511
|
Codebase and user instructions are shown below. Be sure to adhere to these instructions. IMPORTANT: These instructions OVERRIDE any default behavior and you MUST follow them exactly as written.
|
|
@@ -499,19 +518,17 @@ ALWAYS prefer editing an existing file to creating a new one.
|
|
|
499
518
|
NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User.
|
|
500
519
|
|
|
501
520
|
IMPORTANT: this context may or may not be relevant to your tasks. You should not respond to this context unless it is highly relevant to your task.
|
|
502
|
-
</system-reminder>"""
|
|
521
|
+
</system-reminder>"""
|
|
522
|
+
),
|
|
503
523
|
memory_paths=[memory.path for memory in memories],
|
|
504
524
|
memory_mentioned=memory_mentioned or None,
|
|
505
525
|
)
|
|
506
526
|
return None
|
|
507
527
|
|
|
508
528
|
|
|
509
|
-
MEMORY_FILE_NAMES = ["CLAUDE.md", "AGENTS.md", "AGENT.md"]
|
|
510
|
-
|
|
511
|
-
|
|
512
529
|
async def last_path_memory_reminder(
|
|
513
530
|
session: Session,
|
|
514
|
-
) ->
|
|
531
|
+
) -> message.DeveloperMessage | None:
|
|
515
532
|
"""Load CLAUDE.md/AGENTS.md from directories containing files in file_tracker.
|
|
516
533
|
|
|
517
534
|
Uses session.file_tracker to detect accessed paths (works for both tool calls
|
|
@@ -579,9 +596,11 @@ async def last_path_memory_reminder(
|
|
|
579
596
|
if patterns:
|
|
580
597
|
memory_mentioned[memory.path] = patterns
|
|
581
598
|
|
|
582
|
-
return
|
|
583
|
-
|
|
584
|
-
|
|
599
|
+
return message.DeveloperMessage(
|
|
600
|
+
parts=message.text_parts_from_str(
|
|
601
|
+
f"""<system-reminder>{memories_str}
|
|
602
|
+
</system-reminder>"""
|
|
603
|
+
),
|
|
585
604
|
memory_paths=[memory.path for memory in memories],
|
|
586
605
|
memory_mentioned=memory_mentioned or None,
|
|
587
606
|
)
|