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.
Files changed (30) hide show
  1. klaude_code/cli/runtime.py +12 -2
  2. klaude_code/command/__init__.py +3 -0
  3. klaude_code/command/export_online_cmd.py +149 -0
  4. klaude_code/config/config.py +16 -17
  5. klaude_code/core/manager/sub_agent_manager.py +1 -1
  6. klaude_code/core/prompts/prompt-sub-agent-oracle.md +0 -1
  7. klaude_code/core/prompts/prompt-sub-agent-web.md +48 -0
  8. klaude_code/core/task.py +8 -0
  9. klaude_code/core/tool/__init__.py +2 -0
  10. klaude_code/core/tool/report_back_tool.py +28 -2
  11. klaude_code/core/tool/web/web_search_tool.md +23 -0
  12. klaude_code/core/tool/web/web_search_tool.py +126 -0
  13. klaude_code/protocol/commands.py +1 -0
  14. klaude_code/protocol/events.py +8 -0
  15. klaude_code/protocol/sub_agent/__init__.py +1 -1
  16. klaude_code/protocol/sub_agent/explore.py +1 -1
  17. klaude_code/protocol/sub_agent/web.py +78 -0
  18. klaude_code/protocol/tools.py +1 -0
  19. klaude_code/session/templates/export_session.html +99 -24
  20. klaude_code/ui/modes/repl/event_handler.py +36 -9
  21. klaude_code/ui/modes/repl/renderer.py +3 -3
  22. klaude_code/ui/renderers/sub_agent.py +14 -10
  23. klaude_code/ui/renderers/tools.py +4 -10
  24. klaude_code/ui/rich/status.py +32 -6
  25. {klaude_code-1.2.16.dist-info → klaude_code-1.2.17.dist-info}/METADATA +112 -21
  26. {klaude_code-1.2.16.dist-info → klaude_code-1.2.17.dist-info}/RECORD +28 -25
  27. klaude_code/core/prompts/prompt-sub-agent-webfetch.md +0 -46
  28. klaude_code/protocol/sub_agent/web_fetch.py +0 -74
  29. {klaude_code-1.2.16.dist-info → klaude_code-1.2.17.dist-info}/WHEEL +0 -0
  30. {klaude_code-1.2.16.dist-info → klaude_code-1.2.17.dist-info}/entry_points.txt +0 -0
@@ -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
- active_session_id = await initialize_session(components.executor, components.event_queue, session_id=session_id)
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=active_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
@@ -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)
@@ -79,8 +79,8 @@ class Config(BaseModel):
79
79
 
80
80
  def get_example_config() -> Config:
81
81
  return Config(
82
- main_model="gpt-5.1",
83
- sub_agent_models={"explore": "haiku", "oracle": "gpt-5.1-high"},
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="medium",
110
+ reasoning_effort="high",
107
111
  reasoning_summary="auto",
108
- type="enabled",
109
- budget_tokens=None,
110
112
  ),
111
- context_limit=368000,
113
+ context_limit=400000,
112
114
  ),
113
115
  ),
114
116
  ModelConfig(
115
- model_name="gpt-5.1-high",
116
- provider="openai",
117
+ model_name="opus",
118
+ provider="anthropic",
117
119
  model_params=llm_param.LLMConfigModelParameter(
118
- model="gpt-5.1-2025-11-13",
119
- max_tokens=32000,
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=None,
124
+ budget_tokens=31999,
126
125
  ),
127
- context_limit=368000,
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=168000,
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
- This will end the task and return the structured data to the caller.
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
- return type("ReportBackTool", (ReportBackTool,), {"_schema": schema})
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
+ )
@@ -11,6 +11,7 @@ class CommandName(str, Enum):
11
11
  CLEAR = "clear"
12
12
  TERMINAL_SETUP = "terminal-setup"
13
13
  EXPORT = "export"
14
+ EXPORT_ONLINE = "export-online"
14
15
  STATUS = "status"
15
16
  RELEASE_NOTES = "release-notes"
16
17
  THINKING = "thinking"
@@ -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 web_fetch as web_fetch # noqa: E402
117
+ from klaude_code.protocol.sub_agent import web as web # noqa: E402
@@ -37,7 +37,7 @@ EXPLORE_PARAMETERS = {
37
37
  "description": "Optional JSON Schema for sub-agent structured output",
38
38
  },
39
39
  },
40
- "required": ["description", "prompt", "output_format"],
40
+ "required": ["description", "prompt"],
41
41
  "additionalProperties": False,
42
42
  }
43
43