klaude-code 2.4.2__py3-none-any.whl → 2.5.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 +2 -6
- klaude_code/cli/main.py +0 -1
- klaude_code/config/assets/builtin_config.yaml +7 -0
- klaude_code/const.py +7 -4
- klaude_code/core/agent.py +10 -1
- klaude_code/core/agent_profile.py +47 -35
- klaude_code/core/executor.py +6 -21
- klaude_code/core/manager/sub_agent_manager.py +17 -1
- klaude_code/core/prompts/prompt-sub-agent-web.md +4 -4
- klaude_code/core/task.py +65 -4
- klaude_code/core/tool/__init__.py +0 -5
- klaude_code/core/tool/context.py +12 -1
- klaude_code/core/tool/offload.py +311 -0
- klaude_code/core/tool/shell/bash_tool.md +1 -43
- klaude_code/core/tool/sub_agent_tool.py +1 -0
- klaude_code/core/tool/todo/todo_write_tool.md +0 -23
- klaude_code/core/tool/tool_runner.py +14 -9
- klaude_code/core/tool/web/web_fetch_tool.md +1 -1
- klaude_code/core/tool/web/web_fetch_tool.py +14 -39
- klaude_code/core/turn.py +128 -139
- klaude_code/llm/anthropic/client.py +176 -82
- klaude_code/llm/bedrock/client.py +8 -12
- klaude_code/llm/claude/client.py +11 -15
- klaude_code/llm/client.py +31 -4
- klaude_code/llm/codex/client.py +7 -11
- klaude_code/llm/google/client.py +150 -69
- klaude_code/llm/openai_compatible/client.py +10 -15
- klaude_code/llm/openai_compatible/stream.py +68 -6
- klaude_code/llm/openrouter/client.py +9 -15
- klaude_code/llm/partial_message.py +35 -0
- klaude_code/llm/responses/client.py +134 -68
- klaude_code/llm/usage.py +30 -0
- klaude_code/protocol/commands.py +0 -4
- klaude_code/protocol/events/metadata.py +1 -0
- klaude_code/protocol/events/system.py +0 -4
- klaude_code/protocol/model.py +2 -15
- klaude_code/protocol/sub_agent/explore.py +0 -10
- klaude_code/protocol/sub_agent/image_gen.py +0 -7
- klaude_code/protocol/sub_agent/task.py +0 -10
- klaude_code/protocol/sub_agent/web.py +4 -12
- klaude_code/session/templates/export_session.html +4 -4
- klaude_code/skill/manager.py +2 -1
- klaude_code/tui/components/metadata.py +41 -49
- klaude_code/tui/components/rich/markdown.py +1 -3
- klaude_code/tui/components/rich/theme.py +2 -2
- klaude_code/tui/components/tools.py +0 -31
- klaude_code/tui/components/welcome.py +1 -32
- klaude_code/tui/input/prompt_toolkit.py +25 -9
- klaude_code/tui/machine.py +2 -1
- {klaude_code-2.4.2.dist-info → klaude_code-2.5.0.dist-info}/METADATA +1 -1
- {klaude_code-2.4.2.dist-info → klaude_code-2.5.0.dist-info}/RECORD +53 -53
- klaude_code/core/prompts/prompt-nano-banana.md +0 -1
- klaude_code/core/tool/truncation.py +0 -203
- {klaude_code-2.4.2.dist-info → klaude_code-2.5.0.dist-info}/WHEEL +0 -0
- {klaude_code-2.4.2.dist-info → klaude_code-2.5.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
"""Tool Output Offload & Truncation Strategies
|
|
2
|
+
==============================================
|
|
3
|
+
|
|
4
|
+
This module manages how tool outputs are truncated and offloaded to files
|
|
5
|
+
to reduce LLM context usage while preserving access to full content.
|
|
6
|
+
|
|
7
|
+
Design Principles
|
|
8
|
+
-----------------
|
|
9
|
+
Different tools have different output characteristics, so we apply
|
|
10
|
+
tool-specific strategies:
|
|
11
|
+
|
|
12
|
+
┌─────────────┬─────────────────────────┬─────────────────┬────────────────────────────┐
|
|
13
|
+
│ Tool │ Truncation Style │ Offload Policy │ Rationale │
|
|
14
|
+
├─────────────┼─────────────────────────┼─────────────────┼────────────────────────────┤
|
|
15
|
+
│ Read │ Head-focused │ Never │ Source file already exists │
|
|
16
|
+
│ │ (line/char limits) │ │ on filesystem; use offset/ │
|
|
17
|
+
│ │ │ │ limit to paginate │
|
|
18
|
+
├─────────────┼─────────────────────────┼─────────────────┼────────────────────────────┤
|
|
19
|
+
│ Others │ Head + Tail │ On threshold │ Generic fallback strategy │
|
|
20
|
+
│ │ (lines first, then │ │ (2000 lines or 40k chars) │
|
|
21
|
+
│ │ chars as fallback) │ │ │
|
|
22
|
+
└─────────────┴─────────────────────────┴─────────────────┴────────────────────────────┘
|
|
23
|
+
|
|
24
|
+
Implementation Notes
|
|
25
|
+
--------------------
|
|
26
|
+
- Read tool handles its own truncation internally (see read_tool.py)
|
|
27
|
+
- WebFetch handles its own file saving internally (see web_fetch_tool.py)
|
|
28
|
+
- All offload decisions are centralized in this module
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import secrets
|
|
34
|
+
from abc import ABC, abstractmethod
|
|
35
|
+
from dataclasses import dataclass
|
|
36
|
+
from enum import Enum, auto
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
from typing import Protocol
|
|
39
|
+
|
|
40
|
+
from klaude_code.const import (
|
|
41
|
+
TOOL_OUTPUT_DISPLAY_HEAD,
|
|
42
|
+
TOOL_OUTPUT_DISPLAY_HEAD_LINES,
|
|
43
|
+
TOOL_OUTPUT_DISPLAY_TAIL,
|
|
44
|
+
TOOL_OUTPUT_DISPLAY_TAIL_LINES,
|
|
45
|
+
TOOL_OUTPUT_MAX_LENGTH,
|
|
46
|
+
TOOL_OUTPUT_MAX_LINES,
|
|
47
|
+
TOOL_OUTPUT_TRUNCATION_DIR,
|
|
48
|
+
)
|
|
49
|
+
from klaude_code.protocol import tools
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ToolCallLike(Protocol):
|
|
53
|
+
"""Protocol for tool call objects."""
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def tool_name(self) -> str: ...
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# =============================================================================
|
|
60
|
+
# Data Structures
|
|
61
|
+
# =============================================================================
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class OffloadPolicy(Enum):
|
|
65
|
+
"""When to offload full output to filesystem."""
|
|
66
|
+
|
|
67
|
+
NEVER = auto() # Never offload (e.g., Read - source file exists)
|
|
68
|
+
ON_THRESHOLD = auto() # Offload only when exceeding size threshold
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class TruncationStyle(Enum):
|
|
72
|
+
"""How to truncate content that exceeds limits."""
|
|
73
|
+
|
|
74
|
+
HEAD_ONLY = auto() # Keep head, discard tail (important content at top)
|
|
75
|
+
HEAD_TAIL = auto() # Keep head and tail, discard middle (errors at end)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass
|
|
79
|
+
class OffloadResult:
|
|
80
|
+
"""Result of offload/truncation operation."""
|
|
81
|
+
|
|
82
|
+
output: str
|
|
83
|
+
was_truncated: bool
|
|
84
|
+
offloaded_path: str | None = None
|
|
85
|
+
original_length: int = 0
|
|
86
|
+
truncated_chars: int = 0
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# =============================================================================
|
|
90
|
+
# Strategy Interface
|
|
91
|
+
# =============================================================================
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class OffloadStrategy(ABC):
|
|
95
|
+
"""Base class for tool-specific offload strategies."""
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
@abstractmethod
|
|
99
|
+
def offload_policy(self) -> OffloadPolicy:
|
|
100
|
+
"""When to offload content to file."""
|
|
101
|
+
...
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
@abstractmethod
|
|
105
|
+
def truncation_style(self) -> TruncationStyle:
|
|
106
|
+
"""How to truncate content."""
|
|
107
|
+
...
|
|
108
|
+
|
|
109
|
+
@abstractmethod
|
|
110
|
+
def process(self, output: str, tool_call: ToolCallLike | None = None) -> OffloadResult:
|
|
111
|
+
"""Process tool output: truncate and optionally offload."""
|
|
112
|
+
...
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# =============================================================================
|
|
116
|
+
# Strategy Implementations
|
|
117
|
+
# =============================================================================
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class ReadToolStrategy(OffloadStrategy):
|
|
121
|
+
"""Strategy for Read tool output.
|
|
122
|
+
|
|
123
|
+
- Truncation: Head-focused (handled internally by read_tool.py)
|
|
124
|
+
- Offload: Never (source file already on filesystem)
|
|
125
|
+
|
|
126
|
+
This strategy is a pass-through since Read tool handles its own truncation.
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def offload_policy(self) -> OffloadPolicy:
|
|
131
|
+
return OffloadPolicy.NEVER
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def truncation_style(self) -> TruncationStyle:
|
|
135
|
+
return TruncationStyle.HEAD_ONLY
|
|
136
|
+
|
|
137
|
+
def process(self, output: str, tool_call: ToolCallLike | None = None) -> OffloadResult:
|
|
138
|
+
return OffloadResult(output=output, was_truncated=False, original_length=len(output))
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class HeadTailOffloadStrategy(OffloadStrategy):
|
|
142
|
+
"""Strategy for Bash and generic tools.
|
|
143
|
+
|
|
144
|
+
- Truncation: Head + Tail (preserve both ends, errors often at end)
|
|
145
|
+
- Offload: Configurable (default: on threshold)
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
def __init__(
|
|
149
|
+
self,
|
|
150
|
+
max_length: int = TOOL_OUTPUT_MAX_LENGTH,
|
|
151
|
+
head_chars: int = TOOL_OUTPUT_DISPLAY_HEAD,
|
|
152
|
+
tail_chars: int = TOOL_OUTPUT_DISPLAY_TAIL,
|
|
153
|
+
max_lines: int = TOOL_OUTPUT_MAX_LINES,
|
|
154
|
+
head_lines: int = TOOL_OUTPUT_DISPLAY_HEAD_LINES,
|
|
155
|
+
tail_lines: int = TOOL_OUTPUT_DISPLAY_TAIL_LINES,
|
|
156
|
+
offload_dir: str | None = None,
|
|
157
|
+
policy: OffloadPolicy = OffloadPolicy.ON_THRESHOLD,
|
|
158
|
+
):
|
|
159
|
+
self.max_length = max_length
|
|
160
|
+
self.head_chars = head_chars
|
|
161
|
+
self.tail_chars = tail_chars
|
|
162
|
+
self.max_lines = max_lines
|
|
163
|
+
self.head_lines = head_lines
|
|
164
|
+
self.tail_lines = tail_lines
|
|
165
|
+
self.offload_dir = Path(offload_dir or TOOL_OUTPUT_TRUNCATION_DIR)
|
|
166
|
+
self._policy = policy
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
def offload_policy(self) -> OffloadPolicy:
|
|
170
|
+
return self._policy
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def truncation_style(self) -> TruncationStyle:
|
|
174
|
+
return TruncationStyle.HEAD_TAIL
|
|
175
|
+
|
|
176
|
+
def _save_to_file(self, output: str, tool_call: ToolCallLike | None) -> str | None:
|
|
177
|
+
"""Save full output to file. Returns path or None on failure."""
|
|
178
|
+
try:
|
|
179
|
+
self.offload_dir.mkdir(parents=True, exist_ok=True)
|
|
180
|
+
tool_name = (tool_call.tool_name if tool_call else "unknown").replace("/", "_").lower()
|
|
181
|
+
random_hex = secrets.token_hex(8)
|
|
182
|
+
filename = f"klaude-{tool_name}-{random_hex}.log"
|
|
183
|
+
file_path = self.offload_dir / filename
|
|
184
|
+
file_path.write_text(output, encoding="utf-8")
|
|
185
|
+
return str(file_path)
|
|
186
|
+
except OSError:
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
def _should_offload(self, needs_truncation: bool) -> bool:
|
|
190
|
+
"""Determine if content should be offloaded based on policy."""
|
|
191
|
+
if self._policy == OffloadPolicy.NEVER:
|
|
192
|
+
return False
|
|
193
|
+
# ON_THRESHOLD: offload only when truncating
|
|
194
|
+
return needs_truncation
|
|
195
|
+
|
|
196
|
+
def _truncate_by_lines(self, output: str, lines: list[str], offloaded_path: str | None) -> tuple[str, int]:
|
|
197
|
+
"""Truncate by lines. Returns (truncated_output, hidden_lines)."""
|
|
198
|
+
total_lines = len(lines)
|
|
199
|
+
hidden_lines = total_lines - self.head_lines - self.tail_lines
|
|
200
|
+
head = "\n".join(lines[: self.head_lines])
|
|
201
|
+
tail = "\n".join(lines[-self.tail_lines :])
|
|
202
|
+
|
|
203
|
+
if offloaded_path:
|
|
204
|
+
header = (
|
|
205
|
+
f"<system-reminder>Output truncated due to length. "
|
|
206
|
+
f"Showing first {self.head_lines} and last {self.tail_lines} lines of {total_lines} lines. "
|
|
207
|
+
f"Full output saved to: {offloaded_path} </system-reminder>\n\n"
|
|
208
|
+
)
|
|
209
|
+
else:
|
|
210
|
+
header = (
|
|
211
|
+
f"<system-reminder>Output truncated due to length. "
|
|
212
|
+
f"Showing first {self.head_lines} and last {self.tail_lines} lines of {total_lines} lines."
|
|
213
|
+
f"</system-reminder>\n\n"
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
truncated_output = f"{header}{head}\n\n<...{hidden_lines} lines omitted...>\n\n{tail}"
|
|
217
|
+
return truncated_output, hidden_lines
|
|
218
|
+
|
|
219
|
+
def _truncate_by_chars(self, output: str, offloaded_path: str | None) -> tuple[str, int]:
|
|
220
|
+
"""Truncate by characters. Returns (truncated_output, hidden_chars)."""
|
|
221
|
+
original_length = len(output)
|
|
222
|
+
hidden_chars = original_length - self.head_chars - self.tail_chars
|
|
223
|
+
head = output[: self.head_chars]
|
|
224
|
+
tail = output[-self.tail_chars :]
|
|
225
|
+
|
|
226
|
+
if offloaded_path:
|
|
227
|
+
header = (
|
|
228
|
+
f"<system-reminder>Output truncated due to length. "
|
|
229
|
+
f"Showing first {self.head_chars} and last {self.tail_chars} chars of {original_length} chars. "
|
|
230
|
+
f"Full output saved to: {offloaded_path} </system-reminder>\n\n"
|
|
231
|
+
)
|
|
232
|
+
else:
|
|
233
|
+
header = (
|
|
234
|
+
f"<system-reminder>Output truncated due to length. "
|
|
235
|
+
f"Showing first {self.head_chars} and last {self.tail_chars} chars of {original_length} chars."
|
|
236
|
+
f"</system-reminder>\n\n"
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
truncated_output = f"{header}{head}\n\n<...{hidden_chars} chars omitted...>\n\n{tail}"
|
|
240
|
+
return truncated_output, hidden_chars
|
|
241
|
+
|
|
242
|
+
def process(self, output: str, tool_call: ToolCallLike | None = None) -> OffloadResult:
|
|
243
|
+
original_length = len(output)
|
|
244
|
+
lines = output.splitlines()
|
|
245
|
+
total_lines = len(lines)
|
|
246
|
+
|
|
247
|
+
# Check if truncation is needed (by lines or by chars)
|
|
248
|
+
needs_line_truncation = total_lines > self.max_lines
|
|
249
|
+
needs_char_truncation = original_length > self.max_length
|
|
250
|
+
needs_truncation = needs_line_truncation or needs_char_truncation
|
|
251
|
+
|
|
252
|
+
# No truncation needed
|
|
253
|
+
if not needs_truncation:
|
|
254
|
+
return OffloadResult(
|
|
255
|
+
output=output,
|
|
256
|
+
was_truncated=False,
|
|
257
|
+
original_length=original_length,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
# Truncation needed - offload if policy allows
|
|
261
|
+
offloaded_path = None
|
|
262
|
+
if self._should_offload(needs_truncation):
|
|
263
|
+
offloaded_path = self._save_to_file(output, tool_call)
|
|
264
|
+
|
|
265
|
+
# Prefer line-based truncation if line limit exceeded
|
|
266
|
+
if needs_line_truncation:
|
|
267
|
+
truncated_output, hidden = self._truncate_by_lines(output, lines, offloaded_path)
|
|
268
|
+
else:
|
|
269
|
+
truncated_output, hidden = self._truncate_by_chars(output, offloaded_path)
|
|
270
|
+
|
|
271
|
+
return OffloadResult(
|
|
272
|
+
output=truncated_output,
|
|
273
|
+
was_truncated=True,
|
|
274
|
+
offloaded_path=offloaded_path,
|
|
275
|
+
original_length=original_length,
|
|
276
|
+
truncated_chars=hidden,
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
# =============================================================================
|
|
281
|
+
# Strategy Registry
|
|
282
|
+
# =============================================================================
|
|
283
|
+
|
|
284
|
+
_STRATEGY_REGISTRY: dict[str, OffloadStrategy] = {
|
|
285
|
+
tools.READ: ReadToolStrategy(),
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
_DEFAULT_STRATEGY = HeadTailOffloadStrategy()
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def get_strategy(tool_name: str | None) -> OffloadStrategy:
|
|
292
|
+
"""Get the appropriate strategy for a tool."""
|
|
293
|
+
if tool_name and tool_name in _STRATEGY_REGISTRY:
|
|
294
|
+
return _STRATEGY_REGISTRY[tool_name]
|
|
295
|
+
return _DEFAULT_STRATEGY
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
# =============================================================================
|
|
299
|
+
# Public API
|
|
300
|
+
# =============================================================================
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def offload_tool_output(output: str, tool_call: ToolCallLike | None = None) -> OffloadResult:
|
|
304
|
+
"""Process tool output with appropriate offload/truncation strategy.
|
|
305
|
+
|
|
306
|
+
This is the main entry point. It selects the right strategy based on
|
|
307
|
+
the tool type and applies truncation/offload as needed.
|
|
308
|
+
"""
|
|
309
|
+
tool_name = tool_call.tool_name if tool_call else None
|
|
310
|
+
strategy = get_strategy(tool_name)
|
|
311
|
+
return strategy.process(output, tool_call)
|
|
@@ -1,43 +1 @@
|
|
|
1
|
-
Runs a shell command and returns
|
|
2
|
-
|
|
3
|
-
### Usage Notes
|
|
4
|
-
- When searching for text or files, prefer using `rg`, `rg --files` or `fd` respectively because `rg` and `fd` is much faster than alternatives like `grep` and `find`. (If these command is not found, then use alternatives.)
|
|
5
|
-
|
|
6
|
-
### Committing changes with git
|
|
7
|
-
Only create commits when requested by the user. If unclear, ask first. When the user asks you to create a new git commit, follow these steps carefully:
|
|
8
|
-
|
|
9
|
-
Git Safety Protocol:
|
|
10
|
-
- NEVER update the git config
|
|
11
|
-
- NEVER run destructive/irreversible git commands (like push --force, hard reset, etc) unless the user explicitly requests them
|
|
12
|
-
- NEVER skip hooks (--no-verify, --no-gpg-sign, etc) unless the user explicitly requests it
|
|
13
|
-
- NEVER run force push to main/master, warn the user if they request it
|
|
14
|
-
- Avoid git commit --amend. ONLY use --amend when either (1) user explicitly requested amend OR (2) adding edits from pre-commit hook (additional instructions below)
|
|
15
|
-
- Before amending: ALWAYS check authorship (git log -1 --format='%an %ae')
|
|
16
|
-
- NEVER commit changes unless the user explicitly asks you to. It is VERY IMPORTANT to only commit when explicitly asked, otherwise the user will feel that you are being too proactive.
|
|
17
|
-
|
|
18
|
-
1. You have the capability to call multiple tools in a single response. When multiple independent pieces of information are requested and all commands are likely to succeed, batch your tool calls together for optimal performance. run the following bash commands in parallel, each using the Bash tool:
|
|
19
|
-
- Run a git status command to see all untracked files.
|
|
20
|
-
- Run a git diff command to see both staged and unstaged changes that will be committed.
|
|
21
|
-
- Run a git log command to see recent commit messages, so that you can follow this repository's commit message style.
|
|
22
|
-
2. Analyze all staged changes (both previously staged and newly added) and draft a commit message:
|
|
23
|
-
- Summarize the nature of the changes (eg. new feature, enhancement to an existing feature, bug fix, refactoring, test, docs, etc.). Ensure the message accurately reflects the changes and their purpose (i.e. "add" means a wholly new feature, "update" means an enhancement to an existing feature, "fix" means a bug fix, etc.).
|
|
24
|
-
- Do not commit files that likely contain secrets (.env, credentials.json, etc). Warn the user if they specifically request to commit those files
|
|
25
|
-
- Draft a concise (1-2 sentences) commit message that focuses on the "why" rather than the "what"
|
|
26
|
-
- Ensure it accurately reflects the changes and their purpose
|
|
27
|
-
3. You have the capability to call multiple tools in a single response. When multiple independent pieces of information are requested and all commands are likely to succeed, batch your tool calls together for optimal performance. run the following commands in parallel:
|
|
28
|
-
- Add relevant untracked files to the staging area.
|
|
29
|
-
- Run git status to make sure the commit succeeded.
|
|
30
|
-
4. If the commit fails due to pre-commit hook changes, retry the commit ONCE to include these automated changes. If it fails again, it usually means a pre-commit hook is preventing the commit. If the commit succeeds but you notice that files were modified by the pre-commit hook, you MUST amend your commit to include them.
|
|
31
|
-
|
|
32
|
-
Important notes:
|
|
33
|
-
- NEVER run additional commands to read or explore code, besides git bash commands
|
|
34
|
-
- NEVER use the TodoWrite or Task tools
|
|
35
|
-
- DO NOT push to the remote repository unless the user explicitly asks you to do so
|
|
36
|
-
- IMPORTANT: Never use git commands with the -i flag (like git rebase -i or git add -i) since they require interactive input which is not supported.
|
|
37
|
-
- If there are no changes to commit (i.e., no untracked files and no modifications), do not create an empty commit
|
|
38
|
-
- In order to ensure good formatting, ALWAYS pass the commit message via a HEREDOC, a la this example:<example>
|
|
39
|
-
git commit -m "$(cat <<'EOF'
|
|
40
|
-
Commit message here.
|
|
41
|
-
EOF
|
|
42
|
-
)"
|
|
43
|
-
</example>
|
|
1
|
+
Runs a shell command and returns stdout and stderr.
|
|
@@ -1,25 +1,2 @@
|
|
|
1
1
|
Use this tool to create and manage a structured task list for your current coding session. This helps you track progress, organize complex tasks, and demonstrate thoroughness to the user.
|
|
2
2
|
It also helps the user understand the progress of the task and overall progress of their requests.
|
|
3
|
-
|
|
4
|
-
#### When to Use This Tool
|
|
5
|
-
Use this tool proactively in these scenarios:
|
|
6
|
-
|
|
7
|
-
1. Complex multi-step tasks - When a task requires 3 or more distinct steps or actions
|
|
8
|
-
2. Non-trivial and complex tasks - Tasks that require careful planning or multiple operations
|
|
9
|
-
3. User explicitly requests todo list - When the user directly asks you to use the todo list
|
|
10
|
-
4. User provides multiple tasks - When users provide a list of things to be done (numbered or comma-separated)
|
|
11
|
-
5. After receiving new instructions - Immediately capture user requirements as todos
|
|
12
|
-
6. When you start working on a task - Mark it as in_progress BEFORE beginning work. Ideally you should only have one todo as in_progress at a time
|
|
13
|
-
7. After completing a task - Mark it as completed and add any new follow-up tasks discovered during implementation
|
|
14
|
-
|
|
15
|
-
#### When NOT to Use This Tool
|
|
16
|
-
|
|
17
|
-
Skip using this tool when:
|
|
18
|
-
1. There is only a single, straightforward task
|
|
19
|
-
2. The task is trivial and tracking it provides no organizational benefit
|
|
20
|
-
3. The task can be completed in less than 3 trivial steps
|
|
21
|
-
4. The task is purely conversational or informational
|
|
22
|
-
|
|
23
|
-
NOTE that you should not use this tool if there is only one trivial task to do. In this case you are better off just doing the task directly.
|
|
24
|
-
|
|
25
|
-
When in doubt, use this tool. Being proactive with task management demonstrates attentiveness and ensures you complete all requirements successfully.
|
|
@@ -4,9 +4,9 @@ from dataclasses import dataclass
|
|
|
4
4
|
|
|
5
5
|
from klaude_code.const import CANCEL_OUTPUT
|
|
6
6
|
from klaude_code.core.tool.context import ToolContext
|
|
7
|
+
from klaude_code.core.tool.offload import offload_tool_output
|
|
7
8
|
from klaude_code.core.tool.report_back_tool import ReportBackTool
|
|
8
9
|
from klaude_code.core.tool.tool_abc import ToolABC, ToolConcurrencyPolicy
|
|
9
|
-
from klaude_code.core.tool.truncation import truncate_tool_output
|
|
10
10
|
from klaude_code.protocol import message, model, tools
|
|
11
11
|
|
|
12
12
|
|
|
@@ -52,14 +52,8 @@ async def run_tool(
|
|
|
52
52
|
tool_result.call_id = tool_call.call_id
|
|
53
53
|
tool_result.tool_name = tool_call.tool_name
|
|
54
54
|
if tool_result.output_text:
|
|
55
|
-
|
|
56
|
-
tool_result.output_text =
|
|
57
|
-
if truncation_result.was_truncated and truncation_result.saved_file_path:
|
|
58
|
-
tool_result.ui_extra = model.TruncationUIExtra(
|
|
59
|
-
saved_file_path=truncation_result.saved_file_path,
|
|
60
|
-
original_length=truncation_result.original_length,
|
|
61
|
-
truncated_length=truncation_result.truncated_length,
|
|
62
|
-
)
|
|
55
|
+
offload_result = offload_tool_output(tool_result.output_text, tool_call)
|
|
56
|
+
tool_result.output_text = offload_result.output
|
|
63
57
|
return tool_result
|
|
64
58
|
except asyncio.CancelledError:
|
|
65
59
|
# Propagate cooperative cancellation so outer layers can handle interrupts correctly.
|
|
@@ -126,6 +120,7 @@ class ToolExecutor:
|
|
|
126
120
|
self._call_event_emitted: set[str] = set()
|
|
127
121
|
self._concurrent_tasks: set[asyncio.Task[list[ToolExecutorEvent]]] = set()
|
|
128
122
|
self._sub_agent_session_ids: dict[str, str] = {}
|
|
123
|
+
self._sub_agent_metadata_getters: dict[str, Callable[[], model.TaskMetadata | None]] = {}
|
|
129
124
|
|
|
130
125
|
async def run_tools(self, tool_calls: list[ToolCallRequest]) -> AsyncGenerator[ToolExecutorEvent]:
|
|
131
126
|
"""Run the given tool calls and yield execution events.
|
|
@@ -219,12 +214,16 @@ class ToolExecutor:
|
|
|
219
214
|
unfinished = list(self._unfinished_calls.items())
|
|
220
215
|
for idx, (call_id, tool_call) in enumerate(unfinished):
|
|
221
216
|
session_id = self._sub_agent_session_ids.get(call_id)
|
|
217
|
+
# Get partial metadata from sub-agent if available
|
|
218
|
+
metadata_getter = self._sub_agent_metadata_getters.get(call_id)
|
|
219
|
+
task_metadata = metadata_getter() if metadata_getter is not None else None
|
|
222
220
|
cancel_result = message.ToolResultMessage(
|
|
223
221
|
call_id=tool_call.call_id,
|
|
224
222
|
output_text=CANCEL_OUTPUT,
|
|
225
223
|
status="aborted",
|
|
226
224
|
tool_name=tool_call.tool_name,
|
|
227
225
|
ui_extra=model.SessionIdUIExtra(session_id=session_id) if session_id else None,
|
|
226
|
+
task_metadata=task_metadata,
|
|
228
227
|
)
|
|
229
228
|
|
|
230
229
|
if call_id not in self._call_event_emitted:
|
|
@@ -242,6 +241,7 @@ class ToolExecutor:
|
|
|
242
241
|
self._append_history([cancel_result])
|
|
243
242
|
self._unfinished_calls.pop(call_id, None)
|
|
244
243
|
self._sub_agent_session_ids.pop(call_id, None)
|
|
244
|
+
self._sub_agent_metadata_getters.pop(call_id, None)
|
|
245
245
|
|
|
246
246
|
return events_to_yield
|
|
247
247
|
|
|
@@ -278,7 +278,11 @@ class ToolExecutor:
|
|
|
278
278
|
if tool_call.call_id not in self._sub_agent_session_ids:
|
|
279
279
|
self._sub_agent_session_ids[tool_call.call_id] = session_id
|
|
280
280
|
|
|
281
|
+
def _register_metadata_getter(getter: Callable[[], model.TaskMetadata | None]) -> None:
|
|
282
|
+
self._sub_agent_metadata_getters[tool_call.call_id] = getter
|
|
283
|
+
|
|
281
284
|
call_context = self._context.with_record_sub_agent_session_id(_record_sub_agent_session_id)
|
|
285
|
+
call_context = call_context.with_register_sub_agent_metadata_getter(_register_metadata_getter)
|
|
282
286
|
tool_result: message.ToolResultMessage = await run_tool(tool_call, self._registry, call_context)
|
|
283
287
|
|
|
284
288
|
self._append_history([tool_result])
|
|
@@ -287,6 +291,7 @@ class ToolExecutor:
|
|
|
287
291
|
|
|
288
292
|
self._unfinished_calls.pop(tool_call.call_id, None)
|
|
289
293
|
self._sub_agent_session_ids.pop(tool_call.call_id, None)
|
|
294
|
+
self._sub_agent_metadata_getters.pop(tool_call.call_id, None)
|
|
290
295
|
|
|
291
296
|
extra_events = self._build_tool_side_effect_events(tool_result)
|
|
292
297
|
return [result_event, *extra_events]
|
|
@@ -5,4 +5,4 @@ The tool automatically processes the response based on Content-Type:
|
|
|
5
5
|
- JSON responses are formatted with indentation
|
|
6
6
|
- Markdown and other text content is returned as-is
|
|
7
7
|
|
|
8
|
-
Content is always saved to a local file. The file path is
|
|
8
|
+
Content is always saved to a local file. The file path is shown at the start of the output in `[Web content saved to ...]` format. For large content that gets truncated, you can read the saved file directly.
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import json
|
|
3
3
|
import re
|
|
4
|
-
import time
|
|
5
4
|
import urllib.error
|
|
6
5
|
import urllib.request
|
|
7
6
|
from http.client import HTTPResponse
|
|
@@ -21,7 +20,7 @@ from klaude_code.core.tool.tool_abc import ToolABC, ToolConcurrencyPolicy, ToolM
|
|
|
21
20
|
from klaude_code.core.tool.tool_registry import register
|
|
22
21
|
from klaude_code.protocol import llm_param, message, tools
|
|
23
22
|
|
|
24
|
-
WEB_FETCH_SAVE_DIR = Path(TOOL_OUTPUT_TRUNCATION_DIR)
|
|
23
|
+
WEB_FETCH_SAVE_DIR = Path(TOOL_OUTPUT_TRUNCATION_DIR)
|
|
25
24
|
|
|
26
25
|
|
|
27
26
|
def _encode_url(url: str) -> str:
|
|
@@ -29,7 +28,6 @@ def _encode_url(url: str) -> str:
|
|
|
29
28
|
parsed = urlparse(url)
|
|
30
29
|
encoded_path = quote(parsed.path, safe="/-_.~")
|
|
31
30
|
encoded_query = quote(parsed.query, safe="=&-_.~")
|
|
32
|
-
# Handle IDN (Internationalized Domain Names) by encoding to punycode
|
|
33
31
|
try:
|
|
34
32
|
netloc = parsed.netloc.encode("idna").decode("ascii")
|
|
35
33
|
except UnicodeError:
|
|
@@ -55,38 +53,30 @@ def _extract_content_type_and_charset(response: HTTPResponse) -> tuple[str, str
|
|
|
55
53
|
|
|
56
54
|
def _detect_encoding(data: bytes, declared_charset: str | None) -> str:
|
|
57
55
|
"""Detect the encoding of the data."""
|
|
58
|
-
# 1. Use declared charset from HTTP header if available
|
|
59
56
|
if declared_charset:
|
|
60
57
|
return declared_charset
|
|
61
58
|
|
|
62
|
-
# 2. Try to detect from HTML meta tags (check first 2KB)
|
|
63
59
|
head = data[:2048].lower()
|
|
64
|
-
# <meta charset="xxx">
|
|
65
60
|
if match := re.search(rb'<meta[^>]+charset=["\']?([^"\'\s>]+)', head):
|
|
66
61
|
return match.group(1).decode("ascii", errors="ignore")
|
|
67
|
-
# <meta http-equiv="Content-Type" content="text/html; charset=xxx">
|
|
68
62
|
if match := re.search(rb'content=["\'][^"\']*charset=([^"\'\s;]+)', head):
|
|
69
63
|
return match.group(1).decode("ascii", errors="ignore")
|
|
70
64
|
|
|
71
|
-
# 3. Use chardet for automatic detection
|
|
72
65
|
import chardet
|
|
73
66
|
|
|
74
67
|
result = chardet.detect(data)
|
|
75
68
|
if result["encoding"] and result["confidence"] and result["confidence"] > 0.7:
|
|
76
69
|
return result["encoding"]
|
|
77
70
|
|
|
78
|
-
# 4. Default to UTF-8
|
|
79
71
|
return "utf-8"
|
|
80
72
|
|
|
81
73
|
|
|
82
74
|
def _decode_content(data: bytes, declared_charset: str | None) -> str:
|
|
83
75
|
"""Decode bytes to string with automatic encoding detection."""
|
|
84
76
|
encoding = _detect_encoding(data, declared_charset)
|
|
85
|
-
|
|
86
77
|
try:
|
|
87
78
|
return data.decode(encoding)
|
|
88
79
|
except (UnicodeDecodeError, LookupError):
|
|
89
|
-
# Fallback: try UTF-8 with replacement for invalid chars
|
|
90
80
|
return data.decode("utf-8", errors="replace")
|
|
91
81
|
|
|
92
82
|
|
|
@@ -117,29 +107,27 @@ def _extract_url_filename(url: str) -> str:
|
|
|
117
107
|
return name[:URL_FILENAME_MAX_LENGTH] if len(name) > URL_FILENAME_MAX_LENGTH else name
|
|
118
108
|
|
|
119
109
|
|
|
120
|
-
def
|
|
121
|
-
"""Save
|
|
110
|
+
def _save_binary_content(url: str, data: bytes, extension: str = ".bin") -> str | None:
|
|
111
|
+
"""Save binary content to file. Returns file path or None on failure."""
|
|
122
112
|
try:
|
|
123
113
|
WEB_FETCH_SAVE_DIR.mkdir(parents=True, exist_ok=True)
|
|
124
|
-
timestamp = int(time.time())
|
|
125
114
|
identifier = _extract_url_filename(url)
|
|
126
|
-
filename = f"{identifier}
|
|
115
|
+
filename = f"klaude-webfetch-{identifier}{extension}"
|
|
127
116
|
file_path = WEB_FETCH_SAVE_DIR / filename
|
|
128
|
-
file_path.
|
|
117
|
+
file_path.write_bytes(data)
|
|
129
118
|
return str(file_path)
|
|
130
119
|
except OSError:
|
|
131
120
|
return None
|
|
132
121
|
|
|
133
122
|
|
|
134
|
-
def
|
|
135
|
-
"""Save
|
|
123
|
+
def _save_text_content(url: str, content: str) -> str | None:
|
|
124
|
+
"""Save text content to file. Returns file path or None on failure."""
|
|
136
125
|
try:
|
|
137
126
|
WEB_FETCH_SAVE_DIR.mkdir(parents=True, exist_ok=True)
|
|
138
|
-
timestamp = int(time.time())
|
|
139
127
|
identifier = _extract_url_filename(url)
|
|
140
|
-
filename = f"{identifier}
|
|
128
|
+
filename = f"klaude-webfetch-{identifier}.txt"
|
|
141
129
|
file_path = WEB_FETCH_SAVE_DIR / filename
|
|
142
|
-
file_path.
|
|
130
|
+
file_path.write_text(content, encoding="utf-8")
|
|
143
131
|
return str(file_path)
|
|
144
132
|
except OSError:
|
|
145
133
|
return None
|
|
@@ -164,15 +152,7 @@ def _process_content(content_type: str, text: str) -> str:
|
|
|
164
152
|
|
|
165
153
|
|
|
166
154
|
def _fetch_url(url: str, timeout: int = WEB_FETCH_DEFAULT_TIMEOUT_SEC) -> tuple[str, bytes, str | None]:
|
|
167
|
-
"""
|
|
168
|
-
Fetch URL content synchronously.
|
|
169
|
-
|
|
170
|
-
Returns:
|
|
171
|
-
Tuple of (content_type, raw_data, charset)
|
|
172
|
-
|
|
173
|
-
Raises:
|
|
174
|
-
Various exceptions on failure
|
|
175
|
-
"""
|
|
155
|
+
"""Fetch URL content synchronously."""
|
|
176
156
|
headers = {
|
|
177
157
|
"Accept": "text/markdown, */*",
|
|
178
158
|
"User-Agent": WEB_FETCH_USER_AGENT,
|
|
@@ -229,7 +209,6 @@ class WebFetchTool(ToolABC):
|
|
|
229
209
|
del context
|
|
230
210
|
url = args.url
|
|
231
211
|
|
|
232
|
-
# Basic URL validation
|
|
233
212
|
if not url.startswith(("http://", "https://")):
|
|
234
213
|
return message.ToolResultMessage(
|
|
235
214
|
status="error",
|
|
@@ -239,7 +218,7 @@ class WebFetchTool(ToolABC):
|
|
|
239
218
|
try:
|
|
240
219
|
content_type, data, charset = await asyncio.to_thread(_fetch_url, url)
|
|
241
220
|
|
|
242
|
-
# Handle PDF files
|
|
221
|
+
# Handle PDF files - must save binary content
|
|
243
222
|
if content_type == "application/pdf" or _is_pdf_url(url):
|
|
244
223
|
saved_path = _save_binary_content(url, data, ".pdf")
|
|
245
224
|
if saved_path:
|
|
@@ -252,15 +231,11 @@ class WebFetchTool(ToolABC):
|
|
|
252
231
|
output_text=f"Failed to save PDF file (url={url})",
|
|
253
232
|
)
|
|
254
233
|
|
|
255
|
-
# Handle text content
|
|
234
|
+
# Handle text content - save to file and return with path hint
|
|
256
235
|
text = _decode_content(data, charset)
|
|
257
236
|
processed = _process_content(content_type, text)
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
saved_path = _save_web_content(url, processed)
|
|
261
|
-
|
|
262
|
-
# Build output with file path info
|
|
263
|
-
output = f"<file_saved>{saved_path}</file_saved>\n\n{processed}" if saved_path else processed
|
|
237
|
+
saved_path = _save_text_content(url, processed)
|
|
238
|
+
output = f"[Web content saved to {saved_path}]\n\n{processed}" if saved_path else processed
|
|
264
239
|
|
|
265
240
|
return message.ToolResultMessage(
|
|
266
241
|
status="success",
|