klaude-code 1.2.6__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/__init__.py +0 -0
- klaude_code/cli/__init__.py +1 -0
- klaude_code/cli/main.py +298 -0
- klaude_code/cli/runtime.py +331 -0
- klaude_code/cli/session_cmd.py +80 -0
- klaude_code/command/__init__.py +43 -0
- klaude_code/command/clear_cmd.py +20 -0
- klaude_code/command/command_abc.py +92 -0
- klaude_code/command/diff_cmd.py +138 -0
- klaude_code/command/export_cmd.py +86 -0
- klaude_code/command/help_cmd.py +51 -0
- klaude_code/command/model_cmd.py +43 -0
- klaude_code/command/prompt-dev-docs-update.md +56 -0
- klaude_code/command/prompt-dev-docs.md +46 -0
- klaude_code/command/prompt-init.md +45 -0
- klaude_code/command/prompt_command.py +69 -0
- klaude_code/command/refresh_cmd.py +43 -0
- klaude_code/command/registry.py +110 -0
- klaude_code/command/status_cmd.py +111 -0
- klaude_code/command/terminal_setup_cmd.py +252 -0
- klaude_code/config/__init__.py +11 -0
- klaude_code/config/config.py +177 -0
- klaude_code/config/list_model.py +162 -0
- klaude_code/config/select_model.py +67 -0
- klaude_code/const/__init__.py +133 -0
- klaude_code/core/__init__.py +0 -0
- klaude_code/core/agent.py +165 -0
- klaude_code/core/executor.py +485 -0
- klaude_code/core/manager/__init__.py +19 -0
- klaude_code/core/manager/agent_manager.py +127 -0
- klaude_code/core/manager/llm_clients.py +42 -0
- klaude_code/core/manager/llm_clients_builder.py +49 -0
- klaude_code/core/manager/sub_agent_manager.py +86 -0
- klaude_code/core/prompt.py +89 -0
- klaude_code/core/prompts/prompt-claude-code.md +98 -0
- klaude_code/core/prompts/prompt-codex.md +331 -0
- klaude_code/core/prompts/prompt-gemini.md +43 -0
- klaude_code/core/prompts/prompt-subagent-explore.md +27 -0
- klaude_code/core/prompts/prompt-subagent-oracle.md +23 -0
- klaude_code/core/prompts/prompt-subagent-webfetch.md +46 -0
- klaude_code/core/prompts/prompt-subagent.md +8 -0
- klaude_code/core/reminders.py +445 -0
- klaude_code/core/task.py +237 -0
- klaude_code/core/tool/__init__.py +75 -0
- klaude_code/core/tool/file/__init__.py +0 -0
- klaude_code/core/tool/file/apply_patch.py +492 -0
- klaude_code/core/tool/file/apply_patch_tool.md +1 -0
- klaude_code/core/tool/file/apply_patch_tool.py +204 -0
- klaude_code/core/tool/file/edit_tool.md +9 -0
- klaude_code/core/tool/file/edit_tool.py +274 -0
- klaude_code/core/tool/file/multi_edit_tool.md +42 -0
- klaude_code/core/tool/file/multi_edit_tool.py +199 -0
- klaude_code/core/tool/file/read_tool.md +14 -0
- klaude_code/core/tool/file/read_tool.py +326 -0
- klaude_code/core/tool/file/write_tool.md +8 -0
- klaude_code/core/tool/file/write_tool.py +146 -0
- klaude_code/core/tool/memory/__init__.py +0 -0
- klaude_code/core/tool/memory/memory_tool.md +16 -0
- klaude_code/core/tool/memory/memory_tool.py +462 -0
- klaude_code/core/tool/memory/skill_loader.py +245 -0
- klaude_code/core/tool/memory/skill_tool.md +24 -0
- klaude_code/core/tool/memory/skill_tool.py +97 -0
- klaude_code/core/tool/shell/__init__.py +0 -0
- klaude_code/core/tool/shell/bash_tool.md +43 -0
- klaude_code/core/tool/shell/bash_tool.py +123 -0
- klaude_code/core/tool/shell/command_safety.py +363 -0
- klaude_code/core/tool/sub_agent_tool.py +83 -0
- klaude_code/core/tool/todo/__init__.py +0 -0
- klaude_code/core/tool/todo/todo_write_tool.md +182 -0
- klaude_code/core/tool/todo/todo_write_tool.py +121 -0
- klaude_code/core/tool/todo/update_plan_tool.md +3 -0
- klaude_code/core/tool/todo/update_plan_tool.py +104 -0
- klaude_code/core/tool/tool_abc.py +25 -0
- klaude_code/core/tool/tool_context.py +106 -0
- klaude_code/core/tool/tool_registry.py +78 -0
- klaude_code/core/tool/tool_runner.py +252 -0
- klaude_code/core/tool/truncation.py +170 -0
- klaude_code/core/tool/web/__init__.py +0 -0
- klaude_code/core/tool/web/mermaid_tool.md +21 -0
- klaude_code/core/tool/web/mermaid_tool.py +76 -0
- klaude_code/core/tool/web/web_fetch_tool.md +8 -0
- klaude_code/core/tool/web/web_fetch_tool.py +159 -0
- klaude_code/core/turn.py +220 -0
- klaude_code/llm/__init__.py +21 -0
- klaude_code/llm/anthropic/__init__.py +3 -0
- klaude_code/llm/anthropic/client.py +221 -0
- klaude_code/llm/anthropic/input.py +200 -0
- klaude_code/llm/client.py +49 -0
- klaude_code/llm/input_common.py +239 -0
- klaude_code/llm/openai_compatible/__init__.py +3 -0
- klaude_code/llm/openai_compatible/client.py +211 -0
- klaude_code/llm/openai_compatible/input.py +109 -0
- klaude_code/llm/openai_compatible/tool_call_accumulator.py +80 -0
- klaude_code/llm/openrouter/__init__.py +3 -0
- klaude_code/llm/openrouter/client.py +200 -0
- klaude_code/llm/openrouter/input.py +160 -0
- klaude_code/llm/openrouter/reasoning_handler.py +209 -0
- klaude_code/llm/registry.py +22 -0
- klaude_code/llm/responses/__init__.py +3 -0
- klaude_code/llm/responses/client.py +216 -0
- klaude_code/llm/responses/input.py +167 -0
- klaude_code/llm/usage.py +109 -0
- klaude_code/protocol/__init__.py +4 -0
- klaude_code/protocol/commands.py +21 -0
- klaude_code/protocol/events.py +163 -0
- klaude_code/protocol/llm_param.py +147 -0
- klaude_code/protocol/model.py +287 -0
- klaude_code/protocol/op.py +89 -0
- klaude_code/protocol/op_handler.py +28 -0
- klaude_code/protocol/sub_agent.py +348 -0
- klaude_code/protocol/tools.py +15 -0
- klaude_code/session/__init__.py +4 -0
- klaude_code/session/export.py +624 -0
- klaude_code/session/selector.py +76 -0
- klaude_code/session/session.py +474 -0
- klaude_code/session/templates/export_session.html +1434 -0
- klaude_code/trace/__init__.py +3 -0
- klaude_code/trace/log.py +168 -0
- klaude_code/ui/__init__.py +91 -0
- klaude_code/ui/core/__init__.py +1 -0
- klaude_code/ui/core/display.py +103 -0
- klaude_code/ui/core/input.py +71 -0
- klaude_code/ui/core/stage_manager.py +55 -0
- klaude_code/ui/modes/__init__.py +1 -0
- klaude_code/ui/modes/debug/__init__.py +1 -0
- klaude_code/ui/modes/debug/display.py +36 -0
- klaude_code/ui/modes/exec/__init__.py +1 -0
- klaude_code/ui/modes/exec/display.py +63 -0
- klaude_code/ui/modes/repl/__init__.py +51 -0
- klaude_code/ui/modes/repl/clipboard.py +152 -0
- klaude_code/ui/modes/repl/completers.py +429 -0
- klaude_code/ui/modes/repl/display.py +60 -0
- klaude_code/ui/modes/repl/event_handler.py +375 -0
- klaude_code/ui/modes/repl/input_prompt_toolkit.py +198 -0
- klaude_code/ui/modes/repl/key_bindings.py +170 -0
- klaude_code/ui/modes/repl/renderer.py +281 -0
- klaude_code/ui/renderers/__init__.py +0 -0
- klaude_code/ui/renderers/assistant.py +21 -0
- klaude_code/ui/renderers/common.py +8 -0
- klaude_code/ui/renderers/developer.py +158 -0
- klaude_code/ui/renderers/diffs.py +215 -0
- klaude_code/ui/renderers/errors.py +16 -0
- klaude_code/ui/renderers/metadata.py +190 -0
- klaude_code/ui/renderers/sub_agent.py +71 -0
- klaude_code/ui/renderers/thinking.py +39 -0
- klaude_code/ui/renderers/tools.py +551 -0
- klaude_code/ui/renderers/user_input.py +65 -0
- klaude_code/ui/rich/__init__.py +1 -0
- klaude_code/ui/rich/live.py +65 -0
- klaude_code/ui/rich/markdown.py +308 -0
- klaude_code/ui/rich/quote.py +34 -0
- klaude_code/ui/rich/searchable_text.py +71 -0
- klaude_code/ui/rich/status.py +240 -0
- klaude_code/ui/rich/theme.py +274 -0
- klaude_code/ui/terminal/__init__.py +1 -0
- klaude_code/ui/terminal/color.py +244 -0
- klaude_code/ui/terminal/control.py +147 -0
- klaude_code/ui/terminal/notifier.py +107 -0
- klaude_code/ui/terminal/progress_bar.py +87 -0
- klaude_code/ui/utils/__init__.py +1 -0
- klaude_code/ui/utils/common.py +108 -0
- klaude_code/ui/utils/debouncer.py +42 -0
- klaude_code/version.py +163 -0
- klaude_code-1.2.6.dist-info/METADATA +178 -0
- klaude_code-1.2.6.dist-info/RECORD +167 -0
- klaude_code-1.2.6.dist-info/WHEEL +4 -0
- klaude_code-1.2.6.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import re
|
|
3
|
+
import time
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from urllib.parse import urlparse
|
|
8
|
+
|
|
9
|
+
from klaude_code import const
|
|
10
|
+
from klaude_code.protocol import model, tools
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class TruncationResult:
|
|
15
|
+
"""Result of truncation operation."""
|
|
16
|
+
|
|
17
|
+
output: str
|
|
18
|
+
was_truncated: bool
|
|
19
|
+
saved_file_path: str | None = None
|
|
20
|
+
original_length: int = 0
|
|
21
|
+
truncated_length: int = 0
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _extract_url_filename(url: str) -> str:
|
|
25
|
+
"""Extract a safe filename from a URL."""
|
|
26
|
+
parsed = urlparse(url)
|
|
27
|
+
# Combine host and path for a meaningful filename
|
|
28
|
+
host = parsed.netloc.replace(".", "_").replace(":", "_")
|
|
29
|
+
path = parsed.path.strip("/").replace("/", "_")
|
|
30
|
+
if path:
|
|
31
|
+
name = f"{host}_{path}"
|
|
32
|
+
else:
|
|
33
|
+
name = host
|
|
34
|
+
# Sanitize: keep only alphanumeric, underscore, hyphen
|
|
35
|
+
name = re.sub(r"[^a-zA-Z0-9_\-]", "_", name)
|
|
36
|
+
# Limit length
|
|
37
|
+
return name[:80] if len(name) > 80 else name
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class TruncationStrategy(ABC):
|
|
41
|
+
"""Abstract base class for tool output truncation strategies."""
|
|
42
|
+
|
|
43
|
+
@abstractmethod
|
|
44
|
+
def truncate(self, output: str, tool_call: model.ToolCallItem | None = None) -> TruncationResult:
|
|
45
|
+
"""Truncate the output according to the strategy."""
|
|
46
|
+
...
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class SimpleTruncationStrategy(TruncationStrategy):
|
|
50
|
+
"""Simple character-based truncation strategy."""
|
|
51
|
+
|
|
52
|
+
def __init__(self, max_length: int = const.TOOL_OUTPUT_MAX_LENGTH):
|
|
53
|
+
self.max_length = max_length
|
|
54
|
+
|
|
55
|
+
def truncate(self, output: str, tool_call: model.ToolCallItem | None = None) -> TruncationResult:
|
|
56
|
+
if len(output) > self.max_length:
|
|
57
|
+
truncated_length = len(output) - self.max_length
|
|
58
|
+
truncated_output = output[: self.max_length] + f"... (truncated {truncated_length} characters)"
|
|
59
|
+
return TruncationResult(
|
|
60
|
+
output=truncated_output,
|
|
61
|
+
was_truncated=True,
|
|
62
|
+
original_length=len(output),
|
|
63
|
+
truncated_length=truncated_length,
|
|
64
|
+
)
|
|
65
|
+
return TruncationResult(output=output, was_truncated=False, original_length=len(output))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class SmartTruncationStrategy(TruncationStrategy):
|
|
69
|
+
"""Smart truncation strategy that saves full output to file and shows head/tail."""
|
|
70
|
+
|
|
71
|
+
def __init__(
|
|
72
|
+
self,
|
|
73
|
+
max_length: int = const.TOOL_OUTPUT_MAX_LENGTH,
|
|
74
|
+
head_chars: int = const.TOOL_OUTPUT_DISPLAY_HEAD,
|
|
75
|
+
tail_chars: int = const.TOOL_OUTPUT_DISPLAY_TAIL,
|
|
76
|
+
truncation_dir: str = const.TOOL_OUTPUT_TRUNCATION_DIR,
|
|
77
|
+
):
|
|
78
|
+
self.max_length = max_length
|
|
79
|
+
self.head_chars = head_chars
|
|
80
|
+
self.tail_chars = tail_chars
|
|
81
|
+
self.truncation_dir = Path(truncation_dir)
|
|
82
|
+
|
|
83
|
+
def _get_file_identifier(self, tool_call: model.ToolCallItem | None) -> str:
|
|
84
|
+
"""Get a file identifier based on tool call. For WebFetch, use URL; otherwise use call_id."""
|
|
85
|
+
if tool_call and tool_call.name == tools.WEB_FETCH:
|
|
86
|
+
try:
|
|
87
|
+
args = json.loads(tool_call.arguments)
|
|
88
|
+
url = args.get("url", "")
|
|
89
|
+
if url:
|
|
90
|
+
return _extract_url_filename(url)
|
|
91
|
+
except (json.JSONDecodeError, TypeError):
|
|
92
|
+
pass
|
|
93
|
+
# Fallback to call_id
|
|
94
|
+
if tool_call and tool_call.call_id:
|
|
95
|
+
return tool_call.call_id.replace("/", "_")
|
|
96
|
+
return "unknown"
|
|
97
|
+
|
|
98
|
+
def _save_to_file(self, output: str, tool_call: model.ToolCallItem | None) -> str | None:
|
|
99
|
+
"""Save full output to file. Returns file path or None on failure."""
|
|
100
|
+
try:
|
|
101
|
+
self.truncation_dir.mkdir(parents=True, exist_ok=True)
|
|
102
|
+
timestamp = int(time.time())
|
|
103
|
+
tool_name = (tool_call.name if tool_call else "unknown").replace("/", "_")
|
|
104
|
+
identifier = self._get_file_identifier(tool_call)
|
|
105
|
+
filename = f"{tool_name}-{identifier}-{timestamp}.txt"
|
|
106
|
+
file_path = self.truncation_dir / filename
|
|
107
|
+
file_path.write_text(output, encoding="utf-8")
|
|
108
|
+
return str(file_path)
|
|
109
|
+
except (OSError, IOError):
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
def truncate(self, output: str, tool_call: model.ToolCallItem | None = None) -> TruncationResult:
|
|
113
|
+
original_length = len(output)
|
|
114
|
+
|
|
115
|
+
if original_length <= self.max_length:
|
|
116
|
+
return TruncationResult(output=output, was_truncated=False, original_length=original_length)
|
|
117
|
+
|
|
118
|
+
# Save full output to file
|
|
119
|
+
saved_file_path = self._save_to_file(output, tool_call)
|
|
120
|
+
|
|
121
|
+
truncated_length = original_length - self.head_chars - self.tail_chars
|
|
122
|
+
head_content = output[: self.head_chars]
|
|
123
|
+
tail_content = output[-self.tail_chars :]
|
|
124
|
+
|
|
125
|
+
# Build truncated output with file info
|
|
126
|
+
if saved_file_path:
|
|
127
|
+
header = (
|
|
128
|
+
f"<system-reminder>Output truncated: {truncated_length} chars hidden. "
|
|
129
|
+
f"Full tool output saved to {saved_file_path}. "
|
|
130
|
+
f"Use Read with limit+offset or rg/grep to inspect.\n"
|
|
131
|
+
f"Showing first {self.head_chars} and last {self.tail_chars} chars:</system-reminder>\n\n"
|
|
132
|
+
)
|
|
133
|
+
else:
|
|
134
|
+
header = (
|
|
135
|
+
f"<system-reminder>Output truncated: {truncated_length} chars hidden. "
|
|
136
|
+
f"Showing first {self.head_chars} and last {self.tail_chars} chars:</system-reminder>\n\n"
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
truncated_output = (
|
|
140
|
+
f"{header}{head_content}\n\n"
|
|
141
|
+
f"<system-reminder>... {truncated_length} characters omitted ...</system-reminder>\n\n"
|
|
142
|
+
f"{tail_content}"
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
return TruncationResult(
|
|
146
|
+
output=truncated_output,
|
|
147
|
+
was_truncated=True,
|
|
148
|
+
saved_file_path=saved_file_path,
|
|
149
|
+
original_length=original_length,
|
|
150
|
+
truncated_length=truncated_length,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
_default_strategy: TruncationStrategy = SmartTruncationStrategy()
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def get_truncation_strategy() -> TruncationStrategy:
|
|
158
|
+
"""Get the current truncation strategy."""
|
|
159
|
+
return _default_strategy
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def set_truncation_strategy(strategy: TruncationStrategy) -> None:
|
|
163
|
+
"""Set the truncation strategy to use."""
|
|
164
|
+
global _default_strategy
|
|
165
|
+
_default_strategy = strategy
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def truncate_tool_output(output: str, tool_call: model.ToolCallItem | None = None) -> TruncationResult:
|
|
169
|
+
"""Truncate tool output using the current strategy."""
|
|
170
|
+
return get_truncation_strategy().truncate(output, tool_call)
|
|
File without changes
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Renders a Mermaid diagram from the provided code.
|
|
2
|
+
|
|
3
|
+
PROACTIVELY USE DIAGRAMS when they would better convey information than prose alone. The diagrams produced by this tool are shown to the user..
|
|
4
|
+
|
|
5
|
+
You should create diagrams WITHOUT being explicitly asked in these scenarios:
|
|
6
|
+
- When explaining system architecture or component relationships
|
|
7
|
+
- When describing workflows, data flows, or user journeys
|
|
8
|
+
- When explaining algorithms or complex processes
|
|
9
|
+
- When illustrating class hierarchies or entity relationships
|
|
10
|
+
- When showing state transitions or event sequences
|
|
11
|
+
|
|
12
|
+
Diagrams are especially valuable for visualizing:
|
|
13
|
+
- Application architecture and dependencies
|
|
14
|
+
- API interactions and data flow
|
|
15
|
+
- Component hierarchies and relationships
|
|
16
|
+
- State machines and transitions
|
|
17
|
+
- Sequence and timing of operations
|
|
18
|
+
- Decision trees and conditional logic
|
|
19
|
+
|
|
20
|
+
# Styling
|
|
21
|
+
- When defining custom classDefs, always define fill color, stroke color, and text color ("fill", "stroke", "color") explicitly
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import json
|
|
5
|
+
import zlib
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, Field
|
|
9
|
+
|
|
10
|
+
from klaude_code.core.tool.tool_abc import ToolABC, load_desc
|
|
11
|
+
from klaude_code.core.tool.tool_registry import register
|
|
12
|
+
from klaude_code.protocol import llm_param, model, tools
|
|
13
|
+
|
|
14
|
+
_MERMAID_LIVE_PREFIX = "https://mermaid.live/view#pako:"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@register(tools.MERMAID)
|
|
18
|
+
class MermaidTool(ToolABC):
|
|
19
|
+
"""Create shareable Mermaid.live links for diagram rendering."""
|
|
20
|
+
|
|
21
|
+
class MermaidArguments(BaseModel):
|
|
22
|
+
code: str = Field(description="The Mermaid diagram code to render")
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def schema(cls) -> llm_param.ToolSchema:
|
|
26
|
+
return llm_param.ToolSchema(
|
|
27
|
+
name=tools.MERMAID,
|
|
28
|
+
type="function",
|
|
29
|
+
description=load_desc(Path(__file__).parent / "mermaid_tool.md"),
|
|
30
|
+
parameters={
|
|
31
|
+
"type": "object",
|
|
32
|
+
"properties": {
|
|
33
|
+
"code": {
|
|
34
|
+
"description": "The Mermaid diagram code to render (DO NOT override with custom colors or other styles, DO NOT use HTML tags in node labels)",
|
|
35
|
+
"type": "string",
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
"required": ["code"],
|
|
39
|
+
"additionalProperties": False,
|
|
40
|
+
},
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
async def call(cls, arguments: str) -> model.ToolResultItem:
|
|
45
|
+
try:
|
|
46
|
+
args = cls.MermaidArguments.model_validate_json(arguments)
|
|
47
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
48
|
+
return model.ToolResultItem(status="error", output=f"Invalid arguments: {exc}")
|
|
49
|
+
|
|
50
|
+
link = cls._build_link(args.code)
|
|
51
|
+
line_count = cls._count_lines(args.code)
|
|
52
|
+
ui_extra = model.ToolResultUIExtra(
|
|
53
|
+
type=model.ToolResultUIExtraType.MERMAID_LINK,
|
|
54
|
+
mermaid_link=model.MermaidLinkUIExtra(link=link, line_count=line_count),
|
|
55
|
+
)
|
|
56
|
+
output = f"Mermaid diagram rendered successfully ({line_count} lines)."
|
|
57
|
+
return model.ToolResultItem(status="success", output=output, ui_extra=ui_extra)
|
|
58
|
+
|
|
59
|
+
@staticmethod
|
|
60
|
+
def _build_link(code: str) -> str:
|
|
61
|
+
state = {
|
|
62
|
+
"code": code,
|
|
63
|
+
"mermaid": {"theme": "neutral"},
|
|
64
|
+
"autoSync": True,
|
|
65
|
+
"updateDiagram": True,
|
|
66
|
+
}
|
|
67
|
+
json_payload = json.dumps(state, ensure_ascii=False)
|
|
68
|
+
compressed = zlib.compress(json_payload.encode("utf-8"), level=9)
|
|
69
|
+
encoded = base64.urlsafe_b64encode(compressed).decode("ascii").rstrip("=")
|
|
70
|
+
return f"{_MERMAID_LIVE_PREFIX}{encoded}"
|
|
71
|
+
|
|
72
|
+
@staticmethod
|
|
73
|
+
def _count_lines(code: str) -> int:
|
|
74
|
+
if not code:
|
|
75
|
+
return 0
|
|
76
|
+
return len(code.splitlines()) or 0
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Fetch content from a URL and return it in a readable format.
|
|
2
|
+
|
|
3
|
+
The tool automatically processes the response based on Content-Type:
|
|
4
|
+
- HTML pages are converted to Markdown for easier reading
|
|
5
|
+
- JSON responses are formatted with indentation
|
|
6
|
+
- Markdown and other text content is returned as-is
|
|
7
|
+
|
|
8
|
+
Use this tool to retrieve web page content for analysis.
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import urllib.error
|
|
4
|
+
import urllib.request
|
|
5
|
+
from http.client import HTTPResponse
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
from klaude_code.core.tool.tool_abc import ToolABC, load_desc
|
|
11
|
+
from klaude_code.core.tool.tool_registry import register
|
|
12
|
+
from klaude_code.protocol import llm_param, model, tools
|
|
13
|
+
|
|
14
|
+
DEFAULT_TIMEOUT_SEC = 30
|
|
15
|
+
DEFAULT_USER_AGENT = "Mozilla/5.0 (compatible; KlaudeCode/1.0)"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _extract_content_type(response: HTTPResponse) -> str:
|
|
19
|
+
"""Extract the base content type without charset parameters."""
|
|
20
|
+
content_type = response.getheader("Content-Type", "")
|
|
21
|
+
return content_type.split(";")[0].strip().lower()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _validate_utf8(data: bytes) -> str:
|
|
25
|
+
"""Validate and decode bytes as UTF-8."""
|
|
26
|
+
return data.decode("utf-8")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _convert_html_to_markdown(html: str) -> str:
|
|
30
|
+
"""Convert HTML to Markdown using trafilatura."""
|
|
31
|
+
import trafilatura
|
|
32
|
+
|
|
33
|
+
result = trafilatura.extract(html, output_format="markdown", include_links=True, include_images=True)
|
|
34
|
+
return result or ""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _format_json(text: str) -> str:
|
|
38
|
+
"""Format JSON with indentation."""
|
|
39
|
+
try:
|
|
40
|
+
parsed = json.loads(text)
|
|
41
|
+
return json.dumps(parsed, indent=2, ensure_ascii=False)
|
|
42
|
+
except json.JSONDecodeError:
|
|
43
|
+
return text
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _process_content(content_type: str, text: str) -> str:
|
|
47
|
+
"""Process content based on Content-Type header."""
|
|
48
|
+
if content_type == "text/html":
|
|
49
|
+
return _convert_html_to_markdown(text)
|
|
50
|
+
elif content_type == "text/markdown":
|
|
51
|
+
return text
|
|
52
|
+
elif content_type in ("application/json", "text/json"):
|
|
53
|
+
return _format_json(text)
|
|
54
|
+
else:
|
|
55
|
+
return text
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _fetch_url(url: str, timeout: int = DEFAULT_TIMEOUT_SEC) -> tuple[str, str]:
|
|
59
|
+
"""
|
|
60
|
+
Fetch URL content synchronously.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Tuple of (content_type, response_text)
|
|
64
|
+
|
|
65
|
+
Raises:
|
|
66
|
+
Various exceptions on failure
|
|
67
|
+
"""
|
|
68
|
+
headers = {
|
|
69
|
+
"Accept": "text/markdown, */*",
|
|
70
|
+
"User-Agent": DEFAULT_USER_AGENT,
|
|
71
|
+
}
|
|
72
|
+
request = urllib.request.Request(url, headers=headers)
|
|
73
|
+
|
|
74
|
+
with urllib.request.urlopen(request, timeout=timeout) as response:
|
|
75
|
+
content_type = _extract_content_type(response)
|
|
76
|
+
data = response.read()
|
|
77
|
+
text = _validate_utf8(data)
|
|
78
|
+
return content_type, text
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@register(tools.WEB_FETCH)
|
|
82
|
+
class WebFetchTool(ToolABC):
|
|
83
|
+
@classmethod
|
|
84
|
+
def schema(cls) -> llm_param.ToolSchema:
|
|
85
|
+
return llm_param.ToolSchema(
|
|
86
|
+
name=tools.WEB_FETCH,
|
|
87
|
+
type="function",
|
|
88
|
+
description=load_desc(Path(__file__).parent / "web_fetch_tool.md"),
|
|
89
|
+
parameters={
|
|
90
|
+
"type": "object",
|
|
91
|
+
"properties": {
|
|
92
|
+
"url": {
|
|
93
|
+
"type": "string",
|
|
94
|
+
"description": "The URL to fetch",
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
"required": ["url"],
|
|
98
|
+
},
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
class WebFetchArguments(BaseModel):
|
|
102
|
+
url: str
|
|
103
|
+
|
|
104
|
+
@classmethod
|
|
105
|
+
async def call(cls, arguments: str) -> model.ToolResultItem:
|
|
106
|
+
try:
|
|
107
|
+
args = WebFetchTool.WebFetchArguments.model_validate_json(arguments)
|
|
108
|
+
except ValueError as e:
|
|
109
|
+
return model.ToolResultItem(
|
|
110
|
+
status="error",
|
|
111
|
+
output=f"Invalid arguments: {e}",
|
|
112
|
+
)
|
|
113
|
+
return await cls.call_with_args(args)
|
|
114
|
+
|
|
115
|
+
@classmethod
|
|
116
|
+
async def call_with_args(cls, args: WebFetchArguments) -> model.ToolResultItem:
|
|
117
|
+
url = args.url
|
|
118
|
+
|
|
119
|
+
# Basic URL validation
|
|
120
|
+
if not url.startswith(("http://", "https://")):
|
|
121
|
+
return model.ToolResultItem(
|
|
122
|
+
status="error",
|
|
123
|
+
output="Invalid URL: must start with http:// or https://",
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
content_type, text = await asyncio.to_thread(_fetch_url, url)
|
|
128
|
+
processed = _process_content(content_type, text)
|
|
129
|
+
|
|
130
|
+
return model.ToolResultItem(
|
|
131
|
+
status="success",
|
|
132
|
+
output=processed,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
except urllib.error.HTTPError as e:
|
|
136
|
+
return model.ToolResultItem(
|
|
137
|
+
status="error",
|
|
138
|
+
output=f"HTTP error {e.code}: {e.reason}",
|
|
139
|
+
)
|
|
140
|
+
except urllib.error.URLError as e:
|
|
141
|
+
return model.ToolResultItem(
|
|
142
|
+
status="error",
|
|
143
|
+
output=f"URL error: {e.reason}",
|
|
144
|
+
)
|
|
145
|
+
except UnicodeDecodeError as e:
|
|
146
|
+
return model.ToolResultItem(
|
|
147
|
+
status="error",
|
|
148
|
+
output=f"Content is not valid UTF-8: {e}",
|
|
149
|
+
)
|
|
150
|
+
except TimeoutError:
|
|
151
|
+
return model.ToolResultItem(
|
|
152
|
+
status="error",
|
|
153
|
+
output=f"Request timed out after {DEFAULT_TIMEOUT_SEC} seconds",
|
|
154
|
+
)
|
|
155
|
+
except Exception as e:
|
|
156
|
+
return model.ToolResultItem(
|
|
157
|
+
status="error",
|
|
158
|
+
output=f"Failed to fetch URL: {e}",
|
|
159
|
+
)
|
klaude_code/core/turn.py
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import AsyncGenerator, Callable, MutableMapping, Sequence
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
from klaude_code.core.tool import TodoContext, ToolABC, tool_context
|
|
7
|
+
from klaude_code.core.tool.tool_runner import (
|
|
8
|
+
ToolExecutionCallStarted,
|
|
9
|
+
ToolExecutionResult,
|
|
10
|
+
ToolExecutionTodoChange,
|
|
11
|
+
ToolExecutor,
|
|
12
|
+
ToolExecutorEvent,
|
|
13
|
+
)
|
|
14
|
+
from klaude_code.llm import LLMClientABC
|
|
15
|
+
from klaude_code.protocol import events, llm_param, model
|
|
16
|
+
from klaude_code.trace import DebugType, log_debug
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TurnError(Exception):
|
|
20
|
+
"""Raised when a turn fails and should be retried."""
|
|
21
|
+
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class TurnExecutionContext:
|
|
27
|
+
"""Execution context required to run a single turn."""
|
|
28
|
+
|
|
29
|
+
session_id: str
|
|
30
|
+
get_conversation_history: Callable[[], list[model.ConversationItem]]
|
|
31
|
+
append_history: Callable[[Sequence[model.ConversationItem]], None]
|
|
32
|
+
llm_client: LLMClientABC
|
|
33
|
+
system_prompt: str | None
|
|
34
|
+
tools: list[llm_param.ToolSchema]
|
|
35
|
+
tool_registry: dict[str, type[ToolABC]]
|
|
36
|
+
# For tool context
|
|
37
|
+
file_tracker: MutableMapping[str, float]
|
|
38
|
+
todo_context: TodoContext
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def build_events_from_tool_executor_event(session_id: str, event: ToolExecutorEvent) -> list[events.Event]:
|
|
42
|
+
"""Translate internal tool executor events into public protocol events."""
|
|
43
|
+
|
|
44
|
+
ui_events: list[events.Event] = []
|
|
45
|
+
|
|
46
|
+
match event:
|
|
47
|
+
case ToolExecutionCallStarted(tool_call=tool_call):
|
|
48
|
+
ui_events.append(
|
|
49
|
+
events.ToolCallEvent(
|
|
50
|
+
session_id=session_id,
|
|
51
|
+
response_id=tool_call.response_id,
|
|
52
|
+
tool_call_id=tool_call.call_id,
|
|
53
|
+
tool_name=tool_call.name,
|
|
54
|
+
arguments=tool_call.arguments,
|
|
55
|
+
)
|
|
56
|
+
)
|
|
57
|
+
case ToolExecutionResult(tool_call=tool_call, tool_result=tool_result):
|
|
58
|
+
ui_events.append(
|
|
59
|
+
events.ToolResultEvent(
|
|
60
|
+
session_id=session_id,
|
|
61
|
+
response_id=tool_call.response_id,
|
|
62
|
+
tool_call_id=tool_call.call_id,
|
|
63
|
+
tool_name=tool_call.name,
|
|
64
|
+
result=tool_result.output or "",
|
|
65
|
+
ui_extra=tool_result.ui_extra,
|
|
66
|
+
status=tool_result.status,
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
case ToolExecutionTodoChange(todos=todos):
|
|
70
|
+
ui_events.append(
|
|
71
|
+
events.TodoChangeEvent(
|
|
72
|
+
session_id=session_id,
|
|
73
|
+
todos=todos,
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
return ui_events
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class TurnExecutor:
|
|
81
|
+
"""Executes a single model turn including tool calls.
|
|
82
|
+
|
|
83
|
+
Manages the lifecycle of tool execution and tool context internally.
|
|
84
|
+
Raises TurnError on failure.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
def __init__(self, context: TurnExecutionContext) -> None:
|
|
88
|
+
self._context = context
|
|
89
|
+
self._tool_executor: ToolExecutor | None = None
|
|
90
|
+
self._has_tool_call: bool = False
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def has_tool_call(self) -> bool:
|
|
94
|
+
return self._has_tool_call
|
|
95
|
+
|
|
96
|
+
def cancel(self) -> list[events.Event]:
|
|
97
|
+
"""Cancel running tools and return any resulting events."""
|
|
98
|
+
ui_events: list[events.Event] = []
|
|
99
|
+
if self._tool_executor is not None:
|
|
100
|
+
for exec_event in self._tool_executor.cancel():
|
|
101
|
+
for ui_event in build_events_from_tool_executor_event(self._context.session_id, exec_event):
|
|
102
|
+
ui_events.append(ui_event)
|
|
103
|
+
self._tool_executor = None
|
|
104
|
+
return ui_events
|
|
105
|
+
|
|
106
|
+
async def run(self) -> AsyncGenerator[events.Event, None]:
|
|
107
|
+
"""Execute the turn, yielding events as they occur.
|
|
108
|
+
|
|
109
|
+
Raises:
|
|
110
|
+
TurnError: If the turn fails (stream error or non-completed status).
|
|
111
|
+
"""
|
|
112
|
+
ctx = self._context
|
|
113
|
+
|
|
114
|
+
yield events.TurnStartEvent(session_id=ctx.session_id)
|
|
115
|
+
|
|
116
|
+
turn_reasoning_items: list[model.ReasoningTextItem | model.ReasoningEncryptedItem] = []
|
|
117
|
+
turn_assistant_message: model.AssistantMessageItem | None = None
|
|
118
|
+
turn_tool_calls: list[model.ToolCallItem] = []
|
|
119
|
+
response_failed = False
|
|
120
|
+
error_message: str | None = None
|
|
121
|
+
|
|
122
|
+
async for response_item in ctx.llm_client.call(
|
|
123
|
+
llm_param.LLMCallParameter(
|
|
124
|
+
input=ctx.get_conversation_history(),
|
|
125
|
+
system=ctx.system_prompt,
|
|
126
|
+
tools=ctx.tools,
|
|
127
|
+
store=False,
|
|
128
|
+
session_id=ctx.session_id,
|
|
129
|
+
)
|
|
130
|
+
):
|
|
131
|
+
log_debug(
|
|
132
|
+
f"[{response_item.__class__.__name__}]",
|
|
133
|
+
response_item.model_dump_json(exclude_none=True),
|
|
134
|
+
style="green",
|
|
135
|
+
debug_type=DebugType.RESPONSE,
|
|
136
|
+
)
|
|
137
|
+
match response_item:
|
|
138
|
+
case model.StartItem():
|
|
139
|
+
pass
|
|
140
|
+
case model.ReasoningTextItem() as item:
|
|
141
|
+
turn_reasoning_items.append(item)
|
|
142
|
+
yield events.ThinkingEvent(
|
|
143
|
+
content=item.content,
|
|
144
|
+
response_id=item.response_id,
|
|
145
|
+
session_id=ctx.session_id,
|
|
146
|
+
)
|
|
147
|
+
case model.ReasoningEncryptedItem() as item:
|
|
148
|
+
turn_reasoning_items.append(item)
|
|
149
|
+
case model.AssistantMessageDelta() as item:
|
|
150
|
+
yield events.AssistantMessageDeltaEvent(
|
|
151
|
+
content=item.content,
|
|
152
|
+
response_id=item.response_id,
|
|
153
|
+
session_id=ctx.session_id,
|
|
154
|
+
)
|
|
155
|
+
case model.AssistantMessageItem() as item:
|
|
156
|
+
turn_assistant_message = item
|
|
157
|
+
yield events.AssistantMessageEvent(
|
|
158
|
+
content=item.content or "",
|
|
159
|
+
response_id=item.response_id,
|
|
160
|
+
session_id=ctx.session_id,
|
|
161
|
+
)
|
|
162
|
+
case model.ResponseMetadataItem() as item:
|
|
163
|
+
yield events.ResponseMetadataEvent(
|
|
164
|
+
session_id=ctx.session_id,
|
|
165
|
+
metadata=item,
|
|
166
|
+
)
|
|
167
|
+
status = item.status
|
|
168
|
+
if status is not None and status != "completed":
|
|
169
|
+
response_failed = True
|
|
170
|
+
error_message = f"Response status: {status}"
|
|
171
|
+
case model.StreamErrorItem() as item:
|
|
172
|
+
response_failed = True
|
|
173
|
+
error_message = item.error
|
|
174
|
+
log_debug(
|
|
175
|
+
"[StreamError]",
|
|
176
|
+
item.error,
|
|
177
|
+
style="red",
|
|
178
|
+
debug_type=DebugType.RESPONSE,
|
|
179
|
+
)
|
|
180
|
+
case model.ToolCallStartItem() as item:
|
|
181
|
+
yield events.TurnToolCallStartEvent(
|
|
182
|
+
session_id=ctx.session_id,
|
|
183
|
+
response_id=item.response_id,
|
|
184
|
+
tool_call_id=item.call_id,
|
|
185
|
+
tool_name=item.name,
|
|
186
|
+
arguments="",
|
|
187
|
+
)
|
|
188
|
+
case model.ToolCallItem() as item:
|
|
189
|
+
turn_tool_calls.append(item)
|
|
190
|
+
case _:
|
|
191
|
+
pass
|
|
192
|
+
|
|
193
|
+
if response_failed:
|
|
194
|
+
yield events.TurnEndEvent(session_id=ctx.session_id)
|
|
195
|
+
raise TurnError(error_message or "Turn failed")
|
|
196
|
+
|
|
197
|
+
# Append to history only on success
|
|
198
|
+
if turn_reasoning_items:
|
|
199
|
+
ctx.append_history(turn_reasoning_items)
|
|
200
|
+
if turn_assistant_message:
|
|
201
|
+
ctx.append_history([turn_assistant_message])
|
|
202
|
+
if turn_tool_calls:
|
|
203
|
+
ctx.append_history(turn_tool_calls)
|
|
204
|
+
self._has_tool_call = True
|
|
205
|
+
|
|
206
|
+
# Execute tools
|
|
207
|
+
if turn_tool_calls:
|
|
208
|
+
with tool_context(ctx.file_tracker, ctx.todo_context):
|
|
209
|
+
executor = ToolExecutor(
|
|
210
|
+
registry=ctx.tool_registry,
|
|
211
|
+
append_history=ctx.append_history,
|
|
212
|
+
)
|
|
213
|
+
self._tool_executor = executor
|
|
214
|
+
|
|
215
|
+
async for exec_event in executor.run_tools(turn_tool_calls):
|
|
216
|
+
for ui_event in build_events_from_tool_executor_event(ctx.session_id, exec_event):
|
|
217
|
+
yield ui_event
|
|
218
|
+
self._tool_executor = None
|
|
219
|
+
|
|
220
|
+
yield events.TurnEndEvent(session_id=ctx.session_id)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""LLM package init.
|
|
2
|
+
|
|
3
|
+
Imports built-in LLM clients so their ``@register`` decorators run and they
|
|
4
|
+
become available via the registry.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .anthropic import AnthropicClient
|
|
8
|
+
from .client import LLMClientABC
|
|
9
|
+
from .openai_compatible import OpenAICompatibleClient
|
|
10
|
+
from .openrouter import OpenRouterClient
|
|
11
|
+
from .registry import create_llm_client
|
|
12
|
+
from .responses import ResponsesClient
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"LLMClientABC",
|
|
16
|
+
"ResponsesClient",
|
|
17
|
+
"OpenAICompatibleClient",
|
|
18
|
+
"OpenRouterClient",
|
|
19
|
+
"AnthropicClient",
|
|
20
|
+
"create_llm_client",
|
|
21
|
+
]
|