klaude-code 2.1.1__py3-none-any.whl → 2.3.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/__init__.py +1 -2
- klaude_code/app/runtime.py +13 -41
- klaude_code/cli/list_model.py +27 -10
- klaude_code/cli/main.py +42 -159
- klaude_code/config/assets/builtin_config.yaml +36 -14
- klaude_code/config/config.py +144 -7
- klaude_code/config/select_model.py +38 -13
- klaude_code/config/sub_agent_model_helper.py +217 -0
- klaude_code/const.py +2 -2
- klaude_code/core/agent_profile.py +71 -5
- klaude_code/core/executor.py +75 -0
- klaude_code/core/manager/llm_clients_builder.py +18 -12
- klaude_code/core/prompts/prompt-nano-banana.md +1 -0
- klaude_code/core/tool/shell/command_safety.py +4 -189
- klaude_code/core/tool/sub_agent_tool.py +2 -1
- klaude_code/core/turn.py +1 -1
- klaude_code/llm/anthropic/client.py +8 -5
- klaude_code/llm/anthropic/input.py +54 -29
- klaude_code/llm/google/client.py +2 -2
- klaude_code/llm/google/input.py +23 -2
- klaude_code/llm/openai_compatible/input.py +22 -13
- klaude_code/llm/openai_compatible/stream.py +1 -1
- klaude_code/llm/openrouter/input.py +37 -25
- klaude_code/llm/responses/client.py +1 -1
- klaude_code/llm/responses/input.py +96 -57
- klaude_code/protocol/commands.py +1 -2
- klaude_code/protocol/events/system.py +4 -0
- klaude_code/protocol/message.py +2 -2
- klaude_code/protocol/op.py +17 -0
- klaude_code/protocol/op_handler.py +5 -0
- klaude_code/protocol/sub_agent/AGENTS.md +28 -0
- klaude_code/protocol/sub_agent/__init__.py +10 -14
- klaude_code/protocol/sub_agent/image_gen.py +2 -1
- klaude_code/session/codec.py +2 -6
- klaude_code/session/session.py +9 -1
- klaude_code/skill/assets/create-plan/SKILL.md +3 -5
- klaude_code/tui/command/__init__.py +7 -10
- klaude_code/tui/command/clear_cmd.py +1 -1
- klaude_code/tui/command/command_abc.py +1 -2
- klaude_code/tui/command/copy_cmd.py +1 -2
- klaude_code/tui/command/fork_session_cmd.py +4 -4
- klaude_code/tui/command/model_cmd.py +6 -43
- klaude_code/tui/command/model_select.py +75 -15
- klaude_code/tui/command/refresh_cmd.py +1 -2
- klaude_code/tui/command/resume_cmd.py +3 -4
- klaude_code/tui/command/status_cmd.py +1 -1
- klaude_code/tui/command/sub_agent_model_cmd.py +190 -0
- klaude_code/tui/components/bash_syntax.py +1 -1
- klaude_code/tui/components/common.py +1 -1
- klaude_code/tui/components/developer.py +10 -15
- klaude_code/tui/components/metadata.py +2 -64
- klaude_code/tui/components/rich/cjk_wrap.py +3 -2
- klaude_code/tui/components/rich/status.py +49 -3
- klaude_code/tui/components/rich/theme.py +4 -2
- klaude_code/tui/components/sub_agent.py +25 -46
- klaude_code/tui/components/user_input.py +9 -21
- klaude_code/tui/components/welcome.py +99 -0
- klaude_code/tui/input/prompt_toolkit.py +14 -1
- klaude_code/tui/renderer.py +2 -3
- klaude_code/tui/runner.py +2 -2
- klaude_code/tui/terminal/selector.py +8 -18
- klaude_code/ui/__init__.py +0 -24
- klaude_code/ui/common.py +3 -2
- klaude_code/ui/core/display.py +2 -2
- {klaude_code-2.1.1.dist-info → klaude_code-2.3.0.dist-info}/METADATA +16 -81
- {klaude_code-2.1.1.dist-info → klaude_code-2.3.0.dist-info}/RECORD +68 -67
- klaude_code/tui/command/help_cmd.py +0 -51
- klaude_code/tui/command/prompt-commit.md +0 -82
- klaude_code/tui/command/release_notes_cmd.py +0 -85
- klaude_code/ui/exec_mode.py +0 -60
- {klaude_code-2.1.1.dist-info → klaude_code-2.3.0.dist-info}/WHEEL +0 -0
- {klaude_code-2.1.1.dist-info → klaude_code-2.3.0.dist-info}/entry_points.txt +0 -0
klaude_code/core/executor.py
CHANGED
|
@@ -15,6 +15,7 @@ from dataclasses import dataclass
|
|
|
15
15
|
from pathlib import Path
|
|
16
16
|
|
|
17
17
|
from klaude_code.config import load_config
|
|
18
|
+
from klaude_code.config.sub_agent_model_helper import SubAgentModelHelper
|
|
18
19
|
from klaude_code.core.agent import Agent
|
|
19
20
|
from klaude_code.core.agent_profile import DefaultModelProfileProvider, ModelProfileProvider
|
|
20
21
|
from klaude_code.core.manager import LLMClients, SubAgentManager
|
|
@@ -109,6 +110,15 @@ class AgentRuntime:
|
|
|
109
110
|
def current_agent(self) -> Agent | None:
|
|
110
111
|
return self._agent
|
|
111
112
|
|
|
113
|
+
def _get_sub_agent_models(self) -> dict[str, LLMConfigParameter]:
|
|
114
|
+
"""Build a dict of sub-agent type to LLMConfigParameter."""
|
|
115
|
+
enabled = self._model_profile_provider.enabled_sub_agent_types()
|
|
116
|
+
return {
|
|
117
|
+
sub_agent_type: client.get_llm_config()
|
|
118
|
+
for sub_agent_type, client in self._llm_clients.sub_clients.items()
|
|
119
|
+
if sub_agent_type in enabled
|
|
120
|
+
}
|
|
121
|
+
|
|
112
122
|
async def ensure_agent(self, session_id: str | None = None) -> Agent:
|
|
113
123
|
"""Return the active agent, creating or loading a session as needed."""
|
|
114
124
|
|
|
@@ -135,6 +145,7 @@ class AgentRuntime:
|
|
|
135
145
|
session_id=session.id,
|
|
136
146
|
work_dir=str(session.work_dir),
|
|
137
147
|
llm_config=self._llm_clients.main.get_llm_config(),
|
|
148
|
+
sub_agent_models=self._get_sub_agent_models(),
|
|
138
149
|
)
|
|
139
150
|
)
|
|
140
151
|
|
|
@@ -200,6 +211,7 @@ class AgentRuntime:
|
|
|
200
211
|
session_id=agent.session.id,
|
|
201
212
|
work_dir=str(agent.session.work_dir),
|
|
202
213
|
llm_config=self._llm_clients.main.get_llm_config(),
|
|
214
|
+
sub_agent_models=self._get_sub_agent_models(),
|
|
203
215
|
)
|
|
204
216
|
)
|
|
205
217
|
|
|
@@ -223,6 +235,7 @@ class AgentRuntime:
|
|
|
223
235
|
session_id=target_session.id,
|
|
224
236
|
work_dir=str(target_session.work_dir),
|
|
225
237
|
llm_config=self._llm_clients.main.get_llm_config(),
|
|
238
|
+
sub_agent_models=self._get_sub_agent_models(),
|
|
226
239
|
)
|
|
227
240
|
)
|
|
228
241
|
|
|
@@ -406,6 +419,15 @@ class ExecutorContext:
|
|
|
406
419
|
"""Emit an event to the UI display system."""
|
|
407
420
|
await self.event_queue.put(event)
|
|
408
421
|
|
|
422
|
+
def _get_sub_agent_models(self) -> dict[str, LLMConfigParameter]:
|
|
423
|
+
"""Build a dict of sub-agent type to LLMConfigParameter."""
|
|
424
|
+
enabled = self.model_profile_provider.enabled_sub_agent_types()
|
|
425
|
+
return {
|
|
426
|
+
sub_agent_type: client.get_llm_config()
|
|
427
|
+
for sub_agent_type, client in self.llm_clients.sub_clients.items()
|
|
428
|
+
if sub_agent_type in enabled
|
|
429
|
+
}
|
|
430
|
+
|
|
409
431
|
def current_session_id(self) -> str | None:
|
|
410
432
|
"""Return the primary active session id, if any.
|
|
411
433
|
|
|
@@ -455,6 +477,7 @@ class ExecutorContext:
|
|
|
455
477
|
llm_config=llm_config,
|
|
456
478
|
work_dir=str(agent.session.work_dir),
|
|
457
479
|
show_klaude_code_info=False,
|
|
480
|
+
show_sub_agent_models=False,
|
|
458
481
|
)
|
|
459
482
|
)
|
|
460
483
|
|
|
@@ -501,9 +524,61 @@ class ExecutorContext:
|
|
|
501
524
|
work_dir=str(agent.session.work_dir),
|
|
502
525
|
llm_config=agent.profile.llm_client.get_llm_config(),
|
|
503
526
|
show_klaude_code_info=False,
|
|
527
|
+
show_sub_agent_models=False,
|
|
504
528
|
)
|
|
505
529
|
)
|
|
506
530
|
|
|
531
|
+
async def handle_change_sub_agent_model(self, operation: op.ChangeSubAgentModelOperation) -> None:
|
|
532
|
+
"""Handle a change sub-agent model operation."""
|
|
533
|
+
agent = await self._agent_runtime.ensure_agent(operation.session_id)
|
|
534
|
+
config = load_config()
|
|
535
|
+
|
|
536
|
+
helper = SubAgentModelHelper(config)
|
|
537
|
+
|
|
538
|
+
sub_agent_type = operation.sub_agent_type
|
|
539
|
+
model_name = operation.model_name
|
|
540
|
+
|
|
541
|
+
if model_name is None:
|
|
542
|
+
# Clear explicit override and revert to sub-agent default behavior.
|
|
543
|
+
behavior = helper.describe_empty_model_config_behavior(
|
|
544
|
+
sub_agent_type,
|
|
545
|
+
main_model_name=self.llm_clients.main.model_name,
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
resolved = helper.resolve_default_model_override(sub_agent_type)
|
|
549
|
+
if resolved is None:
|
|
550
|
+
# Default: inherit from main client.
|
|
551
|
+
self.llm_clients.sub_clients.pop(sub_agent_type, None)
|
|
552
|
+
else:
|
|
553
|
+
# Default: use a dedicated model (e.g. first available image model).
|
|
554
|
+
llm_config = config.get_model_config(resolved)
|
|
555
|
+
new_client = create_llm_client(llm_config)
|
|
556
|
+
self.llm_clients.sub_clients[sub_agent_type] = new_client
|
|
557
|
+
|
|
558
|
+
display_model = f"({behavior.description})"
|
|
559
|
+
else:
|
|
560
|
+
# Create new client for the sub-agent
|
|
561
|
+
llm_config = config.get_model_config(model_name)
|
|
562
|
+
new_client = create_llm_client(llm_config)
|
|
563
|
+
self.llm_clients.sub_clients[sub_agent_type] = new_client
|
|
564
|
+
display_model = new_client.model_name
|
|
565
|
+
|
|
566
|
+
if operation.save_as_default:
|
|
567
|
+
if model_name is None:
|
|
568
|
+
# Remove from config to inherit
|
|
569
|
+
config.sub_agent_models.pop(sub_agent_type, None)
|
|
570
|
+
else:
|
|
571
|
+
config.sub_agent_models[sub_agent_type] = model_name
|
|
572
|
+
await config.save()
|
|
573
|
+
|
|
574
|
+
saved_note = " (saved in ~/.klaude/klaude-config.yaml)" if operation.save_as_default else ""
|
|
575
|
+
developer_item = message.DeveloperMessage(
|
|
576
|
+
parts=message.text_parts_from_str(f"{sub_agent_type} model: {display_model}{saved_note}"),
|
|
577
|
+
ui_extra=model.build_command_output_extra(commands.CommandName.SUB_AGENT_MODEL),
|
|
578
|
+
)
|
|
579
|
+
agent.session.append_history([developer_item])
|
|
580
|
+
await self.emit_event(events.DeveloperMessageEvent(session_id=agent.session.id, item=developer_item))
|
|
581
|
+
|
|
507
582
|
async def handle_clear_session(self, operation: op.ClearSessionOperation) -> None:
|
|
508
583
|
await self._agent_runtime.clear_session(operation.session_id)
|
|
509
584
|
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from klaude_code.config import Config
|
|
6
|
+
from klaude_code.config.sub_agent_model_helper import SubAgentModelHelper
|
|
6
7
|
from klaude_code.core.manager.llm_clients import LLMClients
|
|
7
8
|
from klaude_code.llm.client import LLMClientABC
|
|
8
9
|
from klaude_code.llm.registry import create_llm_client
|
|
9
10
|
from klaude_code.log import DebugType, log_debug
|
|
10
|
-
from klaude_code.protocol.sub_agent import iter_sub_agent_profiles
|
|
11
11
|
from klaude_code.protocol.tools import SubAgentType
|
|
12
12
|
|
|
13
13
|
|
|
@@ -15,13 +15,20 @@ def build_llm_clients(
|
|
|
15
15
|
config: Config,
|
|
16
16
|
*,
|
|
17
17
|
model_override: str | None = None,
|
|
18
|
+
skip_sub_agents: bool = False,
|
|
18
19
|
) -> LLMClients:
|
|
19
|
-
"""Create an ``LLMClients`` bundle driven by application config.
|
|
20
|
+
"""Create an ``LLMClients`` bundle driven by application config.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
config: Application configuration.
|
|
24
|
+
model_override: Override for the main model name.
|
|
25
|
+
skip_sub_agents: If True, skip initializing sub-agent clients (e.g., for vanilla/banana modes).
|
|
26
|
+
"""
|
|
20
27
|
|
|
21
28
|
# Resolve main agent LLM config
|
|
22
29
|
model_name = model_override or config.main_model
|
|
23
30
|
if model_name is None:
|
|
24
|
-
raise ValueError("No model specified.
|
|
31
|
+
raise ValueError("No model specified. Set main_model in the config or pass --model.")
|
|
25
32
|
llm_config = config.get_model_config(model_name)
|
|
26
33
|
|
|
27
34
|
log_debug(
|
|
@@ -32,17 +39,16 @@ def build_llm_clients(
|
|
|
32
39
|
)
|
|
33
40
|
|
|
34
41
|
main_client = create_llm_client(llm_config)
|
|
35
|
-
sub_clients: dict[SubAgentType, LLMClientABC] = {}
|
|
36
42
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if not model_name:
|
|
40
|
-
continue
|
|
43
|
+
if skip_sub_agents:
|
|
44
|
+
return LLMClients(main=main_client)
|
|
41
45
|
|
|
42
|
-
|
|
43
|
-
|
|
46
|
+
helper = SubAgentModelHelper(config)
|
|
47
|
+
sub_agent_configs = helper.build_sub_agent_client_configs()
|
|
44
48
|
|
|
45
|
-
|
|
46
|
-
|
|
49
|
+
sub_clients: dict[SubAgentType, LLMClientABC] = {}
|
|
50
|
+
for sub_agent_type, sub_model_name in sub_agent_configs.items():
|
|
51
|
+
sub_llm_config = config.get_model_config(sub_model_name)
|
|
52
|
+
sub_clients[sub_agent_type] = create_llm_client(sub_llm_config)
|
|
47
53
|
|
|
48
54
|
return LLMClients(main=main_client, sub_clients=sub_clients)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
You're a helpful assistant with capabilities to generate images and edit images.
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import os
|
|
2
|
-
import re
|
|
3
2
|
import shlex
|
|
4
3
|
|
|
5
4
|
|
|
@@ -11,76 +10,6 @@ class SafetyCheckResult:
|
|
|
11
10
|
self.error_msg = error_msg
|
|
12
11
|
|
|
13
12
|
|
|
14
|
-
def _is_valid_sed_n_arg(s: str | None) -> bool:
|
|
15
|
-
if not s:
|
|
16
|
-
return False
|
|
17
|
-
# Matches: Np or M,Np where M,N are positive integers
|
|
18
|
-
return bool(re.fullmatch(r"\d+(,\d+)?p", s))
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def _is_safe_awk_program(program: str) -> SafetyCheckResult:
|
|
22
|
-
lowered = program.lower()
|
|
23
|
-
|
|
24
|
-
if "`" in program:
|
|
25
|
-
return SafetyCheckResult(False, "awk: backticks not allowed in program")
|
|
26
|
-
if "$(" in program:
|
|
27
|
-
return SafetyCheckResult(False, "awk: command substitution not allowed in program")
|
|
28
|
-
if "|&" in program:
|
|
29
|
-
return SafetyCheckResult(False, "awk: background pipeline not allowed in program")
|
|
30
|
-
|
|
31
|
-
if "system(" in lowered:
|
|
32
|
-
return SafetyCheckResult(False, "awk: system() call not allowed in program")
|
|
33
|
-
|
|
34
|
-
if re.search(r"(?<![|&>])\bprint\s*\|", program, re.IGNORECASE):
|
|
35
|
-
return SafetyCheckResult(False, "awk: piping output to external command not allowed")
|
|
36
|
-
if re.search(r"\bprintf\s*\|", program, re.IGNORECASE):
|
|
37
|
-
return SafetyCheckResult(False, "awk: piping output to external command not allowed")
|
|
38
|
-
|
|
39
|
-
return SafetyCheckResult(True)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def _is_safe_awk_argv(argv: list[str]) -> SafetyCheckResult:
|
|
43
|
-
if len(argv) < 2:
|
|
44
|
-
return SafetyCheckResult(False, "awk: Missing program")
|
|
45
|
-
|
|
46
|
-
program: str | None = None
|
|
47
|
-
|
|
48
|
-
i = 1
|
|
49
|
-
while i < len(argv):
|
|
50
|
-
arg = argv[i]
|
|
51
|
-
|
|
52
|
-
if arg in {"-f", "--file", "--source"} or arg.startswith("-f"):
|
|
53
|
-
return SafetyCheckResult(False, "awk: -f/--file not allowed")
|
|
54
|
-
|
|
55
|
-
if arg in {"-e", "--exec"}:
|
|
56
|
-
if i + 1 >= len(argv):
|
|
57
|
-
return SafetyCheckResult(False, "awk: Missing program for -e")
|
|
58
|
-
script = argv[i + 1]
|
|
59
|
-
program_check = _is_safe_awk_program(script)
|
|
60
|
-
if not program_check.is_safe:
|
|
61
|
-
return program_check
|
|
62
|
-
if program is None:
|
|
63
|
-
program = script
|
|
64
|
-
i += 2
|
|
65
|
-
continue
|
|
66
|
-
|
|
67
|
-
if arg.startswith("-"):
|
|
68
|
-
i += 1
|
|
69
|
-
continue
|
|
70
|
-
|
|
71
|
-
if program is None:
|
|
72
|
-
program_check = _is_safe_awk_program(arg)
|
|
73
|
-
if not program_check.is_safe:
|
|
74
|
-
return program_check
|
|
75
|
-
program = arg
|
|
76
|
-
i += 1
|
|
77
|
-
|
|
78
|
-
if program is None:
|
|
79
|
-
return SafetyCheckResult(False, "awk: Missing program")
|
|
80
|
-
|
|
81
|
-
return SafetyCheckResult(True)
|
|
82
|
-
|
|
83
|
-
|
|
84
13
|
def _is_safe_rm_argv(argv: list[str]) -> SafetyCheckResult:
|
|
85
14
|
"""Check safety of rm command arguments."""
|
|
86
15
|
# Enforce strict safety rules for rm operands
|
|
@@ -217,112 +146,12 @@ def _is_safe_argv(argv: list[str]) -> SafetyCheckResult:
|
|
|
217
146
|
|
|
218
147
|
cmd0 = argv[0]
|
|
219
148
|
|
|
220
|
-
# if _has_shell_redirection(argv):
|
|
221
|
-
# return SafetyCheckResult(False, "Shell redirection and pipelines are not allowed in single commands")
|
|
222
|
-
|
|
223
|
-
# Special handling for rm to prevent dangerous operations
|
|
224
149
|
if cmd0 == "rm":
|
|
225
150
|
return _is_safe_rm_argv(argv)
|
|
226
151
|
|
|
227
|
-
# Special handling for trash to prevent dangerous operations
|
|
228
152
|
if cmd0 == "trash":
|
|
229
153
|
return _is_safe_trash_argv(argv)
|
|
230
154
|
|
|
231
|
-
if cmd0 == "find":
|
|
232
|
-
unsafe_opts = {
|
|
233
|
-
"-exec": "command execution",
|
|
234
|
-
"-execdir": "command execution",
|
|
235
|
-
"-ok": "interactive command execution",
|
|
236
|
-
"-okdir": "interactive command execution",
|
|
237
|
-
"-delete": "file deletion",
|
|
238
|
-
"-fls": "file output",
|
|
239
|
-
"-fprint": "file output",
|
|
240
|
-
"-fprint0": "file output",
|
|
241
|
-
"-fprintf": "formatted file output",
|
|
242
|
-
}
|
|
243
|
-
for arg in argv[1:]:
|
|
244
|
-
if arg in unsafe_opts:
|
|
245
|
-
return SafetyCheckResult(False, f"find: {unsafe_opts[arg]} option '{arg}' not allowed")
|
|
246
|
-
return SafetyCheckResult(True)
|
|
247
|
-
|
|
248
|
-
if cmd0 == "git":
|
|
249
|
-
sub = argv[1] if len(argv) > 1 else None
|
|
250
|
-
if not sub:
|
|
251
|
-
return SafetyCheckResult(False, "git: Missing subcommand")
|
|
252
|
-
|
|
253
|
-
# Allow most local git operations, but block remote operations
|
|
254
|
-
allowed_git_cmds = {
|
|
255
|
-
"add",
|
|
256
|
-
"branch",
|
|
257
|
-
"checkout",
|
|
258
|
-
"commit",
|
|
259
|
-
"config",
|
|
260
|
-
"diff",
|
|
261
|
-
"fetch",
|
|
262
|
-
"init",
|
|
263
|
-
"log",
|
|
264
|
-
"merge",
|
|
265
|
-
"mv",
|
|
266
|
-
"rebase",
|
|
267
|
-
"reset",
|
|
268
|
-
"restore",
|
|
269
|
-
"revert",
|
|
270
|
-
"rm",
|
|
271
|
-
"show",
|
|
272
|
-
"stash",
|
|
273
|
-
"status",
|
|
274
|
-
"switch",
|
|
275
|
-
"tag",
|
|
276
|
-
"clone",
|
|
277
|
-
"worktree",
|
|
278
|
-
"push",
|
|
279
|
-
"pull",
|
|
280
|
-
"remote",
|
|
281
|
-
}
|
|
282
|
-
if sub not in allowed_git_cmds:
|
|
283
|
-
return SafetyCheckResult(False, f"git: Subcommand '{sub}' not in allow list")
|
|
284
|
-
return SafetyCheckResult(True)
|
|
285
|
-
|
|
286
|
-
# Build tools and linters - allow all subcommands
|
|
287
|
-
if cmd0 in {
|
|
288
|
-
"cargo",
|
|
289
|
-
"uv",
|
|
290
|
-
"go",
|
|
291
|
-
"ruff",
|
|
292
|
-
"pyright",
|
|
293
|
-
"make",
|
|
294
|
-
"npm",
|
|
295
|
-
"pnpm",
|
|
296
|
-
"bun",
|
|
297
|
-
}:
|
|
298
|
-
return SafetyCheckResult(True)
|
|
299
|
-
|
|
300
|
-
if cmd0 == "sed":
|
|
301
|
-
# Allow sed -n patterns (line printing)
|
|
302
|
-
if len(argv) >= 3 and argv[1] == "-n" and _is_valid_sed_n_arg(argv[2]):
|
|
303
|
-
return SafetyCheckResult(True)
|
|
304
|
-
# Allow simple text replacement: sed 's/old/new/g' file
|
|
305
|
-
# or sed -i 's/old/new/g' file for in-place editing
|
|
306
|
-
if len(argv) >= 3:
|
|
307
|
-
# Find the sed script argument (usually starts with 's/')
|
|
308
|
-
for arg in argv[1:]:
|
|
309
|
-
if arg.startswith("s/") or arg.startswith("s|"):
|
|
310
|
-
# Basic safety check: no command execution in replacement
|
|
311
|
-
if ";" in arg:
|
|
312
|
-
return SafetyCheckResult(False, f"sed: Command separator ';' not allowed in '{arg}'")
|
|
313
|
-
if "`" in arg:
|
|
314
|
-
return SafetyCheckResult(False, f"sed: Backticks not allowed in '{arg}'")
|
|
315
|
-
if "$(" in arg:
|
|
316
|
-
return SafetyCheckResult(False, f"sed: Command substitution not allowed in '{arg}'")
|
|
317
|
-
return SafetyCheckResult(True)
|
|
318
|
-
return SafetyCheckResult(
|
|
319
|
-
False,
|
|
320
|
-
"sed: Only text replacement (s/old/new/) or line printing (-n 'Np') is allowed",
|
|
321
|
-
)
|
|
322
|
-
|
|
323
|
-
if cmd0 == "awk":
|
|
324
|
-
return _is_safe_awk_argv(argv)
|
|
325
|
-
|
|
326
155
|
# Default allow when command is not explicitly restricted
|
|
327
156
|
return SafetyCheckResult(True)
|
|
328
157
|
|
|
@@ -330,30 +159,16 @@ def _is_safe_argv(argv: list[str]) -> SafetyCheckResult:
|
|
|
330
159
|
def is_safe_command(command: str) -> SafetyCheckResult:
|
|
331
160
|
"""Determine if a command is safe enough to run.
|
|
332
161
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
find -exec/-delete, etc.) and otherwise lets the real shell surface
|
|
336
|
-
syntax errors (for example, unmatched quotes in complex multiline
|
|
337
|
-
scripts).
|
|
162
|
+
Only rm and trash commands are checked for safety. All other commands
|
|
163
|
+
are allowed by default.
|
|
338
164
|
"""
|
|
339
|
-
|
|
340
|
-
# Try to parse into an argv-style list first. If this fails (e.g. due
|
|
341
|
-
# to unterminated quotes in a complex heredoc), treat the command as
|
|
342
|
-
# safe here and let bash itself perform syntax checking instead of
|
|
343
|
-
# blocking execution pre-emptively.
|
|
344
165
|
try:
|
|
345
166
|
argv = shlex.split(command, posix=True)
|
|
346
167
|
except ValueError:
|
|
347
|
-
# If we cannot reliably parse the command
|
|
348
|
-
#
|
|
349
|
-
# real shell surface any syntax errors instead of blocking execution
|
|
350
|
-
# pre-emptively.
|
|
168
|
+
# If we cannot reliably parse the command, treat it as safe here
|
|
169
|
+
# and let the real shell surface any syntax errors
|
|
351
170
|
return SafetyCheckResult(True)
|
|
352
171
|
|
|
353
|
-
# All further safety checks are done directly on the parsed argv via
|
|
354
|
-
# _is_safe_argv. We intentionally avoid trying to re-interpret complex
|
|
355
|
-
# shell sequences here and rely on the real shell to handle syntax.
|
|
356
|
-
|
|
357
172
|
if not argv:
|
|
358
173
|
return SafetyCheckResult(False, "Empty command")
|
|
359
174
|
|
|
@@ -31,11 +31,12 @@ class SubAgentTool(ToolABC):
|
|
|
31
31
|
@classmethod
|
|
32
32
|
def for_profile(cls, profile: SubAgentProfile) -> type[SubAgentTool]:
|
|
33
33
|
"""Create a tool class for a specific sub-agent profile."""
|
|
34
|
-
|
|
34
|
+
tool_cls = type(
|
|
35
35
|
f"{profile.name}Tool",
|
|
36
36
|
(SubAgentTool,),
|
|
37
37
|
{"_profile": profile},
|
|
38
38
|
)
|
|
39
|
+
return cast(type[SubAgentTool], tool_cls)
|
|
39
40
|
|
|
40
41
|
@classmethod
|
|
41
42
|
def metadata(cls) -> ToolMetadata:
|
klaude_code/core/turn.py
CHANGED
|
@@ -348,7 +348,7 @@ class TurnExecutor:
|
|
|
348
348
|
style="red",
|
|
349
349
|
debug_type=DebugType.RESPONSE,
|
|
350
350
|
)
|
|
351
|
-
case message.
|
|
351
|
+
case message.ToolCallStartDelta() as msg:
|
|
352
352
|
if thinking_active:
|
|
353
353
|
thinking_active = False
|
|
354
354
|
yield events.ThinkingEndEvent(
|
|
@@ -16,6 +16,7 @@ from anthropic.types.beta.beta_raw_message_start_event import BetaRawMessageStar
|
|
|
16
16
|
from anthropic.types.beta.beta_signature_delta import BetaSignatureDelta
|
|
17
17
|
from anthropic.types.beta.beta_text_delta import BetaTextDelta
|
|
18
18
|
from anthropic.types.beta.beta_thinking_delta import BetaThinkingDelta
|
|
19
|
+
from anthropic.types.beta.beta_tool_choice_auto_param import BetaToolChoiceAutoParam
|
|
19
20
|
from anthropic.types.beta.beta_tool_use_block import BetaToolUseBlock
|
|
20
21
|
from anthropic.types.beta.message_create_params import MessageCreateParamsStreaming
|
|
21
22
|
|
|
@@ -82,12 +83,14 @@ def build_payload(
|
|
|
82
83
|
# Prepend extra betas, avoiding duplicates
|
|
83
84
|
betas = [b for b in extra_betas if b not in betas] + betas
|
|
84
85
|
|
|
86
|
+
tool_choice: BetaToolChoiceAutoParam = {
|
|
87
|
+
"type": "auto",
|
|
88
|
+
"disable_parallel_tool_use": False,
|
|
89
|
+
}
|
|
90
|
+
|
|
85
91
|
payload: MessageCreateParamsStreaming = {
|
|
86
92
|
"model": str(param.model),
|
|
87
|
-
"tool_choice":
|
|
88
|
-
"type": "auto",
|
|
89
|
-
"disable_parallel_tool_use": False,
|
|
90
|
-
},
|
|
93
|
+
"tool_choice": tool_choice,
|
|
91
94
|
"stream": True,
|
|
92
95
|
"max_tokens": param.max_tokens or DEFAULT_MAX_TOKENS,
|
|
93
96
|
"temperature": param.temperature or DEFAULT_TEMPERATURE,
|
|
@@ -169,7 +172,7 @@ async def parse_anthropic_stream(
|
|
|
169
172
|
match event.content_block:
|
|
170
173
|
case BetaToolUseBlock() as block:
|
|
171
174
|
metadata_tracker.record_token()
|
|
172
|
-
yield message.
|
|
175
|
+
yield message.ToolCallStartDelta(
|
|
173
176
|
response_id=response_id,
|
|
174
177
|
call_id=block.id,
|
|
175
178
|
name=block.name,
|
|
@@ -8,10 +8,13 @@ import json
|
|
|
8
8
|
from typing import Literal, cast
|
|
9
9
|
|
|
10
10
|
from anthropic.types.beta.beta_base64_image_source_param import BetaBase64ImageSourceParam
|
|
11
|
+
from anthropic.types.beta.beta_content_block_param import BetaContentBlockParam
|
|
11
12
|
from anthropic.types.beta.beta_image_block_param import BetaImageBlockParam
|
|
12
13
|
from anthropic.types.beta.beta_message_param import BetaMessageParam
|
|
13
14
|
from anthropic.types.beta.beta_text_block_param import BetaTextBlockParam
|
|
14
15
|
from anthropic.types.beta.beta_tool_param import BetaToolParam
|
|
16
|
+
from anthropic.types.beta.beta_tool_result_block_param import BetaToolResultBlockParam
|
|
17
|
+
from anthropic.types.beta.beta_tool_use_block_param import BetaToolUseBlockParam
|
|
15
18
|
from anthropic.types.beta.beta_url_image_source_param import BetaURLImageSourceParam
|
|
16
19
|
|
|
17
20
|
from klaude_code.const import EMPTY_TOOL_OUTPUT_MESSAGE
|
|
@@ -60,29 +63,29 @@ def _user_message_to_message(
|
|
|
60
63
|
blocks: list[BetaTextBlockParam | BetaImageBlockParam] = []
|
|
61
64
|
for part in msg.parts:
|
|
62
65
|
if isinstance(part, message.TextPart):
|
|
63
|
-
blocks.append({"type": "text", "text": part.text})
|
|
66
|
+
blocks.append(cast(BetaTextBlockParam, {"type": "text", "text": part.text}))
|
|
64
67
|
elif isinstance(part, message.ImageURLPart):
|
|
65
68
|
blocks.append(_image_part_to_block(part))
|
|
66
69
|
if attachment.text:
|
|
67
|
-
blocks.append({"type": "text", "text": attachment.text})
|
|
70
|
+
blocks.append(cast(BetaTextBlockParam, {"type": "text", "text": attachment.text}))
|
|
68
71
|
for image in attachment.images:
|
|
69
72
|
blocks.append(_image_part_to_block(image))
|
|
70
73
|
if not blocks:
|
|
71
|
-
blocks.append({"type": "text", "text": ""})
|
|
74
|
+
blocks.append(cast(BetaTextBlockParam, {"type": "text", "text": ""}))
|
|
72
75
|
return {"role": "user", "content": blocks}
|
|
73
76
|
|
|
74
77
|
|
|
75
78
|
def _tool_message_to_block(
|
|
76
79
|
msg: message.ToolResultMessage,
|
|
77
80
|
attachment: DeveloperAttachment,
|
|
78
|
-
) ->
|
|
81
|
+
) -> BetaToolResultBlockParam:
|
|
79
82
|
"""Convert a single tool result message to a tool_result block."""
|
|
80
83
|
tool_content: list[BetaTextBlockParam | BetaImageBlockParam] = []
|
|
81
84
|
merged_text = merge_reminder_text(
|
|
82
85
|
msg.output_text or EMPTY_TOOL_OUTPUT_MESSAGE,
|
|
83
86
|
attachment.text,
|
|
84
87
|
)
|
|
85
|
-
tool_content.append({"type": "text", "text": merged_text})
|
|
88
|
+
tool_content.append(cast(BetaTextBlockParam, {"type": "text", "text": merged_text}))
|
|
86
89
|
for image in [part for part in msg.parts if isinstance(part, message.ImageURLPart)]:
|
|
87
90
|
tool_content.append(_image_part_to_block(image))
|
|
88
91
|
for image in attachment.images:
|
|
@@ -95,7 +98,7 @@ def _tool_message_to_block(
|
|
|
95
98
|
}
|
|
96
99
|
|
|
97
100
|
|
|
98
|
-
def _tool_blocks_to_message(blocks: list[
|
|
101
|
+
def _tool_blocks_to_message(blocks: list[BetaToolResultBlockParam]) -> BetaMessageParam:
|
|
99
102
|
"""Convert one or more tool_result blocks to a single user message."""
|
|
100
103
|
return {
|
|
101
104
|
"role": "user",
|
|
@@ -104,7 +107,7 @@ def _tool_blocks_to_message(blocks: list[dict[str, object]]) -> BetaMessageParam
|
|
|
104
107
|
|
|
105
108
|
|
|
106
109
|
def _assistant_message_to_message(msg: message.AssistantMessage, model_name: str | None) -> BetaMessageParam:
|
|
107
|
-
content: list[
|
|
110
|
+
content: list[BetaContentBlockParam] = []
|
|
108
111
|
current_thinking_content: str | None = None
|
|
109
112
|
native_thinking_parts, degraded_thinking_texts = split_thinking_parts(msg, model_name)
|
|
110
113
|
native_thinking_ids = {id(part) for part in native_thinking_parts}
|
|
@@ -113,7 +116,7 @@ def _assistant_message_to_message(msg: message.AssistantMessage, model_name: str
|
|
|
113
116
|
nonlocal current_thinking_content
|
|
114
117
|
if current_thinking_content is None:
|
|
115
118
|
return
|
|
116
|
-
|
|
119
|
+
degraded_thinking_texts.append(current_thinking_content)
|
|
117
120
|
current_thinking_content = None
|
|
118
121
|
|
|
119
122
|
for part in msg.parts:
|
|
@@ -127,33 +130,47 @@ def _assistant_message_to_message(msg: message.AssistantMessage, model_name: str
|
|
|
127
130
|
continue
|
|
128
131
|
if part.signature:
|
|
129
132
|
content.append(
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
133
|
+
cast(
|
|
134
|
+
BetaContentBlockParam,
|
|
135
|
+
{
|
|
136
|
+
"type": "thinking",
|
|
137
|
+
"thinking": current_thinking_content or "",
|
|
138
|
+
"signature": part.signature,
|
|
139
|
+
},
|
|
140
|
+
)
|
|
135
141
|
)
|
|
136
142
|
current_thinking_content = None
|
|
137
143
|
continue
|
|
138
144
|
|
|
139
145
|
_flush_thinking()
|
|
140
146
|
if isinstance(part, message.TextPart):
|
|
141
|
-
content.append({"type": "text", "text": part.text})
|
|
147
|
+
content.append(cast(BetaTextBlockParam, {"type": "text", "text": part.text}))
|
|
142
148
|
elif isinstance(part, message.ToolCallPart):
|
|
149
|
+
tool_input: dict[str, object] = {}
|
|
150
|
+
if part.arguments_json:
|
|
151
|
+
try:
|
|
152
|
+
parsed = json.loads(part.arguments_json)
|
|
153
|
+
except json.JSONDecodeError:
|
|
154
|
+
parsed = {"_raw": part.arguments_json}
|
|
155
|
+
tool_input = cast(dict[str, object], parsed) if isinstance(parsed, dict) else {"_value": parsed}
|
|
156
|
+
|
|
143
157
|
content.append(
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
158
|
+
cast(
|
|
159
|
+
BetaToolUseBlockParam,
|
|
160
|
+
{
|
|
161
|
+
"type": "tool_use",
|
|
162
|
+
"id": part.call_id,
|
|
163
|
+
"name": part.tool_name,
|
|
164
|
+
"input": tool_input,
|
|
165
|
+
},
|
|
166
|
+
)
|
|
150
167
|
)
|
|
151
168
|
|
|
152
169
|
_flush_thinking()
|
|
153
170
|
|
|
154
171
|
if degraded_thinking_texts:
|
|
155
172
|
degraded_text = "<thinking>\n" + "\n".join(degraded_thinking_texts) + "\n</thinking>"
|
|
156
|
-
content.insert(0, {"type": "text", "text": degraded_text})
|
|
173
|
+
content.insert(0, cast(BetaTextBlockParam, {"type": "text", "text": degraded_text}))
|
|
157
174
|
|
|
158
175
|
return {"role": "assistant", "content": content}
|
|
159
176
|
|
|
@@ -174,7 +191,7 @@ def convert_history_to_input(
|
|
|
174
191
|
) -> list[BetaMessageParam]:
|
|
175
192
|
"""Convert a list of messages to beta message params."""
|
|
176
193
|
messages: list[BetaMessageParam] = []
|
|
177
|
-
pending_tool_blocks: list[
|
|
194
|
+
pending_tool_blocks: list[BetaToolResultBlockParam] = []
|
|
178
195
|
|
|
179
196
|
def flush_tool_blocks() -> None:
|
|
180
197
|
nonlocal pending_tool_blocks
|
|
@@ -213,7 +230,12 @@ def convert_system_to_input(
|
|
|
213
230
|
parts.append("\n".join(part.text for part in msg.parts))
|
|
214
231
|
if not parts:
|
|
215
232
|
return []
|
|
216
|
-
|
|
233
|
+
block: BetaTextBlockParam = {
|
|
234
|
+
"type": "text",
|
|
235
|
+
"text": "\n".join(parts),
|
|
236
|
+
"cache_control": {"type": "ephemeral"},
|
|
237
|
+
}
|
|
238
|
+
return [block]
|
|
217
239
|
|
|
218
240
|
|
|
219
241
|
def convert_tool_schema(
|
|
@@ -222,11 +244,14 @@ def convert_tool_schema(
|
|
|
222
244
|
if tools is None:
|
|
223
245
|
return []
|
|
224
246
|
return [
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
247
|
+
cast(
|
|
248
|
+
BetaToolParam,
|
|
249
|
+
{
|
|
250
|
+
"input_schema": tool.parameters,
|
|
251
|
+
"type": "custom",
|
|
252
|
+
"name": tool.name,
|
|
253
|
+
"description": tool.description,
|
|
254
|
+
},
|
|
255
|
+
)
|
|
231
256
|
for tool in tools
|
|
232
257
|
]
|