klaude-code 2.9.0__py3-none-any.whl → 2.10.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/app/runtime.py +1 -1
- klaude_code/auth/antigravity/oauth.py +33 -29
- klaude_code/auth/claude/oauth.py +34 -49
- klaude_code/cli/cost_cmd.py +4 -4
- klaude_code/cli/list_model.py +1 -2
- klaude_code/config/assets/builtin_config.yaml +17 -0
- klaude_code/const.py +4 -3
- klaude_code/core/agent_profile.py +2 -5
- klaude_code/core/bash_mode.py +276 -0
- klaude_code/core/executor.py +40 -7
- klaude_code/core/manager/llm_clients.py +1 -0
- klaude_code/core/manager/llm_clients_builder.py +2 -2
- klaude_code/core/memory.py +140 -0
- klaude_code/core/reminders.py +17 -89
- klaude_code/core/task.py +1 -1
- klaude_code/core/tool/file/read_tool.py +13 -2
- klaude_code/core/tool/shell/bash_tool.py +1 -1
- klaude_code/core/turn.py +10 -4
- klaude_code/llm/bedrock_anthropic/__init__.py +3 -0
- klaude_code/llm/input_common.py +18 -0
- klaude_code/llm/{codex → openai_codex}/__init__.py +1 -1
- klaude_code/llm/{codex → openai_codex}/client.py +3 -3
- klaude_code/llm/openai_compatible/client.py +3 -1
- klaude_code/llm/openai_compatible/stream.py +19 -9
- klaude_code/llm/{responses → openai_responses}/client.py +1 -1
- klaude_code/llm/registry.py +3 -3
- klaude_code/llm/stream_parts.py +3 -1
- klaude_code/llm/usage.py +1 -1
- klaude_code/protocol/events.py +17 -1
- klaude_code/protocol/message.py +1 -0
- klaude_code/protocol/model.py +14 -1
- klaude_code/protocol/op.py +12 -0
- klaude_code/protocol/op_handler.py +5 -0
- klaude_code/session/session.py +22 -1
- klaude_code/tui/command/resume_cmd.py +1 -1
- klaude_code/tui/commands.py +15 -0
- klaude_code/tui/components/bash_syntax.py +4 -0
- klaude_code/tui/components/command_output.py +4 -5
- klaude_code/tui/components/developer.py +1 -3
- klaude_code/tui/components/diffs.py +3 -2
- klaude_code/tui/components/metadata.py +23 -26
- klaude_code/tui/components/rich/code_panel.py +31 -16
- klaude_code/tui/components/rich/markdown.py +44 -28
- klaude_code/tui/components/rich/status.py +2 -2
- klaude_code/tui/components/rich/theme.py +28 -16
- klaude_code/tui/components/tools.py +23 -0
- klaude_code/tui/components/user_input.py +49 -58
- klaude_code/tui/components/welcome.py +47 -2
- klaude_code/tui/display.py +15 -7
- klaude_code/tui/input/completers.py +8 -0
- klaude_code/tui/input/key_bindings.py +37 -1
- klaude_code/tui/input/prompt_toolkit.py +58 -31
- klaude_code/tui/machine.py +87 -49
- klaude_code/tui/renderer.py +148 -30
- klaude_code/tui/runner.py +22 -0
- klaude_code/tui/terminal/image.py +24 -3
- klaude_code/tui/terminal/notifier.py +11 -12
- klaude_code/tui/terminal/selector.py +1 -1
- klaude_code/ui/terminal/title.py +4 -2
- {klaude_code-2.9.0.dist-info → klaude_code-2.10.0.dist-info}/METADATA +1 -1
- {klaude_code-2.9.0.dist-info → klaude_code-2.10.0.dist-info}/RECORD +67 -66
- klaude_code/llm/bedrock/__init__.py +0 -3
- klaude_code/tui/components/assistant.py +0 -2
- /klaude_code/llm/{bedrock → bedrock_anthropic}/client.py +0 -0
- /klaude_code/llm/{codex → openai_codex}/prompt_sync.py +0 -0
- /klaude_code/llm/{responses → openai_responses}/__init__.py +0 -0
- /klaude_code/llm/{responses → openai_responses}/input.py +0 -0
- {klaude_code-2.9.0.dist-info → klaude_code-2.10.0.dist-info}/WHEEL +0 -0
- {klaude_code-2.9.0.dist-info → klaude_code-2.10.0.dist-info}/entry_points.txt +0 -0
klaude_code/core/executor.py
CHANGED
|
@@ -18,9 +18,11 @@ from klaude_code.config import load_config
|
|
|
18
18
|
from klaude_code.config.sub_agent_model_helper import SubAgentModelHelper
|
|
19
19
|
from klaude_code.core.agent import Agent
|
|
20
20
|
from klaude_code.core.agent_profile import DefaultModelProfileProvider, ModelProfileProvider
|
|
21
|
+
from klaude_code.core.bash_mode import run_bash_command
|
|
21
22
|
from klaude_code.core.compaction import CompactionReason, run_compaction
|
|
22
23
|
from klaude_code.core.loaded_skills import get_loaded_skill_names_by_location
|
|
23
24
|
from klaude_code.core.manager import LLMClients, SubAgentManager
|
|
25
|
+
from klaude_code.core.memory import get_existing_memory_paths_by_location
|
|
24
26
|
from klaude_code.llm.registry import create_llm_client
|
|
25
27
|
from klaude_code.log import DebugType, log_debug
|
|
26
28
|
from klaude_code.protocol import commands, events, message, model, op
|
|
@@ -134,18 +136,19 @@ class AgentRuntime:
|
|
|
134
136
|
compact_llm_client=self._llm_clients.compact,
|
|
135
137
|
)
|
|
136
138
|
|
|
137
|
-
async for evt in agent.replay_history():
|
|
138
|
-
await self._emit_event(evt)
|
|
139
|
-
|
|
140
139
|
await self._emit_event(
|
|
141
140
|
events.WelcomeEvent(
|
|
142
141
|
session_id=session.id,
|
|
143
142
|
work_dir=str(session.work_dir),
|
|
144
143
|
llm_config=self._llm_clients.main.get_llm_config(),
|
|
145
144
|
loaded_skills=get_loaded_skill_names_by_location(),
|
|
145
|
+
loaded_memories=get_existing_memory_paths_by_location(work_dir=session.work_dir),
|
|
146
146
|
)
|
|
147
147
|
)
|
|
148
148
|
|
|
149
|
+
async for evt in agent.replay_history():
|
|
150
|
+
await self._emit_event(evt)
|
|
151
|
+
|
|
149
152
|
self._agent = agent
|
|
150
153
|
log_debug(
|
|
151
154
|
f"Initialized agent for session: {session.id}",
|
|
@@ -179,6 +182,23 @@ class AgentRuntime:
|
|
|
179
182
|
)
|
|
180
183
|
self._task_manager.register(operation.id, task, operation.session_id)
|
|
181
184
|
|
|
185
|
+
async def run_bash(self, operation: op.RunBashOperation) -> None:
|
|
186
|
+
agent = await self.ensure_agent(operation.session_id)
|
|
187
|
+
|
|
188
|
+
existing_active = self._task_manager.get(operation.id)
|
|
189
|
+
if existing_active is not None and not existing_active.task.done():
|
|
190
|
+
raise RuntimeError(f"Active task already registered for operation {operation.id}")
|
|
191
|
+
|
|
192
|
+
task: asyncio.Task[None] = asyncio.create_task(
|
|
193
|
+
self._run_bash_task(
|
|
194
|
+
session=agent.session,
|
|
195
|
+
command=operation.command,
|
|
196
|
+
task_id=operation.id,
|
|
197
|
+
session_id=operation.session_id,
|
|
198
|
+
)
|
|
199
|
+
)
|
|
200
|
+
self._task_manager.register(operation.id, task, operation.session_id)
|
|
201
|
+
|
|
182
202
|
async def continue_agent(self, operation: op.ContinueAgentOperation) -> None:
|
|
183
203
|
"""Continue agent execution without adding a new user message."""
|
|
184
204
|
agent = await self.ensure_agent(operation.session_id)
|
|
@@ -230,6 +250,7 @@ class AgentRuntime:
|
|
|
230
250
|
work_dir=str(agent.session.work_dir),
|
|
231
251
|
llm_config=self._llm_clients.main.get_llm_config(),
|
|
232
252
|
loaded_skills=get_loaded_skill_names_by_location(),
|
|
253
|
+
loaded_memories=get_existing_memory_paths_by_location(work_dir=agent.session.work_dir),
|
|
233
254
|
)
|
|
234
255
|
)
|
|
235
256
|
|
|
@@ -249,18 +270,19 @@ class AgentRuntime:
|
|
|
249
270
|
compact_llm_client=self._llm_clients.compact,
|
|
250
271
|
)
|
|
251
272
|
|
|
252
|
-
async for evt in agent.replay_history():
|
|
253
|
-
await self._emit_event(evt)
|
|
254
|
-
|
|
255
273
|
await self._emit_event(
|
|
256
274
|
events.WelcomeEvent(
|
|
257
275
|
session_id=target_session.id,
|
|
258
276
|
work_dir=str(target_session.work_dir),
|
|
259
277
|
llm_config=self._llm_clients.main.get_llm_config(),
|
|
260
278
|
loaded_skills=get_loaded_skill_names_by_location(),
|
|
279
|
+
loaded_memories=get_existing_memory_paths_by_location(work_dir=target_session.work_dir),
|
|
261
280
|
)
|
|
262
281
|
)
|
|
263
282
|
|
|
283
|
+
async for evt in agent.replay_history():
|
|
284
|
+
await self._emit_event(evt)
|
|
285
|
+
|
|
264
286
|
self._agent = agent
|
|
265
287
|
log_debug(
|
|
266
288
|
f"Resumed session: {target_session.id}",
|
|
@@ -359,6 +381,14 @@ class AgentRuntime:
|
|
|
359
381
|
debug_type=DebugType.EXECUTION,
|
|
360
382
|
)
|
|
361
383
|
|
|
384
|
+
async def _run_bash_task(self, *, session: Session, command: str, task_id: str, session_id: str) -> None:
|
|
385
|
+
await run_bash_command(
|
|
386
|
+
emit_event=self._emit_event,
|
|
387
|
+
session=session,
|
|
388
|
+
session_id=session_id,
|
|
389
|
+
command=command,
|
|
390
|
+
)
|
|
391
|
+
|
|
362
392
|
async def _run_compaction_task(
|
|
363
393
|
self,
|
|
364
394
|
agent: Agent,
|
|
@@ -467,7 +497,7 @@ class ModelSwitcher:
|
|
|
467
497
|
config.main_model = model_name
|
|
468
498
|
await config.save()
|
|
469
499
|
|
|
470
|
-
return llm_config,
|
|
500
|
+
return llm_config, model_name
|
|
471
501
|
|
|
472
502
|
def change_thinking(self, agent: Agent, *, thinking: Thinking) -> Thinking | None:
|
|
473
503
|
"""Apply thinking configuration to the agent's active LLM config and persisted session."""
|
|
@@ -540,6 +570,9 @@ class ExecutorContext:
|
|
|
540
570
|
async def handle_run_agent(self, operation: op.RunAgentOperation) -> None:
|
|
541
571
|
await self._agent_runtime.run_agent(operation)
|
|
542
572
|
|
|
573
|
+
async def handle_run_bash(self, operation: op.RunBashOperation) -> None:
|
|
574
|
+
await self._agent_runtime.run_bash(operation)
|
|
575
|
+
|
|
543
576
|
async def handle_continue_agent(self, operation: op.ContinueAgentOperation) -> None:
|
|
544
577
|
await self._agent_runtime.continue_agent(operation)
|
|
545
578
|
|
|
@@ -19,6 +19,7 @@ class LLMClients:
|
|
|
19
19
|
"""Container for LLM clients used by main agent and sub-agents."""
|
|
20
20
|
|
|
21
21
|
main: LLMClientABC
|
|
22
|
+
main_model_alias: str = ""
|
|
22
23
|
sub_clients: dict[SubAgentType, LLMClientABC] = dataclass_field(default_factory=_default_sub_clients)
|
|
23
24
|
compact: LLMClientABC | None = None
|
|
24
25
|
|
|
@@ -53,7 +53,7 @@ def build_llm_clients(
|
|
|
53
53
|
compact_client = create_llm_client(compact_llm_config)
|
|
54
54
|
|
|
55
55
|
if skip_sub_agents:
|
|
56
|
-
return LLMClients(main=main_client, compact=compact_client)
|
|
56
|
+
return LLMClients(main=main_client, main_model_alias=model_name, compact=compact_client)
|
|
57
57
|
|
|
58
58
|
helper = SubAgentModelHelper(config)
|
|
59
59
|
sub_agent_configs = helper.build_sub_agent_client_configs()
|
|
@@ -63,4 +63,4 @@ def build_llm_clients(
|
|
|
63
63
|
sub_llm_config = config.get_model_config(sub_model_name)
|
|
64
64
|
sub_clients[sub_agent_type] = create_llm_client(sub_llm_config)
|
|
65
65
|
|
|
66
|
-
return LLMClients(main=main_client, sub_clients=sub_clients, compact=compact_client)
|
|
66
|
+
return LLMClients(main=main_client, main_model_alias=model_name, sub_clients=sub_clients, compact=compact_client)
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Memory file loading and management.
|
|
2
|
+
|
|
3
|
+
This module handles CLAUDE.md and AGENTS.md memory files - discovery, loading,
|
|
4
|
+
and providing summaries for UI display.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel
|
|
11
|
+
|
|
12
|
+
MEMORY_FILE_NAMES = ["CLAUDE.md", "AGENTS.md", "AGENT.md"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Memory(BaseModel):
|
|
16
|
+
"""Represents a loaded memory file."""
|
|
17
|
+
|
|
18
|
+
path: str
|
|
19
|
+
instruction: str
|
|
20
|
+
content: str
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_memory_paths(*, work_dir: Path) -> list[tuple[Path, str]]:
|
|
24
|
+
"""Return all possible memory file paths with their descriptions."""
|
|
25
|
+
user_dirs = [Path.home() / ".claude", Path.home() / ".codex"]
|
|
26
|
+
project_dirs = [work_dir, work_dir / ".claude"]
|
|
27
|
+
|
|
28
|
+
paths: list[tuple[Path, str]] = []
|
|
29
|
+
for d in user_dirs:
|
|
30
|
+
for fname in MEMORY_FILE_NAMES:
|
|
31
|
+
paths.append((d / fname, "user's private global instructions for all projects"))
|
|
32
|
+
for d in project_dirs:
|
|
33
|
+
for fname in MEMORY_FILE_NAMES:
|
|
34
|
+
paths.append((d / fname, "project instructions, checked into the codebase"))
|
|
35
|
+
return paths
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_existing_memory_files(*, work_dir: Path) -> dict[str, list[str]]:
|
|
39
|
+
"""Return existing memory file paths grouped by location (user/project)."""
|
|
40
|
+
result: dict[str, list[str]] = {"user": [], "project": []}
|
|
41
|
+
work_dir = work_dir.resolve()
|
|
42
|
+
|
|
43
|
+
for memory_path, _instruction in get_memory_paths(work_dir=work_dir):
|
|
44
|
+
if memory_path.exists() and memory_path.is_file():
|
|
45
|
+
path_str = str(memory_path)
|
|
46
|
+
resolved = memory_path.resolve()
|
|
47
|
+
try:
|
|
48
|
+
resolved.relative_to(work_dir)
|
|
49
|
+
result["project"].append(path_str)
|
|
50
|
+
except ValueError:
|
|
51
|
+
result["user"].append(path_str)
|
|
52
|
+
|
|
53
|
+
return result
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def get_existing_memory_paths_by_location(*, work_dir: Path) -> dict[str, list[str]]:
|
|
57
|
+
"""Return existing memory file paths grouped by location for WelcomeEvent."""
|
|
58
|
+
result = get_existing_memory_files(work_dir=work_dir)
|
|
59
|
+
if not result.get("user") and not result.get("project"):
|
|
60
|
+
return {}
|
|
61
|
+
return result
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def format_memory_content(memory: Memory) -> str:
|
|
65
|
+
"""Format a single memory file content for display."""
|
|
66
|
+
return f"Contents of {memory.path} ({memory.instruction}):\n\n{memory.content}"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def format_memories_reminder(memories: list[Memory], include_header: bool = True) -> str:
|
|
70
|
+
"""Format memory files into a system reminder string."""
|
|
71
|
+
memories_str = "\n\n".join(format_memory_content(m) for m in memories)
|
|
72
|
+
if include_header:
|
|
73
|
+
return f"""<system-reminder>
|
|
74
|
+
Loaded memory files. Follow these instructions. Do not mention them to the user unless explicitly asked.
|
|
75
|
+
|
|
76
|
+
{memories_str}
|
|
77
|
+
</system-reminder>"""
|
|
78
|
+
return f"<system-reminder>{memories_str}\n</system-reminder>"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def discover_memory_files_near_paths(
|
|
82
|
+
paths: list[str],
|
|
83
|
+
*,
|
|
84
|
+
work_dir: Path,
|
|
85
|
+
is_memory_loaded: Callable[[str], bool],
|
|
86
|
+
mark_memory_loaded: Callable[[str], None],
|
|
87
|
+
) -> list[Memory]:
|
|
88
|
+
"""Discover and load CLAUDE.md/AGENTS.md from directories containing accessed files.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
paths: List of file paths that have been accessed.
|
|
92
|
+
is_memory_loaded: Callback to check if a memory file is already loaded.
|
|
93
|
+
mark_memory_loaded: Callback to mark a memory file as loaded.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
List of newly discovered Memory objects.
|
|
97
|
+
"""
|
|
98
|
+
memories: list[Memory] = []
|
|
99
|
+
work_dir = work_dir.resolve()
|
|
100
|
+
seen_memory_files: set[str] = set()
|
|
101
|
+
|
|
102
|
+
for p_str in paths:
|
|
103
|
+
p = Path(p_str)
|
|
104
|
+
full = (work_dir / p).resolve() if not p.is_absolute() else p.resolve()
|
|
105
|
+
try:
|
|
106
|
+
_ = full.relative_to(work_dir)
|
|
107
|
+
except ValueError:
|
|
108
|
+
continue
|
|
109
|
+
|
|
110
|
+
deepest_dir = full if full.is_dir() else full.parent
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
rel_parts = deepest_dir.relative_to(work_dir).parts
|
|
114
|
+
except ValueError:
|
|
115
|
+
continue
|
|
116
|
+
|
|
117
|
+
current_dir = work_dir
|
|
118
|
+
for part in rel_parts:
|
|
119
|
+
current_dir = current_dir / part
|
|
120
|
+
for fname in MEMORY_FILE_NAMES:
|
|
121
|
+
mem_path = current_dir / fname
|
|
122
|
+
mem_path_str = str(mem_path)
|
|
123
|
+
if mem_path_str in seen_memory_files or is_memory_loaded(mem_path_str):
|
|
124
|
+
continue
|
|
125
|
+
if mem_path.exists() and mem_path.is_file():
|
|
126
|
+
try:
|
|
127
|
+
text = mem_path.read_text(encoding="utf-8", errors="replace")
|
|
128
|
+
except (PermissionError, UnicodeDecodeError, OSError):
|
|
129
|
+
continue
|
|
130
|
+
mark_memory_loaded(mem_path_str)
|
|
131
|
+
seen_memory_files.add(mem_path_str)
|
|
132
|
+
memories.append(
|
|
133
|
+
Memory(
|
|
134
|
+
path=mem_path_str,
|
|
135
|
+
instruction="project instructions, discovered near last accessed path",
|
|
136
|
+
content=text,
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
return memories
|
klaude_code/core/reminders.py
CHANGED
|
@@ -4,9 +4,13 @@ import shlex
|
|
|
4
4
|
from dataclasses import dataclass
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
|
|
7
|
-
from
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
from klaude_code.const import REMINDER_COOLDOWN_TURNS, TODO_REMINDER_TOOL_CALL_THRESHOLD
|
|
8
|
+
from klaude_code.core.memory import (
|
|
9
|
+
Memory,
|
|
10
|
+
discover_memory_files_near_paths,
|
|
11
|
+
format_memories_reminder,
|
|
12
|
+
get_memory_paths,
|
|
13
|
+
)
|
|
10
14
|
from klaude_code.core.tool import BashTool, ReadTool, build_todo_context
|
|
11
15
|
from klaude_code.core.tool.context import ToolContext
|
|
12
16
|
from klaude_code.core.tool.file._utils import hash_text_sha256
|
|
@@ -382,28 +386,6 @@ def _compute_file_content_sha256(path: str) -> str | None:
|
|
|
382
386
|
return None
|
|
383
387
|
|
|
384
388
|
|
|
385
|
-
def get_memory_paths() -> list[tuple[Path, str]]:
|
|
386
|
-
return [
|
|
387
|
-
(
|
|
388
|
-
Path.home() / ".claude" / "CLAUDE.md",
|
|
389
|
-
"user's private global instructions for all projects",
|
|
390
|
-
),
|
|
391
|
-
(
|
|
392
|
-
Path.home() / ".codex" / "AGENTS.md",
|
|
393
|
-
"user's private global instructions for all projects",
|
|
394
|
-
),
|
|
395
|
-
(Path.cwd() / "AGENTS.md", "project instructions, checked into the codebase"),
|
|
396
|
-
(Path.cwd() / "CLAUDE.md", "project instructions, checked into the codebase"),
|
|
397
|
-
(Path.cwd() / ".claude" / "CLAUDE.md", "project instructions, checked into the codebase"),
|
|
398
|
-
]
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
class Memory(BaseModel):
|
|
402
|
-
path: str
|
|
403
|
-
instruction: str
|
|
404
|
-
content: str
|
|
405
|
-
|
|
406
|
-
|
|
407
389
|
def get_last_user_message_image_paths(session: Session) -> list[str]:
|
|
408
390
|
"""Get image file paths from the last user message in conversation history."""
|
|
409
391
|
for item in reversed(session.conversation_history):
|
|
@@ -502,7 +484,7 @@ def _mark_memory_loaded(session: Session, path: str) -> None:
|
|
|
502
484
|
|
|
503
485
|
async def memory_reminder(session: Session) -> message.DeveloperMessage | None:
|
|
504
486
|
"""CLAUDE.md AGENTS.md"""
|
|
505
|
-
memory_paths = get_memory_paths()
|
|
487
|
+
memory_paths = get_memory_paths(work_dir=session.work_dir)
|
|
506
488
|
memories: list[Memory] = []
|
|
507
489
|
for memory_path, instruction in memory_paths:
|
|
508
490
|
path_str = str(memory_path)
|
|
@@ -514,21 +496,12 @@ async def memory_reminder(session: Session) -> message.DeveloperMessage | None:
|
|
|
514
496
|
except (PermissionError, UnicodeDecodeError, OSError):
|
|
515
497
|
continue
|
|
516
498
|
if len(memories) > 0:
|
|
517
|
-
memories_str = "\n\n".join(
|
|
518
|
-
[f"Contents of {memory.path} ({memory.instruction}):\n\n{memory.content}" for memory in memories]
|
|
519
|
-
)
|
|
520
499
|
loaded_files = [
|
|
521
500
|
model.MemoryFileLoaded(path=memory.path, mentioned_patterns=_extract_at_patterns(memory.content))
|
|
522
501
|
for memory in memories
|
|
523
502
|
]
|
|
524
503
|
return message.DeveloperMessage(
|
|
525
|
-
parts=message.text_parts_from_str(
|
|
526
|
-
f"""<system-reminder>
|
|
527
|
-
Loaded memory files. Follow these instructions. Do not mention them to the user unless explicitly asked.
|
|
528
|
-
|
|
529
|
-
{memories_str}
|
|
530
|
-
</system-reminder>"""
|
|
531
|
-
),
|
|
504
|
+
parts=message.text_parts_from_str(format_memories_reminder(memories, include_header=True)),
|
|
532
505
|
ui_extra=model.DeveloperUIExtra(items=[model.MemoryLoadedUIItem(files=loaded_files)]),
|
|
533
506
|
)
|
|
534
507
|
return None
|
|
@@ -546,68 +519,23 @@ async def last_path_memory_reminder(
|
|
|
546
519
|
return None
|
|
547
520
|
|
|
548
521
|
paths = list(session.file_tracker.keys())
|
|
549
|
-
memories
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
p = Path(p_str)
|
|
556
|
-
full = (cwd / p).resolve() if not p.is_absolute() else p.resolve()
|
|
557
|
-
try:
|
|
558
|
-
_ = full.relative_to(cwd)
|
|
559
|
-
except ValueError:
|
|
560
|
-
# Not under cwd; skip
|
|
561
|
-
continue
|
|
562
|
-
|
|
563
|
-
# Determine the deepest directory to scan (file parent or directory itself)
|
|
564
|
-
deepest_dir = full if full.is_dir() else full.parent
|
|
565
|
-
|
|
566
|
-
# Iterate each directory level from cwd to deepest_dir
|
|
567
|
-
try:
|
|
568
|
-
rel_parts = deepest_dir.relative_to(cwd).parts
|
|
569
|
-
except ValueError:
|
|
570
|
-
# Shouldn't happen due to check above, but guard anyway
|
|
571
|
-
continue
|
|
572
|
-
|
|
573
|
-
current_dir = cwd
|
|
574
|
-
for part in rel_parts:
|
|
575
|
-
current_dir = current_dir / part
|
|
576
|
-
for fname in MEMORY_FILE_NAMES:
|
|
577
|
-
mem_path = current_dir / fname
|
|
578
|
-
mem_path_str = str(mem_path)
|
|
579
|
-
if mem_path_str in seen_memory_files or _is_memory_loaded(session, mem_path_str):
|
|
580
|
-
continue
|
|
581
|
-
if mem_path.exists() and mem_path.is_file():
|
|
582
|
-
try:
|
|
583
|
-
text = mem_path.read_text(encoding="utf-8", errors="replace")
|
|
584
|
-
except (PermissionError, UnicodeDecodeError, OSError):
|
|
585
|
-
continue
|
|
586
|
-
_mark_memory_loaded(session, mem_path_str)
|
|
587
|
-
seen_memory_files.add(mem_path_str)
|
|
588
|
-
memories.append(
|
|
589
|
-
Memory(
|
|
590
|
-
path=mem_path_str,
|
|
591
|
-
instruction="project instructions, discovered near last accessed path",
|
|
592
|
-
content=text,
|
|
593
|
-
)
|
|
594
|
-
)
|
|
522
|
+
memories = discover_memory_files_near_paths(
|
|
523
|
+
paths,
|
|
524
|
+
work_dir=session.work_dir,
|
|
525
|
+
is_memory_loaded=lambda p: _is_memory_loaded(session, p),
|
|
526
|
+
mark_memory_loaded=lambda p: _mark_memory_loaded(session, p),
|
|
527
|
+
)
|
|
595
528
|
|
|
596
529
|
if len(memories) > 0:
|
|
597
|
-
memories_str = "\n\n".join(
|
|
598
|
-
[f"Contents of {memory.path} ({memory.instruction}):\n\n{memory.content}" for memory in memories]
|
|
599
|
-
)
|
|
600
530
|
loaded_files = [
|
|
601
531
|
model.MemoryFileLoaded(path=memory.path, mentioned_patterns=_extract_at_patterns(memory.content))
|
|
602
532
|
for memory in memories
|
|
603
533
|
]
|
|
604
534
|
return message.DeveloperMessage(
|
|
605
|
-
parts=message.text_parts_from_str(
|
|
606
|
-
f"""<system-reminder>{memories_str}
|
|
607
|
-
</system-reminder>"""
|
|
608
|
-
),
|
|
535
|
+
parts=message.text_parts_from_str(format_memories_reminder(memories, include_header=False)),
|
|
609
536
|
ui_extra=model.DeveloperUIExtra(items=[model.MemoryLoadedUIItem(files=loaded_files)]),
|
|
610
537
|
)
|
|
538
|
+
return None
|
|
611
539
|
|
|
612
540
|
|
|
613
541
|
ALL_REMINDERS = [
|
klaude_code/core/task.py
CHANGED
|
@@ -210,7 +210,7 @@ class TaskExecutor:
|
|
|
210
210
|
accumulated = self._metadata_accumulator.get_partial_item(task_duration_s)
|
|
211
211
|
if accumulated is not None:
|
|
212
212
|
session_id = self._context.session_ctx.session_id
|
|
213
|
-
ui_events.append(events.TaskMetadataEvent(metadata=accumulated, session_id=session_id
|
|
213
|
+
ui_events.append(events.TaskMetadataEvent(metadata=accumulated, session_id=session_id))
|
|
214
214
|
self._context.session_ctx.append_history([accumulated])
|
|
215
215
|
|
|
216
216
|
return ui_events
|
|
@@ -22,7 +22,7 @@ from klaude_code.core.tool.file._utils import file_exists, is_directory
|
|
|
22
22
|
from klaude_code.core.tool.tool_abc import ToolABC, load_desc
|
|
23
23
|
from klaude_code.core.tool.tool_registry import register
|
|
24
24
|
from klaude_code.protocol import llm_param, message, model, tools
|
|
25
|
-
from klaude_code.protocol.model import ImageUIExtra
|
|
25
|
+
from klaude_code.protocol.model import ImageUIExtra, ReadPreviewLine, ReadPreviewUIExtra
|
|
26
26
|
|
|
27
27
|
_IMAGE_MIME_TYPES: dict[str, str] = {
|
|
28
28
|
".png": "image/png",
|
|
@@ -346,4 +346,15 @@ class ReadTool(ToolABC):
|
|
|
346
346
|
read_result_str = "\n".join(lines_out)
|
|
347
347
|
_track_file_access(context.file_tracker, file_path, content_sha256=read_result.content_sha256)
|
|
348
348
|
|
|
349
|
-
|
|
349
|
+
# When offset > 1, show a preview of the first 5 lines in UI
|
|
350
|
+
ui_extra = None
|
|
351
|
+
if args.offset is not None and args.offset > 1:
|
|
352
|
+
preview_count = 5
|
|
353
|
+
preview_lines = [
|
|
354
|
+
ReadPreviewLine(line_no=line_no, content=content)
|
|
355
|
+
for line_no, content in read_result.selected_lines[:preview_count]
|
|
356
|
+
]
|
|
357
|
+
remaining = len(read_result.selected_lines) - len(preview_lines)
|
|
358
|
+
ui_extra = ReadPreviewUIExtra(lines=preview_lines, remaining_lines=remaining)
|
|
359
|
+
|
|
360
|
+
return message.ToolResultMessage(status="success", output_text=read_result_str, ui_extra=ui_extra)
|
|
@@ -342,7 +342,7 @@ class BashTool(ToolABC):
|
|
|
342
342
|
if not combined:
|
|
343
343
|
combined = f"Command exited with code {rc}"
|
|
344
344
|
return message.ToolResultMessage(
|
|
345
|
-
status="
|
|
345
|
+
status="success",
|
|
346
346
|
# Preserve leading whitespace; only trim trailing newlines.
|
|
347
347
|
output_text=combined.rstrip("\n"),
|
|
348
348
|
)
|
klaude_code/core/turn.py
CHANGED
|
@@ -194,11 +194,17 @@ class TurnExecutor:
|
|
|
194
194
|
and self._turn_result.assistant_message is not None
|
|
195
195
|
and self._turn_result.assistant_message.parts
|
|
196
196
|
):
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
197
|
+
# Discard partial message if it only contains thinking parts
|
|
198
|
+
has_non_thinking = any(
|
|
199
|
+
not isinstance(part, message.ThinkingTextPart)
|
|
200
|
+
for part in self._turn_result.assistant_message.parts
|
|
201
201
|
)
|
|
202
|
+
if has_non_thinking:
|
|
203
|
+
session_ctx.append_history([self._turn_result.assistant_message])
|
|
204
|
+
# Add continuation prompt to avoid Anthropic thinking block requirement
|
|
205
|
+
session_ctx.append_history(
|
|
206
|
+
[message.UserMessage(parts=[message.TextPart(text="<system>continue</system>")])]
|
|
207
|
+
)
|
|
202
208
|
yield events.TurnEndEvent(session_id=session_ctx.session_id)
|
|
203
209
|
raise TurnError(self._turn_result.stream_error.error)
|
|
204
210
|
|
klaude_code/llm/input_common.py
CHANGED
|
@@ -149,6 +149,14 @@ def build_assistant_common_fields(
|
|
|
149
149
|
}
|
|
150
150
|
for tc in tool_calls
|
|
151
151
|
]
|
|
152
|
+
|
|
153
|
+
thinking_parts = [part for part in msg.parts if isinstance(part, message.ThinkingTextPart)]
|
|
154
|
+
if thinking_parts:
|
|
155
|
+
thinking_text = "".join(part.text for part in thinking_parts)
|
|
156
|
+
reasoning_field = next((p.reasoning_field for p in thinking_parts if p.reasoning_field), None)
|
|
157
|
+
if thinking_text and reasoning_field:
|
|
158
|
+
result[reasoning_field] = thinking_text
|
|
159
|
+
|
|
152
160
|
return result
|
|
153
161
|
|
|
154
162
|
|
|
@@ -185,4 +193,14 @@ def apply_config_defaults(param: "LLMCallParameter", config: "LLMConfigParameter
|
|
|
185
193
|
param.verbosity = config.verbosity
|
|
186
194
|
if param.thinking is None:
|
|
187
195
|
param.thinking = config.thinking
|
|
196
|
+
if param.modalities is None:
|
|
197
|
+
param.modalities = config.modalities
|
|
198
|
+
if param.image_config is None:
|
|
199
|
+
param.image_config = config.image_config
|
|
200
|
+
elif config.image_config is not None:
|
|
201
|
+
# Merge field-level: param overrides config defaults
|
|
202
|
+
if param.image_config.aspect_ratio is None:
|
|
203
|
+
param.image_config.aspect_ratio = config.image_config.aspect_ratio
|
|
204
|
+
if param.image_config.image_size is None:
|
|
205
|
+
param.image_config.image_size = config.image_config.image_size
|
|
188
206
|
return param
|
|
@@ -20,9 +20,9 @@ from klaude_code.const import (
|
|
|
20
20
|
)
|
|
21
21
|
from klaude_code.llm.client import LLMClientABC, LLMStreamABC
|
|
22
22
|
from klaude_code.llm.input_common import apply_config_defaults
|
|
23
|
+
from klaude_code.llm.openai_responses.client import ResponsesLLMStream
|
|
24
|
+
from klaude_code.llm.openai_responses.input import convert_history_to_input, convert_tool_schema
|
|
23
25
|
from klaude_code.llm.registry import register
|
|
24
|
-
from klaude_code.llm.responses.client import ResponsesLLMStream
|
|
25
|
-
from klaude_code.llm.responses.input import convert_history_to_input, convert_tool_schema
|
|
26
26
|
from klaude_code.llm.usage import MetadataTracker, error_llm_stream
|
|
27
27
|
from klaude_code.log import DebugType, log_debug
|
|
28
28
|
from klaude_code.protocol import llm_param
|
|
@@ -164,7 +164,7 @@ def _is_invalid_instruction_error(e: Exception) -> bool:
|
|
|
164
164
|
|
|
165
165
|
def _invalidate_prompt_cache_for_model(model_id: str) -> None:
|
|
166
166
|
"""Invalidate the cached prompt for a model to force refresh."""
|
|
167
|
-
from klaude_code.llm.
|
|
167
|
+
from klaude_code.llm.openai_codex.prompt_sync import invalidate_cache
|
|
168
168
|
|
|
169
169
|
log_debug(
|
|
170
170
|
f"Invalidating prompt cache for model {model_id} due to invalid instruction error",
|
|
@@ -39,9 +39,11 @@ def build_payload(param: llm_param.LLMCallParameter) -> tuple[CompletionCreatePa
|
|
|
39
39
|
"max_tokens": param.max_tokens,
|
|
40
40
|
"tools": tools,
|
|
41
41
|
"reasoning_effort": param.thinking.reasoning_effort if param.thinking else None,
|
|
42
|
-
"verbosity": param.verbosity,
|
|
43
42
|
}
|
|
44
43
|
|
|
44
|
+
if param.verbosity:
|
|
45
|
+
payload["verbosity"] = param.verbosity
|
|
46
|
+
|
|
45
47
|
return payload, extra_body
|
|
46
48
|
|
|
47
49
|
|
|
@@ -76,9 +76,11 @@ class StreamStateManager:
|
|
|
76
76
|
"""Set the response ID once received from the stream."""
|
|
77
77
|
self.response_id = response_id
|
|
78
78
|
|
|
79
|
-
def append_thinking_text(self, text: str) -> None:
|
|
79
|
+
def append_thinking_text(self, text: str, *, reasoning_field: str | None = None) -> None:
|
|
80
80
|
"""Append thinking text, merging with the previous ThinkingTextPart when possible."""
|
|
81
|
-
append_thinking_text_part(
|
|
81
|
+
append_thinking_text_part(
|
|
82
|
+
self.assistant_parts, text, model_id=self.param_model, reasoning_field=reasoning_field
|
|
83
|
+
)
|
|
82
84
|
|
|
83
85
|
def append_text(self, text: str) -> None:
|
|
84
86
|
"""Append assistant text, merging with the previous TextPart when possible."""
|
|
@@ -150,6 +152,7 @@ class ReasoningDeltaResult:
|
|
|
150
152
|
|
|
151
153
|
handled: bool
|
|
152
154
|
outputs: list[str | message.Part]
|
|
155
|
+
reasoning_field: str | None = None # Original field name: reasoning_content, reasoning, reasoning_text
|
|
153
156
|
|
|
154
157
|
|
|
155
158
|
class ReasoningHandlerABC(ABC):
|
|
@@ -168,8 +171,11 @@ class ReasoningHandlerABC(ABC):
|
|
|
168
171
|
"""Flush buffered reasoning content (usually at stage transition/finalize)."""
|
|
169
172
|
|
|
170
173
|
|
|
174
|
+
REASONING_FIELDS = ("reasoning_content", "reasoning", "reasoning_text")
|
|
175
|
+
|
|
176
|
+
|
|
171
177
|
class DefaultReasoningHandler(ReasoningHandlerABC):
|
|
172
|
-
"""Handles OpenAI-compatible reasoning fields (reasoning_content / reasoning)."""
|
|
178
|
+
"""Handles OpenAI-compatible reasoning fields (reasoning_content / reasoning / reasoning_text)."""
|
|
173
179
|
|
|
174
180
|
def __init__(
|
|
175
181
|
self,
|
|
@@ -179,16 +185,20 @@ class DefaultReasoningHandler(ReasoningHandlerABC):
|
|
|
179
185
|
) -> None:
|
|
180
186
|
self._param_model = param_model
|
|
181
187
|
self._response_id = response_id
|
|
188
|
+
self._reasoning_field: str | None = None
|
|
182
189
|
|
|
183
190
|
def set_response_id(self, response_id: str | None) -> None:
|
|
184
191
|
self._response_id = response_id
|
|
185
192
|
|
|
186
193
|
def on_delta(self, delta: object) -> ReasoningDeltaResult:
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
194
|
+
for field_name in REASONING_FIELDS:
|
|
195
|
+
content = getattr(delta, field_name, None)
|
|
196
|
+
if content:
|
|
197
|
+
if self._reasoning_field is None:
|
|
198
|
+
self._reasoning_field = field_name
|
|
199
|
+
text = str(content)
|
|
200
|
+
return ReasoningDeltaResult(handled=True, outputs=[text], reasoning_field=self._reasoning_field)
|
|
201
|
+
return ReasoningDeltaResult(handled=False, outputs=[])
|
|
192
202
|
|
|
193
203
|
def flush(self) -> list[message.Part]:
|
|
194
204
|
return []
|
|
@@ -282,7 +292,7 @@ async def parse_chat_completions_stream(
|
|
|
282
292
|
if not output:
|
|
283
293
|
continue
|
|
284
294
|
metadata_tracker.record_token()
|
|
285
|
-
state.append_thinking_text(output)
|
|
295
|
+
state.append_thinking_text(output, reasoning_field=reasoning_result.reasoning_field)
|
|
286
296
|
yield message.ThinkingTextDelta(content=output, response_id=state.response_id)
|
|
287
297
|
else:
|
|
288
298
|
state.assistant_parts.append(output)
|
|
@@ -11,8 +11,8 @@ from openai.types.responses.response_create_params import ResponseCreateParamsSt
|
|
|
11
11
|
from klaude_code.const import LLM_HTTP_TIMEOUT_CONNECT, LLM_HTTP_TIMEOUT_READ, LLM_HTTP_TIMEOUT_TOTAL
|
|
12
12
|
from klaude_code.llm.client import LLMClientABC, LLMStreamABC
|
|
13
13
|
from klaude_code.llm.input_common import apply_config_defaults
|
|
14
|
+
from klaude_code.llm.openai_responses.input import convert_history_to_input, convert_tool_schema
|
|
14
15
|
from klaude_code.llm.registry import register
|
|
15
|
-
from klaude_code.llm.responses.input import convert_history_to_input, convert_tool_schema
|
|
16
16
|
from klaude_code.llm.stream_parts import (
|
|
17
17
|
append_text_part,
|
|
18
18
|
append_thinking_text_part,
|