klaude-code 1.2.16__py3-none-any.whl → 1.2.17__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- klaude_code/cli/runtime.py +12 -2
- klaude_code/command/__init__.py +3 -0
- klaude_code/command/export_online_cmd.py +149 -0
- klaude_code/config/config.py +16 -17
- klaude_code/core/manager/sub_agent_manager.py +1 -1
- klaude_code/core/prompts/prompt-sub-agent-oracle.md +0 -1
- klaude_code/core/prompts/prompt-sub-agent-web.md +48 -0
- klaude_code/core/task.py +8 -0
- klaude_code/core/tool/__init__.py +2 -0
- klaude_code/core/tool/report_back_tool.py +28 -2
- klaude_code/core/tool/web/web_search_tool.md +23 -0
- klaude_code/core/tool/web/web_search_tool.py +126 -0
- klaude_code/protocol/commands.py +1 -0
- klaude_code/protocol/events.py +8 -0
- klaude_code/protocol/sub_agent/__init__.py +1 -1
- klaude_code/protocol/sub_agent/explore.py +1 -1
- klaude_code/protocol/sub_agent/web.py +78 -0
- klaude_code/protocol/tools.py +1 -0
- klaude_code/session/templates/export_session.html +99 -24
- klaude_code/ui/modes/repl/event_handler.py +36 -9
- klaude_code/ui/modes/repl/renderer.py +3 -3
- klaude_code/ui/renderers/sub_agent.py +14 -10
- klaude_code/ui/renderers/tools.py +4 -10
- klaude_code/ui/rich/status.py +32 -6
- {klaude_code-1.2.16.dist-info → klaude_code-1.2.17.dist-info}/METADATA +112 -21
- {klaude_code-1.2.16.dist-info → klaude_code-1.2.17.dist-info}/RECORD +28 -25
- klaude_code/core/prompts/prompt-sub-agent-webfetch.md +0 -46
- klaude_code/protocol/sub_agent/web_fetch.py +0 -74
- {klaude_code-1.2.16.dist-info → klaude_code-1.2.17.dist-info}/WHEEL +0 -0
- {klaude_code-1.2.16.dist-info → klaude_code-1.2.17.dist-info}/entry_points.txt +0 -0
klaude_code/cli/runtime.py
CHANGED
|
@@ -261,7 +261,15 @@ async def run_interactive(init_config: AppInitConfig, session_id: str | None = N
|
|
|
261
261
|
restore_sigint = install_sigint_double_press_exit(_show_toast_once, _hide_progress)
|
|
262
262
|
|
|
263
263
|
try:
|
|
264
|
-
|
|
264
|
+
await initialize_session(components.executor, components.event_queue, session_id=session_id)
|
|
265
|
+
|
|
266
|
+
def _get_active_session_id() -> str | None:
|
|
267
|
+
"""Get the current active session ID dynamically.
|
|
268
|
+
|
|
269
|
+
This is necessary because /clear command creates a new session with a different ID.
|
|
270
|
+
"""
|
|
271
|
+
active_ids = components.executor.context.agent_manager.active_session_ids()
|
|
272
|
+
return active_ids[0] if active_ids else None
|
|
265
273
|
|
|
266
274
|
# Input
|
|
267
275
|
await input_provider.start()
|
|
@@ -272,6 +280,8 @@ async def run_interactive(init_config: AppInitConfig, session_id: str | None = N
|
|
|
272
280
|
elif user_input.text.strip() == "":
|
|
273
281
|
continue
|
|
274
282
|
# Submit user input operation - directly use the payload from iter_inputs
|
|
283
|
+
# Use dynamic session_id lookup to handle /clear creating new sessions
|
|
284
|
+
active_session_id = _get_active_session_id()
|
|
275
285
|
submission_id = await components.executor.submit(
|
|
276
286
|
op.UserInputOperation(input=user_input, session_id=active_session_id)
|
|
277
287
|
)
|
|
@@ -282,7 +292,7 @@ async def run_interactive(init_config: AppInitConfig, session_id: str | None = N
|
|
|
282
292
|
else:
|
|
283
293
|
# Esc monitor for long-running, interruptible operations
|
|
284
294
|
async def _on_esc_interrupt() -> None:
|
|
285
|
-
await components.executor.submit(op.InterruptOperation(target_session_id=
|
|
295
|
+
await components.executor.submit(op.InterruptOperation(target_session_id=_get_active_session_id()))
|
|
286
296
|
|
|
287
297
|
stop_event, esc_task = start_esc_interrupt_monitor(_on_esc_interrupt)
|
|
288
298
|
# Wait for this specific task to complete before accepting next input
|
klaude_code/command/__init__.py
CHANGED
|
@@ -30,6 +30,7 @@ def ensure_commands_loaded() -> None:
|
|
|
30
30
|
from .clear_cmd import ClearCommand
|
|
31
31
|
from .diff_cmd import DiffCommand
|
|
32
32
|
from .export_cmd import ExportCommand
|
|
33
|
+
from .export_online_cmd import ExportOnlineCommand
|
|
33
34
|
from .help_cmd import HelpCommand
|
|
34
35
|
from .model_cmd import ModelCommand
|
|
35
36
|
from .refresh_cmd import RefreshTerminalCommand
|
|
@@ -40,6 +41,7 @@ def ensure_commands_loaded() -> None:
|
|
|
40
41
|
|
|
41
42
|
# Register in desired display order
|
|
42
43
|
register(ExportCommand())
|
|
44
|
+
register(ExportOnlineCommand())
|
|
43
45
|
register(RefreshTerminalCommand())
|
|
44
46
|
register(ThinkingCommand())
|
|
45
47
|
register(ModelCommand())
|
|
@@ -60,6 +62,7 @@ def __getattr__(name: str) -> object:
|
|
|
60
62
|
"ClearCommand": "clear_cmd",
|
|
61
63
|
"DiffCommand": "diff_cmd",
|
|
62
64
|
"ExportCommand": "export_cmd",
|
|
65
|
+
"ExportOnlineCommand": "export_online_cmd",
|
|
63
66
|
"HelpCommand": "help_cmd",
|
|
64
67
|
"ModelCommand": "model_cmd",
|
|
65
68
|
"RefreshTerminalCommand": "refresh_cmd",
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import secrets
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
import tempfile
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from klaude_code.command.command_abc import CommandABC, CommandResult
|
|
11
|
+
from klaude_code.protocol import commands, events, model
|
|
12
|
+
from klaude_code.session.export import build_export_html
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from klaude_code.core.agent import Agent
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ExportOnlineCommand(CommandABC):
|
|
19
|
+
"""Export and deploy the current session to surge.sh as a static webpage."""
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def name(self) -> commands.CommandName:
|
|
23
|
+
return commands.CommandName.EXPORT_ONLINE
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def summary(self) -> str:
|
|
27
|
+
return "Export and deploy session to surge.sh"
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def support_addition_params(self) -> bool:
|
|
31
|
+
return False
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def is_interactive(self) -> bool:
|
|
35
|
+
return False
|
|
36
|
+
|
|
37
|
+
async def run(self, raw: str, agent: Agent) -> CommandResult:
|
|
38
|
+
# Check if npx or surge is available
|
|
39
|
+
surge_cmd = self._get_surge_command()
|
|
40
|
+
if not surge_cmd:
|
|
41
|
+
event = events.DeveloperMessageEvent(
|
|
42
|
+
session_id=agent.session.id,
|
|
43
|
+
item=model.DeveloperMessageItem(
|
|
44
|
+
content="surge.sh CLI not found. Install with: npm install -g surge",
|
|
45
|
+
command_output=model.CommandOutput(command_name=self.name, is_error=True),
|
|
46
|
+
),
|
|
47
|
+
)
|
|
48
|
+
return CommandResult(events=[event])
|
|
49
|
+
|
|
50
|
+
# Check if user is logged in to surge
|
|
51
|
+
if not self._is_surge_logged_in(surge_cmd):
|
|
52
|
+
login_cmd = " ".join([*surge_cmd, "login"])
|
|
53
|
+
event = events.DeveloperMessageEvent(
|
|
54
|
+
session_id=agent.session.id,
|
|
55
|
+
item=model.DeveloperMessageItem(
|
|
56
|
+
content=f"Not logged in to surge.sh. Please run: {login_cmd}",
|
|
57
|
+
command_output=model.CommandOutput(command_name=self.name, is_error=True),
|
|
58
|
+
),
|
|
59
|
+
)
|
|
60
|
+
return CommandResult(events=[event])
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
html_doc = self._build_html(agent)
|
|
64
|
+
domain = self._generate_domain()
|
|
65
|
+
url = self._deploy_to_surge(surge_cmd, html_doc, domain)
|
|
66
|
+
|
|
67
|
+
event = events.DeveloperMessageEvent(
|
|
68
|
+
session_id=agent.session.id,
|
|
69
|
+
item=model.DeveloperMessageItem(
|
|
70
|
+
content=f"Session deployed to: {url}",
|
|
71
|
+
command_output=model.CommandOutput(command_name=self.name),
|
|
72
|
+
),
|
|
73
|
+
)
|
|
74
|
+
return CommandResult(events=[event])
|
|
75
|
+
except Exception as exc:
|
|
76
|
+
import traceback
|
|
77
|
+
|
|
78
|
+
event = events.DeveloperMessageEvent(
|
|
79
|
+
session_id=agent.session.id,
|
|
80
|
+
item=model.DeveloperMessageItem(
|
|
81
|
+
content=f"Failed to deploy session: {exc}\n{traceback.format_exc()}",
|
|
82
|
+
command_output=model.CommandOutput(command_name=self.name, is_error=True),
|
|
83
|
+
),
|
|
84
|
+
)
|
|
85
|
+
return CommandResult(events=[event])
|
|
86
|
+
|
|
87
|
+
def _get_surge_command(self) -> list[str] | None:
|
|
88
|
+
"""Check if surge CLI is available, prefer npx if available."""
|
|
89
|
+
# Check for npx first (more common)
|
|
90
|
+
if shutil.which("npx"):
|
|
91
|
+
return ["npx", "surge"]
|
|
92
|
+
# Check for globally installed surge
|
|
93
|
+
if shutil.which("surge"):
|
|
94
|
+
return ["surge"]
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
def _is_surge_logged_in(self, surge_cmd: list[str]) -> bool:
|
|
98
|
+
"""Check if user is logged in to surge.sh via 'surge whoami'."""
|
|
99
|
+
try:
|
|
100
|
+
cmd = [*surge_cmd, "whoami"]
|
|
101
|
+
result = subprocess.run(
|
|
102
|
+
cmd,
|
|
103
|
+
capture_output=True,
|
|
104
|
+
text=True,
|
|
105
|
+
timeout=30,
|
|
106
|
+
)
|
|
107
|
+
# If logged in, whoami returns 0 and prints the email
|
|
108
|
+
# If not logged in, it returns non-zero or prints "Not Authenticated"
|
|
109
|
+
if result.returncode != 0:
|
|
110
|
+
return False
|
|
111
|
+
output = (result.stdout + result.stderr).lower()
|
|
112
|
+
if "not authenticated" in output or "not logged in" in output:
|
|
113
|
+
return False
|
|
114
|
+
return bool(result.stdout.strip())
|
|
115
|
+
except (subprocess.TimeoutExpired, OSError):
|
|
116
|
+
return False
|
|
117
|
+
|
|
118
|
+
def _generate_domain(self) -> str:
|
|
119
|
+
"""Generate a random subdomain for surge.sh."""
|
|
120
|
+
random_suffix = secrets.token_hex(4)
|
|
121
|
+
return f"klaude-session-{random_suffix}.surge.sh"
|
|
122
|
+
|
|
123
|
+
def _deploy_to_surge(self, surge_cmd: list[str], html_content: str, domain: str) -> str:
|
|
124
|
+
"""Deploy HTML content to surge.sh and return the URL."""
|
|
125
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
126
|
+
html_path = Path(tmpdir) / "index.html"
|
|
127
|
+
html_path.write_text(html_content, encoding="utf-8")
|
|
128
|
+
|
|
129
|
+
# Run surge with --domain flag
|
|
130
|
+
cmd = [*surge_cmd, tmpdir, "--domain", domain]
|
|
131
|
+
result = subprocess.run(
|
|
132
|
+
cmd,
|
|
133
|
+
capture_output=True,
|
|
134
|
+
text=True,
|
|
135
|
+
timeout=60,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
if result.returncode != 0:
|
|
139
|
+
error_msg = result.stderr or result.stdout or "Unknown error"
|
|
140
|
+
raise RuntimeError(f"Surge deployment failed: {error_msg}")
|
|
141
|
+
|
|
142
|
+
return f"https://{domain}"
|
|
143
|
+
|
|
144
|
+
def _build_html(self, agent: Agent) -> str:
|
|
145
|
+
profile = agent.profile
|
|
146
|
+
system_prompt = (profile.system_prompt if profile else "") or ""
|
|
147
|
+
tools = profile.tools if profile else []
|
|
148
|
+
model_name = profile.llm_client.model_name if profile else "unknown"
|
|
149
|
+
return build_export_html(agent.session, system_prompt, tools, model_name)
|
klaude_code/config/config.py
CHANGED
|
@@ -79,8 +79,8 @@ class Config(BaseModel):
|
|
|
79
79
|
|
|
80
80
|
def get_example_config() -> Config:
|
|
81
81
|
return Config(
|
|
82
|
-
main_model="
|
|
83
|
-
sub_agent_models={"explore": "haiku", "oracle": "gpt-5.1
|
|
82
|
+
main_model="opus",
|
|
83
|
+
sub_agent_models={"explore": "haiku", "oracle": "gpt-5.1", "webagent": "haiku", "task": "opus"},
|
|
84
84
|
provider_list=[
|
|
85
85
|
llm_param.LLMConfigProviderParameter(
|
|
86
86
|
provider_name="openai",
|
|
@@ -93,6 +93,11 @@ def get_example_config() -> Config:
|
|
|
93
93
|
protocol=llm_param.LLMClientProtocol.OPENROUTER,
|
|
94
94
|
api_key="your-openrouter-api-key",
|
|
95
95
|
),
|
|
96
|
+
llm_param.LLMConfigProviderParameter(
|
|
97
|
+
provider_name="anthropic",
|
|
98
|
+
protocol=llm_param.LLMClientProtocol.ANTHROPIC,
|
|
99
|
+
api_key="your-anthropic-api-key",
|
|
100
|
+
),
|
|
96
101
|
],
|
|
97
102
|
model_list=[
|
|
98
103
|
ModelConfig(
|
|
@@ -100,31 +105,25 @@ def get_example_config() -> Config:
|
|
|
100
105
|
provider="openai",
|
|
101
106
|
model_params=llm_param.LLMConfigModelParameter(
|
|
102
107
|
model="gpt-5.1-2025-11-13",
|
|
103
|
-
max_tokens=32000,
|
|
104
108
|
verbosity="medium",
|
|
105
109
|
thinking=llm_param.Thinking(
|
|
106
|
-
reasoning_effort="
|
|
110
|
+
reasoning_effort="high",
|
|
107
111
|
reasoning_summary="auto",
|
|
108
|
-
type="enabled",
|
|
109
|
-
budget_tokens=None,
|
|
110
112
|
),
|
|
111
|
-
context_limit=
|
|
113
|
+
context_limit=400000,
|
|
112
114
|
),
|
|
113
115
|
),
|
|
114
116
|
ModelConfig(
|
|
115
|
-
model_name="
|
|
116
|
-
provider="
|
|
117
|
+
model_name="opus",
|
|
118
|
+
provider="anthropic",
|
|
117
119
|
model_params=llm_param.LLMConfigModelParameter(
|
|
118
|
-
model="
|
|
119
|
-
|
|
120
|
-
verbosity="medium",
|
|
120
|
+
model="claude-opus-4-5-20251101",
|
|
121
|
+
verbosity="high",
|
|
121
122
|
thinking=llm_param.Thinking(
|
|
122
|
-
reasoning_effort="high",
|
|
123
|
-
reasoning_summary="auto",
|
|
124
123
|
type="enabled",
|
|
125
|
-
budget_tokens=
|
|
124
|
+
budget_tokens=31999,
|
|
126
125
|
),
|
|
127
|
-
context_limit=
|
|
126
|
+
context_limit=200000,
|
|
128
127
|
),
|
|
129
128
|
),
|
|
130
129
|
ModelConfig(
|
|
@@ -136,7 +135,7 @@ def get_example_config() -> Config:
|
|
|
136
135
|
provider_routing=llm_param.OpenRouterProviderRouting(
|
|
137
136
|
sort="throughput",
|
|
138
137
|
),
|
|
139
|
-
context_limit=
|
|
138
|
+
context_limit=200000,
|
|
140
139
|
),
|
|
141
140
|
),
|
|
142
141
|
],
|
|
@@ -57,7 +57,7 @@ class SubAgentManager:
|
|
|
57
57
|
# Structured Output
|
|
58
58
|
You have a `report_back` tool available. When you complete the task,\
|
|
59
59
|
you MUST call `report_back` with the structured result matching the required schema.\
|
|
60
|
-
|
|
60
|
+
Only the content passed to `report_back` will be returned to user.\
|
|
61
61
|
"""
|
|
62
62
|
base_prompt = child_profile.system_prompt or ""
|
|
63
63
|
child_profile = AgentProfile(
|
|
@@ -3,7 +3,6 @@ You are the Oracle - an expert AI advisor with advanced reasoning capabilities
|
|
|
3
3
|
Your role is to provide high-quality technical guidance, code reviews, architectural advice, and strategic planning for software engineering tasks.
|
|
4
4
|
You are running inside an AI coding system in which you act as a sub-agent that's used when the main agent needs a smarter, more capable model to help out.
|
|
5
5
|
|
|
6
|
-
|
|
7
6
|
Key responsibilities:
|
|
8
7
|
- Analyze code and architecture patterns
|
|
9
8
|
- Provide detailed technical reviews and recommendations
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
You are a web research agent that searches and fetches web content to provide up-to-date information.
|
|
2
|
+
|
|
3
|
+
## Available Tools
|
|
4
|
+
|
|
5
|
+
**WebSearch**: Search the web via DuckDuckGo
|
|
6
|
+
- Returns: title, URL, and snippet for each result
|
|
7
|
+
- Parameter `max_results`: control result count (default: 10, max: 20)
|
|
8
|
+
- Snippets are brief summaries - use WebFetch for full content
|
|
9
|
+
|
|
10
|
+
**WebFetch**: Fetch and process web page content
|
|
11
|
+
- HTML pages are automatically converted to Markdown
|
|
12
|
+
- JSON responses are auto-formatted with indentation
|
|
13
|
+
- Other text content returned as-is
|
|
14
|
+
|
|
15
|
+
## Tool Usage Strategy
|
|
16
|
+
|
|
17
|
+
Scale tool calls to query complexity:
|
|
18
|
+
- Simple facts: 1-2 calls
|
|
19
|
+
- Medium research: 3-5 calls
|
|
20
|
+
- Deep research/comparisons: 5-10 calls
|
|
21
|
+
|
|
22
|
+
Balance efficiency with thoroughness. For open-ended questions (e.g., "recommendations for video games" or "recent developments in RL"), use more calls for comprehensive answers.
|
|
23
|
+
|
|
24
|
+
## Search Guidelines
|
|
25
|
+
|
|
26
|
+
- Keep queries concise (1-6 words). Start broad, then narrow if needed
|
|
27
|
+
- Avoid repeating similar queries - they won't yield new results
|
|
28
|
+
- NEVER use '-', 'site:', or quotes unless explicitly asked
|
|
29
|
+
- Include year/date for time-sensitive queries (check "Today's date" in <env>)
|
|
30
|
+
- Use WebFetch to get full content - search snippets are often insufficient
|
|
31
|
+
- Follow relevant links on pages with WebFetch
|
|
32
|
+
- If truncated results are saved to local files, use grep/read to explore
|
|
33
|
+
|
|
34
|
+
## Response Guidelines
|
|
35
|
+
|
|
36
|
+
- Only your last message is returned to the main agent
|
|
37
|
+
- Be succinct - include only relevant information
|
|
38
|
+
- Lead with the most recent info for evolving topics
|
|
39
|
+
- Favor original sources (company blogs, papers, gov sites) over aggregators
|
|
40
|
+
- Note conflicting sources when they exist
|
|
41
|
+
|
|
42
|
+
## Sources (REQUIRED)
|
|
43
|
+
|
|
44
|
+
You MUST end every response with a "Sources:" section listing all URLs as markdown links:
|
|
45
|
+
|
|
46
|
+
Sources:
|
|
47
|
+
- [Source Title](https://example.com)
|
|
48
|
+
- [Another Source](https://example.com/page) (saved: /path/to/file)
|
klaude_code/core/task.py
CHANGED
|
@@ -182,6 +182,14 @@ class TaskExecutor:
|
|
|
182
182
|
yield am
|
|
183
183
|
case events.ResponseMetadataEvent() as e:
|
|
184
184
|
metadata_accumulator.add(e.metadata)
|
|
185
|
+
# Emit context usage event if available
|
|
186
|
+
if e.metadata.usage is not None:
|
|
187
|
+
context_percent = e.metadata.usage.context_usage_percent
|
|
188
|
+
if context_percent is not None:
|
|
189
|
+
yield events.ContextUsageEvent(
|
|
190
|
+
session_id=session_ctx.session_id,
|
|
191
|
+
context_percent=context_percent,
|
|
192
|
+
)
|
|
185
193
|
case events.ToolResultEvent() as e:
|
|
186
194
|
# Collect sub-agent task metadata from tool results
|
|
187
195
|
if e.task_metadata is not None:
|
|
@@ -28,6 +28,7 @@ from .tool_runner import run_tool
|
|
|
28
28
|
from .truncation import SimpleTruncationStrategy, TruncationStrategy, get_truncation_strategy, set_truncation_strategy
|
|
29
29
|
from .web.mermaid_tool import MermaidTool
|
|
30
30
|
from .web.web_fetch_tool import WebFetchTool
|
|
31
|
+
from .web.web_search_tool import WebSearchTool
|
|
31
32
|
|
|
32
33
|
__all__ = [
|
|
33
34
|
"MEMORY_DIR_NAME",
|
|
@@ -53,6 +54,7 @@ __all__ = [
|
|
|
53
54
|
"TruncationStrategy",
|
|
54
55
|
"UpdatePlanTool",
|
|
55
56
|
"WebFetchTool",
|
|
57
|
+
"WebSearchTool",
|
|
56
58
|
"WriteTool",
|
|
57
59
|
"build_todo_context",
|
|
58
60
|
"current_run_subtask_callback",
|
|
@@ -1,10 +1,35 @@
|
|
|
1
1
|
"""ReportBackTool for sub-agents to return structured output."""
|
|
2
2
|
|
|
3
|
-
from typing import Any, ClassVar
|
|
3
|
+
from typing import Any, ClassVar, cast
|
|
4
4
|
|
|
5
5
|
from klaude_code.protocol import llm_param, model, tools
|
|
6
6
|
|
|
7
7
|
|
|
8
|
+
def _normalize_schema_types(schema: dict[str, Any]) -> dict[str, Any]:
|
|
9
|
+
"""Recursively normalize JSON schema type values to lowercase.
|
|
10
|
+
|
|
11
|
+
Some LLMs (e.g., Gemini 3) generate type values in uppercase like "OBJECT", "STRING".
|
|
12
|
+
Standard JSON Schema requires lowercase type values.
|
|
13
|
+
"""
|
|
14
|
+
result: dict[str, Any] = {}
|
|
15
|
+
for key, value in schema.items():
|
|
16
|
+
if key == "type" and isinstance(value, str):
|
|
17
|
+
result[key] = value.lower()
|
|
18
|
+
elif isinstance(value, dict):
|
|
19
|
+
result[key] = _normalize_schema_types(cast(dict[str, Any], value))
|
|
20
|
+
elif isinstance(value, list):
|
|
21
|
+
normalized_list: list[Any] = []
|
|
22
|
+
for item in cast(list[Any], value):
|
|
23
|
+
if isinstance(item, dict):
|
|
24
|
+
normalized_list.append(_normalize_schema_types(cast(dict[str, Any], item)))
|
|
25
|
+
else:
|
|
26
|
+
normalized_list.append(item)
|
|
27
|
+
result[key] = normalized_list
|
|
28
|
+
else:
|
|
29
|
+
result[key] = value
|
|
30
|
+
return result
|
|
31
|
+
|
|
32
|
+
|
|
8
33
|
class ReportBackTool:
|
|
9
34
|
"""Special tool for sub-agents to return structured output and end the task.
|
|
10
35
|
|
|
@@ -29,7 +54,8 @@ class ReportBackTool:
|
|
|
29
54
|
Returns:
|
|
30
55
|
A new class with the schema set as a class variable.
|
|
31
56
|
"""
|
|
32
|
-
|
|
57
|
+
normalized = _normalize_schema_types(schema)
|
|
58
|
+
return type("ReportBackTool", (ReportBackTool,), {"_schema": normalized})
|
|
33
59
|
|
|
34
60
|
@classmethod
|
|
35
61
|
def schema(cls) -> llm_param.ToolSchema:
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
- Search the web and use the results to inform responses
|
|
2
|
+
- Provides up-to-date information for current events and recent data
|
|
3
|
+
- Returns search result information formatted as search result blocks, including links as markdown hyperlinks
|
|
4
|
+
- Use this tool for accessing information beyond your knowledge cutoff
|
|
5
|
+
- Searches are performed automatically within a single API call
|
|
6
|
+
|
|
7
|
+
CRITICAL REQUIREMENT - You MUST follow this:
|
|
8
|
+
- After answering the user's question, you MUST include a "Sources:" section at the end of your response
|
|
9
|
+
- In the Sources section, list all relevant URLs from the search results as markdown hyperlinks: [Title](URL)
|
|
10
|
+
- This is MANDATORY - never skip including sources in your response
|
|
11
|
+
- Example format:
|
|
12
|
+
|
|
13
|
+
[Your answer here]
|
|
14
|
+
|
|
15
|
+
Sources:
|
|
16
|
+
- [Source Title 1](https://example.com/1)
|
|
17
|
+
- [Source Title 2](https://example.com/2)
|
|
18
|
+
|
|
19
|
+
Usage notes:
|
|
20
|
+
- Domain filtering is supported to include or block specific websites
|
|
21
|
+
- Web search is only available in the US
|
|
22
|
+
- Account for "Today's date" in <env>. For example, if <env> says "Today's date: 2025-07-01", and the user wants the latest docs, do not use 2024 in the search query. Use 2025.
|
|
23
|
+
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
from klaude_code.core.tool.tool_abc import ToolABC, load_desc
|
|
8
|
+
from klaude_code.core.tool.tool_registry import register
|
|
9
|
+
from klaude_code.protocol import llm_param, model, tools
|
|
10
|
+
|
|
11
|
+
DEFAULT_MAX_RESULTS = 10
|
|
12
|
+
MAX_RESULTS_LIMIT = 20
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class SearchResult:
|
|
17
|
+
"""A single search result from DuckDuckGo."""
|
|
18
|
+
|
|
19
|
+
title: str
|
|
20
|
+
url: str
|
|
21
|
+
snippet: str
|
|
22
|
+
position: int
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _search_duckduckgo(query: str, max_results: int) -> list[SearchResult]:
|
|
26
|
+
"""Perform a web search using ddgs library."""
|
|
27
|
+
from ddgs import DDGS # type: ignore
|
|
28
|
+
|
|
29
|
+
results: list[SearchResult] = []
|
|
30
|
+
|
|
31
|
+
with DDGS() as ddgs:
|
|
32
|
+
for i, r in enumerate(ddgs.text(query, max_results=max_results)):
|
|
33
|
+
results.append(
|
|
34
|
+
SearchResult(
|
|
35
|
+
title=r.get("title", ""),
|
|
36
|
+
url=r.get("href", ""),
|
|
37
|
+
snippet=r.get("body", ""),
|
|
38
|
+
position=i + 1,
|
|
39
|
+
)
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
return results
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _format_results(results: list[SearchResult]) -> str:
|
|
46
|
+
"""Format search results for LLM consumption."""
|
|
47
|
+
if not results:
|
|
48
|
+
return (
|
|
49
|
+
"No results were found for your search query. "
|
|
50
|
+
"Please try rephrasing your search or using different keywords."
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
lines = [f"Found {len(results)} search results:\n"]
|
|
54
|
+
|
|
55
|
+
for result in results:
|
|
56
|
+
lines.append(f"{result.position}. {result.title}")
|
|
57
|
+
lines.append(f" URL: {result.url}")
|
|
58
|
+
lines.append(f" Summary: {result.snippet}\n")
|
|
59
|
+
|
|
60
|
+
return "\n".join(lines)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@register(tools.WEB_SEARCH)
|
|
64
|
+
class WebSearchTool(ToolABC):
|
|
65
|
+
@classmethod
|
|
66
|
+
def schema(cls) -> llm_param.ToolSchema:
|
|
67
|
+
return llm_param.ToolSchema(
|
|
68
|
+
name=tools.WEB_SEARCH,
|
|
69
|
+
type="function",
|
|
70
|
+
description=load_desc(Path(__file__).parent / "web_search_tool.md"),
|
|
71
|
+
parameters={
|
|
72
|
+
"type": "object",
|
|
73
|
+
"properties": {
|
|
74
|
+
"query": {
|
|
75
|
+
"type": "string",
|
|
76
|
+
"description": "The search query to use",
|
|
77
|
+
},
|
|
78
|
+
"max_results": {
|
|
79
|
+
"type": "integer",
|
|
80
|
+
"description": f"Maximum number of results to return (default: {DEFAULT_MAX_RESULTS}, max: {MAX_RESULTS_LIMIT})",
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
"required": ["query"],
|
|
84
|
+
},
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
class WebSearchArguments(BaseModel):
|
|
88
|
+
query: str
|
|
89
|
+
max_results: int = DEFAULT_MAX_RESULTS
|
|
90
|
+
|
|
91
|
+
@classmethod
|
|
92
|
+
async def call(cls, arguments: str) -> model.ToolResultItem:
|
|
93
|
+
try:
|
|
94
|
+
args = WebSearchTool.WebSearchArguments.model_validate_json(arguments)
|
|
95
|
+
except ValueError as e:
|
|
96
|
+
return model.ToolResultItem(
|
|
97
|
+
status="error",
|
|
98
|
+
output=f"Invalid arguments: {e}",
|
|
99
|
+
)
|
|
100
|
+
return await cls.call_with_args(args)
|
|
101
|
+
|
|
102
|
+
@classmethod
|
|
103
|
+
async def call_with_args(cls, args: WebSearchArguments) -> model.ToolResultItem:
|
|
104
|
+
query = args.query.strip()
|
|
105
|
+
if not query:
|
|
106
|
+
return model.ToolResultItem(
|
|
107
|
+
status="error",
|
|
108
|
+
output="Query cannot be empty",
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
max_results = min(max(args.max_results, 1), MAX_RESULTS_LIMIT)
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
results = await asyncio.to_thread(_search_duckduckgo, query, max_results)
|
|
115
|
+
formatted = _format_results(results)
|
|
116
|
+
|
|
117
|
+
return model.ToolResultItem(
|
|
118
|
+
status="success",
|
|
119
|
+
output=formatted,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
except Exception as e:
|
|
123
|
+
return model.ToolResultItem(
|
|
124
|
+
status="error",
|
|
125
|
+
output=f"Search failed: {e}",
|
|
126
|
+
)
|
klaude_code/protocol/commands.py
CHANGED
klaude_code/protocol/events.py
CHANGED
|
@@ -133,6 +133,13 @@ class TodoChangeEvent(BaseModel):
|
|
|
133
133
|
todos: list[model.TodoItem]
|
|
134
134
|
|
|
135
135
|
|
|
136
|
+
class ContextUsageEvent(BaseModel):
|
|
137
|
+
"""Real-time context usage update during task execution."""
|
|
138
|
+
|
|
139
|
+
session_id: str
|
|
140
|
+
context_percent: float # Context usage percentage (0-100)
|
|
141
|
+
|
|
142
|
+
|
|
136
143
|
HistoryItemEvent = (
|
|
137
144
|
ThinkingEvent
|
|
138
145
|
| TaskStartEvent
|
|
@@ -178,4 +185,5 @@ Event = (
|
|
|
178
185
|
| TurnStartEvent
|
|
179
186
|
| TurnEndEvent
|
|
180
187
|
| TurnToolCallStartEvent
|
|
188
|
+
| ContextUsageEvent
|
|
181
189
|
)
|
|
@@ -114,4 +114,4 @@ def sub_agent_tool_names(enabled_only: bool = False, model_name: str | None = No
|
|
|
114
114
|
from klaude_code.protocol.sub_agent import explore as explore # noqa: E402
|
|
115
115
|
from klaude_code.protocol.sub_agent import oracle as oracle # noqa: E402
|
|
116
116
|
from klaude_code.protocol.sub_agent import task as task # noqa: E402
|
|
117
|
-
from klaude_code.protocol.sub_agent import
|
|
117
|
+
from klaude_code.protocol.sub_agent import web as web # noqa: E402
|