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.
Files changed (167) hide show
  1. klaude_code/__init__.py +0 -0
  2. klaude_code/cli/__init__.py +1 -0
  3. klaude_code/cli/main.py +298 -0
  4. klaude_code/cli/runtime.py +331 -0
  5. klaude_code/cli/session_cmd.py +80 -0
  6. klaude_code/command/__init__.py +43 -0
  7. klaude_code/command/clear_cmd.py +20 -0
  8. klaude_code/command/command_abc.py +92 -0
  9. klaude_code/command/diff_cmd.py +138 -0
  10. klaude_code/command/export_cmd.py +86 -0
  11. klaude_code/command/help_cmd.py +51 -0
  12. klaude_code/command/model_cmd.py +43 -0
  13. klaude_code/command/prompt-dev-docs-update.md +56 -0
  14. klaude_code/command/prompt-dev-docs.md +46 -0
  15. klaude_code/command/prompt-init.md +45 -0
  16. klaude_code/command/prompt_command.py +69 -0
  17. klaude_code/command/refresh_cmd.py +43 -0
  18. klaude_code/command/registry.py +110 -0
  19. klaude_code/command/status_cmd.py +111 -0
  20. klaude_code/command/terminal_setup_cmd.py +252 -0
  21. klaude_code/config/__init__.py +11 -0
  22. klaude_code/config/config.py +177 -0
  23. klaude_code/config/list_model.py +162 -0
  24. klaude_code/config/select_model.py +67 -0
  25. klaude_code/const/__init__.py +133 -0
  26. klaude_code/core/__init__.py +0 -0
  27. klaude_code/core/agent.py +165 -0
  28. klaude_code/core/executor.py +485 -0
  29. klaude_code/core/manager/__init__.py +19 -0
  30. klaude_code/core/manager/agent_manager.py +127 -0
  31. klaude_code/core/manager/llm_clients.py +42 -0
  32. klaude_code/core/manager/llm_clients_builder.py +49 -0
  33. klaude_code/core/manager/sub_agent_manager.py +86 -0
  34. klaude_code/core/prompt.py +89 -0
  35. klaude_code/core/prompts/prompt-claude-code.md +98 -0
  36. klaude_code/core/prompts/prompt-codex.md +331 -0
  37. klaude_code/core/prompts/prompt-gemini.md +43 -0
  38. klaude_code/core/prompts/prompt-subagent-explore.md +27 -0
  39. klaude_code/core/prompts/prompt-subagent-oracle.md +23 -0
  40. klaude_code/core/prompts/prompt-subagent-webfetch.md +46 -0
  41. klaude_code/core/prompts/prompt-subagent.md +8 -0
  42. klaude_code/core/reminders.py +445 -0
  43. klaude_code/core/task.py +237 -0
  44. klaude_code/core/tool/__init__.py +75 -0
  45. klaude_code/core/tool/file/__init__.py +0 -0
  46. klaude_code/core/tool/file/apply_patch.py +492 -0
  47. klaude_code/core/tool/file/apply_patch_tool.md +1 -0
  48. klaude_code/core/tool/file/apply_patch_tool.py +204 -0
  49. klaude_code/core/tool/file/edit_tool.md +9 -0
  50. klaude_code/core/tool/file/edit_tool.py +274 -0
  51. klaude_code/core/tool/file/multi_edit_tool.md +42 -0
  52. klaude_code/core/tool/file/multi_edit_tool.py +199 -0
  53. klaude_code/core/tool/file/read_tool.md +14 -0
  54. klaude_code/core/tool/file/read_tool.py +326 -0
  55. klaude_code/core/tool/file/write_tool.md +8 -0
  56. klaude_code/core/tool/file/write_tool.py +146 -0
  57. klaude_code/core/tool/memory/__init__.py +0 -0
  58. klaude_code/core/tool/memory/memory_tool.md +16 -0
  59. klaude_code/core/tool/memory/memory_tool.py +462 -0
  60. klaude_code/core/tool/memory/skill_loader.py +245 -0
  61. klaude_code/core/tool/memory/skill_tool.md +24 -0
  62. klaude_code/core/tool/memory/skill_tool.py +97 -0
  63. klaude_code/core/tool/shell/__init__.py +0 -0
  64. klaude_code/core/tool/shell/bash_tool.md +43 -0
  65. klaude_code/core/tool/shell/bash_tool.py +123 -0
  66. klaude_code/core/tool/shell/command_safety.py +363 -0
  67. klaude_code/core/tool/sub_agent_tool.py +83 -0
  68. klaude_code/core/tool/todo/__init__.py +0 -0
  69. klaude_code/core/tool/todo/todo_write_tool.md +182 -0
  70. klaude_code/core/tool/todo/todo_write_tool.py +121 -0
  71. klaude_code/core/tool/todo/update_plan_tool.md +3 -0
  72. klaude_code/core/tool/todo/update_plan_tool.py +104 -0
  73. klaude_code/core/tool/tool_abc.py +25 -0
  74. klaude_code/core/tool/tool_context.py +106 -0
  75. klaude_code/core/tool/tool_registry.py +78 -0
  76. klaude_code/core/tool/tool_runner.py +252 -0
  77. klaude_code/core/tool/truncation.py +170 -0
  78. klaude_code/core/tool/web/__init__.py +0 -0
  79. klaude_code/core/tool/web/mermaid_tool.md +21 -0
  80. klaude_code/core/tool/web/mermaid_tool.py +76 -0
  81. klaude_code/core/tool/web/web_fetch_tool.md +8 -0
  82. klaude_code/core/tool/web/web_fetch_tool.py +159 -0
  83. klaude_code/core/turn.py +220 -0
  84. klaude_code/llm/__init__.py +21 -0
  85. klaude_code/llm/anthropic/__init__.py +3 -0
  86. klaude_code/llm/anthropic/client.py +221 -0
  87. klaude_code/llm/anthropic/input.py +200 -0
  88. klaude_code/llm/client.py +49 -0
  89. klaude_code/llm/input_common.py +239 -0
  90. klaude_code/llm/openai_compatible/__init__.py +3 -0
  91. klaude_code/llm/openai_compatible/client.py +211 -0
  92. klaude_code/llm/openai_compatible/input.py +109 -0
  93. klaude_code/llm/openai_compatible/tool_call_accumulator.py +80 -0
  94. klaude_code/llm/openrouter/__init__.py +3 -0
  95. klaude_code/llm/openrouter/client.py +200 -0
  96. klaude_code/llm/openrouter/input.py +160 -0
  97. klaude_code/llm/openrouter/reasoning_handler.py +209 -0
  98. klaude_code/llm/registry.py +22 -0
  99. klaude_code/llm/responses/__init__.py +3 -0
  100. klaude_code/llm/responses/client.py +216 -0
  101. klaude_code/llm/responses/input.py +167 -0
  102. klaude_code/llm/usage.py +109 -0
  103. klaude_code/protocol/__init__.py +4 -0
  104. klaude_code/protocol/commands.py +21 -0
  105. klaude_code/protocol/events.py +163 -0
  106. klaude_code/protocol/llm_param.py +147 -0
  107. klaude_code/protocol/model.py +287 -0
  108. klaude_code/protocol/op.py +89 -0
  109. klaude_code/protocol/op_handler.py +28 -0
  110. klaude_code/protocol/sub_agent.py +348 -0
  111. klaude_code/protocol/tools.py +15 -0
  112. klaude_code/session/__init__.py +4 -0
  113. klaude_code/session/export.py +624 -0
  114. klaude_code/session/selector.py +76 -0
  115. klaude_code/session/session.py +474 -0
  116. klaude_code/session/templates/export_session.html +1434 -0
  117. klaude_code/trace/__init__.py +3 -0
  118. klaude_code/trace/log.py +168 -0
  119. klaude_code/ui/__init__.py +91 -0
  120. klaude_code/ui/core/__init__.py +1 -0
  121. klaude_code/ui/core/display.py +103 -0
  122. klaude_code/ui/core/input.py +71 -0
  123. klaude_code/ui/core/stage_manager.py +55 -0
  124. klaude_code/ui/modes/__init__.py +1 -0
  125. klaude_code/ui/modes/debug/__init__.py +1 -0
  126. klaude_code/ui/modes/debug/display.py +36 -0
  127. klaude_code/ui/modes/exec/__init__.py +1 -0
  128. klaude_code/ui/modes/exec/display.py +63 -0
  129. klaude_code/ui/modes/repl/__init__.py +51 -0
  130. klaude_code/ui/modes/repl/clipboard.py +152 -0
  131. klaude_code/ui/modes/repl/completers.py +429 -0
  132. klaude_code/ui/modes/repl/display.py +60 -0
  133. klaude_code/ui/modes/repl/event_handler.py +375 -0
  134. klaude_code/ui/modes/repl/input_prompt_toolkit.py +198 -0
  135. klaude_code/ui/modes/repl/key_bindings.py +170 -0
  136. klaude_code/ui/modes/repl/renderer.py +281 -0
  137. klaude_code/ui/renderers/__init__.py +0 -0
  138. klaude_code/ui/renderers/assistant.py +21 -0
  139. klaude_code/ui/renderers/common.py +8 -0
  140. klaude_code/ui/renderers/developer.py +158 -0
  141. klaude_code/ui/renderers/diffs.py +215 -0
  142. klaude_code/ui/renderers/errors.py +16 -0
  143. klaude_code/ui/renderers/metadata.py +190 -0
  144. klaude_code/ui/renderers/sub_agent.py +71 -0
  145. klaude_code/ui/renderers/thinking.py +39 -0
  146. klaude_code/ui/renderers/tools.py +551 -0
  147. klaude_code/ui/renderers/user_input.py +65 -0
  148. klaude_code/ui/rich/__init__.py +1 -0
  149. klaude_code/ui/rich/live.py +65 -0
  150. klaude_code/ui/rich/markdown.py +308 -0
  151. klaude_code/ui/rich/quote.py +34 -0
  152. klaude_code/ui/rich/searchable_text.py +71 -0
  153. klaude_code/ui/rich/status.py +240 -0
  154. klaude_code/ui/rich/theme.py +274 -0
  155. klaude_code/ui/terminal/__init__.py +1 -0
  156. klaude_code/ui/terminal/color.py +244 -0
  157. klaude_code/ui/terminal/control.py +147 -0
  158. klaude_code/ui/terminal/notifier.py +107 -0
  159. klaude_code/ui/terminal/progress_bar.py +87 -0
  160. klaude_code/ui/utils/__init__.py +1 -0
  161. klaude_code/ui/utils/common.py +108 -0
  162. klaude_code/ui/utils/debouncer.py +42 -0
  163. klaude_code/version.py +163 -0
  164. klaude_code-1.2.6.dist-info/METADATA +178 -0
  165. klaude_code-1.2.6.dist-info/RECORD +167 -0
  166. klaude_code-1.2.6.dist-info/WHEEL +4 -0
  167. klaude_code-1.2.6.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,121 @@
1
+ from pathlib import Path
2
+
3
+ from pydantic import BaseModel
4
+
5
+ from klaude_code.core.tool.tool_abc import ToolABC, load_desc
6
+ from klaude_code.core.tool.tool_context import get_current_todo_context
7
+ from klaude_code.core.tool.tool_registry import register
8
+ from klaude_code.protocol import llm_param, model, tools
9
+
10
+
11
+ def get_new_completed_todos(old_todos: list[model.TodoItem], new_todos: list[model.TodoItem]) -> list[str]:
12
+ """
13
+ Compare old and new todo lists to find newly completed todos.
14
+
15
+ Args:
16
+ old_todos: Previous todo list from session
17
+ new_todos: New todo list being set
18
+
19
+ Returns:
20
+ List of TodoItem content that were just completed (status changed to 'completed')
21
+ """
22
+ # Create a mapping of old todos by content for quick lookup
23
+ old_todos_map = {todo.content: todo for todo in old_todos}
24
+
25
+ new_completed: list[str] = []
26
+ for new_todo in new_todos:
27
+ # Check if this todo exists in the old list
28
+ old_todo = old_todos_map.get(new_todo.content)
29
+ if new_todo.status != "completed":
30
+ continue
31
+ if old_todo is not None:
32
+ # Todo existed before, check if status changed to completed
33
+ if old_todo.status != "completed":
34
+ new_completed.append(new_todo.content)
35
+ else:
36
+ # New completed todo
37
+ new_completed.append(new_todo.content)
38
+ return new_completed
39
+
40
+
41
+ class TodoWriteArguments(BaseModel):
42
+ todos: list[model.TodoItem]
43
+
44
+
45
+ @register(tools.TODO_WRITE)
46
+ class TodoWriteTool(ToolABC):
47
+ @classmethod
48
+ def schema(cls) -> llm_param.ToolSchema:
49
+ return llm_param.ToolSchema(
50
+ name=tools.TODO_WRITE,
51
+ type="function",
52
+ description=load_desc(Path(__file__).parent / "todo_write_tool.md"),
53
+ parameters={
54
+ "type": "object",
55
+ "properties": {
56
+ "todos": {
57
+ "type": "array",
58
+ "items": {
59
+ "type": "object",
60
+ "properties": {
61
+ "content": {"type": "string", "minLength": 1},
62
+ "status": {
63
+ "type": "string",
64
+ "enum": ["pending", "in_progress", "completed"],
65
+ },
66
+ "activeForm": {"type": "string", "minLength": 1},
67
+ },
68
+ "required": ["content", "status", "activeForm"],
69
+ "additionalProperties": False,
70
+ },
71
+ "description": "The updated todo list",
72
+ }
73
+ },
74
+ "required": ["todos"],
75
+ "additionalProperties": False,
76
+ },
77
+ )
78
+
79
+ @classmethod
80
+ async def call(cls, arguments: str) -> model.ToolResultItem:
81
+ try:
82
+ args = TodoWriteArguments.model_validate_json(arguments)
83
+ except ValueError as e:
84
+ return model.ToolResultItem(
85
+ status="error",
86
+ output=f"Invalid arguments: {e}",
87
+ )
88
+
89
+ # Get current todo context to store todos
90
+ todo_context = get_current_todo_context()
91
+ if todo_context is None:
92
+ return model.ToolResultItem(
93
+ status="error",
94
+ output="No active session found",
95
+ )
96
+
97
+ # Get current todos before updating (for comparison)
98
+ old_todos = todo_context.get_todos()
99
+
100
+ # Find newly completed todos
101
+ new_completed = get_new_completed_todos(old_todos, args.todos)
102
+
103
+ # Store todos via todo context
104
+ todo_context.set_todos(args.todos)
105
+
106
+ ui_extra = model.TodoUIExtra(todos=args.todos, new_completed=new_completed)
107
+
108
+ response = f"""Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable
109
+
110
+ <system-reminder>
111
+ Your todo list has changed. DO NOT mention this explicitly to the user. Here are the latest contents of your todo list:
112
+
113
+ {model.todo_list_str(args.todos)}. Continue on with the tasks at hand if applicable.
114
+ </system-reminder>"""
115
+
116
+ return model.ToolResultItem(
117
+ status="success",
118
+ output=response,
119
+ ui_extra=model.ToolResultUIExtra(type=model.ToolResultUIExtraType.TODO_LIST, todo_list=ui_extra),
120
+ side_effects=[model.ToolSideEffect.TODO_CHANGE],
121
+ )
@@ -0,0 +1,3 @@
1
+ Updates the task plan.
2
+ Provide an optional explanation and a list of plan items, each with a step and status.
3
+ At most one step can be in_progress at a time.
@@ -0,0 +1,104 @@
1
+ """
2
+ update_plan tool: Codex variant of todo_write tool
3
+ """
4
+
5
+ from pathlib import Path
6
+
7
+ from pydantic import BaseModel, field_validator
8
+
9
+ from klaude_code.core.tool.tool_abc import ToolABC, load_desc
10
+ from klaude_code.core.tool.tool_context import get_current_todo_context
11
+ from klaude_code.core.tool.tool_registry import register
12
+ from klaude_code.protocol import llm_param, model, tools
13
+
14
+ from .todo_write_tool import get_new_completed_todos
15
+
16
+
17
+ class PlanItemArguments(BaseModel):
18
+ step: str
19
+ status: model.TodoStatusType
20
+
21
+ @field_validator("step")
22
+ @classmethod
23
+ def validate_step(cls, value: str) -> str:
24
+ if not value.strip():
25
+ raise ValueError("step must be a non-empty string")
26
+ return value
27
+
28
+
29
+ class UpdatePlanArguments(BaseModel):
30
+ plan: list[PlanItemArguments]
31
+ explanation: str | None = None
32
+
33
+ @field_validator("plan")
34
+ @classmethod
35
+ def validate_plan(cls, value: list[PlanItemArguments]) -> list[PlanItemArguments]:
36
+ if not value:
37
+ raise ValueError("plan must contain at least one item")
38
+ in_progress_count = sum(1 for item in value if item.status == "in_progress")
39
+ if in_progress_count > 1:
40
+ raise ValueError("plan can have at most one in_progress step")
41
+ return value
42
+
43
+
44
+ @register(tools.UPDATE_PLAN)
45
+ class UpdatePlanTool(ToolABC):
46
+ @classmethod
47
+ def schema(cls) -> llm_param.ToolSchema:
48
+ return llm_param.ToolSchema(
49
+ name=tools.UPDATE_PLAN,
50
+ type="function",
51
+ description=load_desc(Path(__file__).parent / "update_plan_tool.md"),
52
+ parameters={
53
+ "type": "object",
54
+ "properties": {
55
+ "explanation": {
56
+ "type": "string",
57
+ "description": "Optional explanation for the current plan state.",
58
+ },
59
+ "plan": {
60
+ "type": "array",
61
+ "description": "The list of steps",
62
+ "items": {
63
+ "type": "object",
64
+ "properties": {
65
+ "step": {"type": "string", "minLength": 1},
66
+ "status": {
67
+ "type": "string",
68
+ "enum": ["pending", "in_progress", "completed"],
69
+ },
70
+ },
71
+ "required": ["step", "status"],
72
+ "additionalProperties": False,
73
+ },
74
+ },
75
+ },
76
+ "required": ["plan"],
77
+ "additionalProperties": False,
78
+ },
79
+ )
80
+
81
+ @classmethod
82
+ async def call(cls, arguments: str) -> model.ToolResultItem:
83
+ try:
84
+ args = UpdatePlanArguments.model_validate_json(arguments)
85
+ except ValueError as exc:
86
+ return model.ToolResultItem(status="error", output=f"Invalid arguments: {exc}")
87
+
88
+ todo_context = get_current_todo_context()
89
+ if todo_context is None:
90
+ return model.ToolResultItem(status="error", output="No active session found")
91
+
92
+ new_todos = [model.TodoItem(content=item.step, status=item.status) for item in args.plan]
93
+ old_todos = todo_context.get_todos()
94
+ new_completed = get_new_completed_todos(old_todos, new_todos)
95
+ todo_context.set_todos(new_todos)
96
+
97
+ ui_extra = model.TodoUIExtra(todos=new_todos, new_completed=new_completed)
98
+
99
+ return model.ToolResultItem(
100
+ status="success",
101
+ output="Plan updated",
102
+ ui_extra=model.ToolResultUIExtra(type=model.ToolResultUIExtraType.TODO_LIST, todo_list=ui_extra),
103
+ side_effects=[model.ToolSideEffect.TODO_CHANGE],
104
+ )
@@ -0,0 +1,25 @@
1
+ import string
2
+ from abc import ABC, abstractmethod
3
+ from pathlib import Path
4
+
5
+ from klaude_code.protocol import llm_param, model
6
+
7
+
8
+ def load_desc(path: Path, substitutions: dict[str, str] | None = None) -> str:
9
+ """Load a tool description from a file, with optional substitutions."""
10
+ description = path.read_text(encoding="utf-8")
11
+ if substitutions:
12
+ description = string.Template(description).substitute(substitutions)
13
+ return description
14
+
15
+
16
+ class ToolABC(ABC):
17
+ @classmethod
18
+ @abstractmethod
19
+ def schema(cls) -> llm_param.ToolSchema:
20
+ raise NotImplementedError
21
+
22
+ @classmethod
23
+ @abstractmethod
24
+ async def call(cls, arguments: str) -> model.ToolResultItem:
25
+ raise NotImplementedError
@@ -0,0 +1,106 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Awaitable, Callable, Generator, MutableMapping
4
+ from contextlib import contextmanager
5
+ from contextvars import ContextVar, Token
6
+ from dataclasses import dataclass
7
+
8
+ from klaude_code.protocol import model
9
+ from klaude_code.protocol.sub_agent import SubAgentResult
10
+ from klaude_code.session.session import Session
11
+
12
+
13
+ @dataclass
14
+ class TodoContext:
15
+ """Todo access interface exposed to tools.
16
+
17
+ Tools can only read the current todo list and replace it with
18
+ a new list; they cannot access the full Session object.
19
+ """
20
+
21
+ get_todos: Callable[[], list[model.TodoItem]]
22
+ set_todos: Callable[[list[model.TodoItem]], None]
23
+
24
+
25
+ @dataclass
26
+ class ToolContextToken:
27
+ """Tokens used to restore tool execution context.
28
+
29
+ This captures the contextvar tokens for the current file tracker
30
+ and todo context so callers can safely reset them after a tool
31
+ finishes running.
32
+ """
33
+
34
+ file_tracker_token: Token[MutableMapping[str, float] | None] | None
35
+ todo_token: Token[TodoContext | None] | None
36
+
37
+
38
+ # Holds the current file tracker mapping for tool execution context.
39
+ # Set by Agent/Reminder right before invoking a tool.
40
+ current_file_tracker_var: ContextVar[MutableMapping[str, float] | None] = ContextVar(
41
+ "current_file_tracker", default=None
42
+ )
43
+
44
+
45
+ # Holds the todo access context for tools.
46
+ current_todo_context_var: ContextVar[TodoContext | None] = ContextVar("current_todo_context", default=None)
47
+
48
+
49
+ def set_tool_context_from_session(session: Session) -> ToolContextToken:
50
+ """Bind the given session's file tracker and todos into tool context.
51
+
52
+ This should be called by the Agent or reminder helpers immediately
53
+ before invoking tools so that file and todo tools can operate on
54
+ the correct per-session state without seeing the full Session.
55
+ """
56
+
57
+ file_tracker_token = current_file_tracker_var.set(session.file_tracker)
58
+ todo_ctx = TodoContext(
59
+ get_todos=lambda: session.todos,
60
+ set_todos=lambda todos: setattr(session, "todos", todos),
61
+ )
62
+ todo_token = current_todo_context_var.set(todo_ctx)
63
+ return ToolContextToken(file_tracker_token=file_tracker_token, todo_token=todo_token)
64
+
65
+
66
+ def reset_tool_context(token: ToolContextToken) -> None:
67
+ """Restore tool execution context from a previously captured token."""
68
+
69
+ if token.file_tracker_token is not None:
70
+ current_file_tracker_var.reset(token.file_tracker_token)
71
+ if token.todo_token is not None:
72
+ current_todo_context_var.reset(token.todo_token)
73
+
74
+
75
+ @contextmanager
76
+ def tool_context(
77
+ file_tracker: MutableMapping[str, float], todo_ctx: TodoContext
78
+ ) -> Generator[ToolContextToken, None, None]:
79
+ """Context manager for setting and resetting tool execution context."""
80
+
81
+ file_tracker_token = current_file_tracker_var.set(file_tracker)
82
+ todo_token = current_todo_context_var.set(todo_ctx)
83
+ token = ToolContextToken(file_tracker_token=file_tracker_token, todo_token=todo_token)
84
+ try:
85
+ yield token
86
+ finally:
87
+ reset_tool_context(token)
88
+
89
+
90
+ def get_current_file_tracker() -> MutableMapping[str, float] | None:
91
+ """Return the current file tracker mapping for this tool context."""
92
+
93
+ return current_file_tracker_var.get()
94
+
95
+
96
+ def get_current_todo_context() -> TodoContext | None:
97
+ """Return the current todo access context for this tool context."""
98
+
99
+ return current_todo_context_var.get()
100
+
101
+
102
+ # Holds a handle to run a nested subtask (sub-agent) from within a tool call.
103
+ # The callable takes a model.SubAgentState and returns a SubAgentResult.
104
+ current_run_subtask_callback: ContextVar[Callable[[model.SubAgentState], Awaitable[SubAgentResult]] | None] = (
105
+ ContextVar("current_run_subtask_callback", default=None)
106
+ )
@@ -0,0 +1,78 @@
1
+ from typing import Callable, TypeVar
2
+
3
+ from klaude_code.core.tool.sub_agent_tool import SubAgentTool
4
+ from klaude_code.core.tool.tool_abc import ToolABC
5
+ from klaude_code.protocol import llm_param, tools
6
+ from klaude_code.protocol.sub_agent import get_sub_agent_profile, iter_sub_agent_profiles, sub_agent_tool_names
7
+
8
+ _REGISTRY: dict[str, type[ToolABC]] = {}
9
+
10
+ T = TypeVar("T", bound=ToolABC)
11
+
12
+
13
+ def register(name: str) -> Callable[[type[T]], type[T]]:
14
+ def _decorator(cls: type[T]) -> type[T]:
15
+ _REGISTRY[name] = cls
16
+ return cls
17
+
18
+ return _decorator
19
+
20
+
21
+ def _register_sub_agent_tools() -> None:
22
+ """Automatically register all sub-agent tools based on their profiles."""
23
+ for profile in iter_sub_agent_profiles():
24
+ tool_cls = SubAgentTool.for_profile(profile)
25
+ _REGISTRY[profile.name] = tool_cls
26
+
27
+
28
+ _register_sub_agent_tools()
29
+
30
+
31
+ def list_tools() -> list[str]:
32
+ return list(_REGISTRY.keys())
33
+
34
+
35
+ def get_tool_schemas(tool_names: list[str]) -> list[llm_param.ToolSchema]:
36
+ schemas: list[llm_param.ToolSchema] = []
37
+ for tool_name in tool_names:
38
+ if tool_name not in _REGISTRY:
39
+ raise ValueError(f"Unknown Tool: {tool_name}")
40
+ schemas.append(_REGISTRY[tool_name].schema())
41
+ return schemas
42
+
43
+
44
+ def get_registry() -> dict[str, type[ToolABC]]:
45
+ """Get the global tool registry."""
46
+ return _REGISTRY
47
+
48
+
49
+ def load_agent_tools(
50
+ model_name: str, sub_agent_type: tools.SubAgentType | None = None, *, vanilla: bool = False
51
+ ) -> list[llm_param.ToolSchema]:
52
+ """Get tools for an agent based on model and agent type.
53
+
54
+ Args:
55
+ model_name: The model name.
56
+ sub_agent_type: If None, returns main agent tools. Otherwise returns sub-agent tools.
57
+ vanilla: If True, returns minimal vanilla tools (ignores sub_agent_type).
58
+ """
59
+ if vanilla:
60
+ return get_tool_schemas([tools.BASH, tools.EDIT, tools.WRITE, tools.READ])
61
+
62
+ if sub_agent_type is not None:
63
+ profile = get_sub_agent_profile(sub_agent_type)
64
+ if not profile.enabled_for_model(model_name):
65
+ return []
66
+ return get_tool_schemas(list(profile.tool_set))
67
+
68
+ # Main agent tools
69
+ if "gpt-5" in model_name:
70
+ tool_names = [tools.BASH, tools.READ, tools.APPLY_PATCH, tools.UPDATE_PLAN]
71
+ elif "gemini-3" in model_name:
72
+ tool_names = [tools.BASH, tools.READ, tools.EDIT, tools.WRITE]
73
+ else:
74
+ tool_names = [tools.BASH, tools.READ, tools.EDIT, tools.WRITE, tools.TODO_WRITE]
75
+
76
+ tool_names.extend(sub_agent_tool_names(enabled_only=True, model_name=model_name))
77
+ tool_names.extend([tools.SKILL, tools.MERMAID, tools.MEMORY])
78
+ return get_tool_schemas(tool_names)
@@ -0,0 +1,252 @@
1
+ import asyncio
2
+ from collections.abc import AsyncGenerator, Callable, Iterable, Sequence
3
+ from dataclasses import dataclass
4
+
5
+ from klaude_code import const
6
+ from klaude_code.core.tool.tool_abc import ToolABC
7
+ from klaude_code.core.tool.truncation import truncate_tool_output
8
+ from klaude_code.protocol import model
9
+ from klaude_code.protocol.sub_agent import is_sub_agent_tool
10
+
11
+
12
+ async def run_tool(tool_call: model.ToolCallItem, registry: dict[str, type[ToolABC]]) -> model.ToolResultItem:
13
+ """Execute a tool call and return the result.
14
+
15
+ Args:
16
+ tool_call: The tool call to execute.
17
+ registry: The tool registry mapping tool names to tool classes.
18
+
19
+ Returns:
20
+ The result of the tool execution.
21
+ """
22
+ if tool_call.name not in registry:
23
+ return model.ToolResultItem(
24
+ call_id=tool_call.call_id,
25
+ output=f"Tool {tool_call.name} not exists",
26
+ status="error",
27
+ tool_name=tool_call.name,
28
+ )
29
+ try:
30
+ tool_result = await registry[tool_call.name].call(tool_call.arguments)
31
+ tool_result.call_id = tool_call.call_id
32
+ tool_result.tool_name = tool_call.name
33
+ if tool_result.output:
34
+ truncation_result = truncate_tool_output(tool_result.output, tool_call)
35
+ tool_result.output = truncation_result.output
36
+ if truncation_result.was_truncated and truncation_result.saved_file_path:
37
+ tool_result.ui_extra = model.ToolResultUIExtra(
38
+ type=model.ToolResultUIExtraType.TRUNCATION,
39
+ truncation=model.TruncationUIExtra(
40
+ saved_file_path=truncation_result.saved_file_path,
41
+ original_length=truncation_result.original_length,
42
+ truncated_length=truncation_result.truncated_length,
43
+ ),
44
+ )
45
+ return tool_result
46
+ except asyncio.CancelledError:
47
+ # Propagate cooperative cancellation so outer layers can handle interrupts correctly.
48
+ raise
49
+ except Exception as e:
50
+ return model.ToolResultItem(
51
+ call_id=tool_call.call_id,
52
+ output=f"Tool {tool_call.name} execution error: {e.__class__.__name__} {e}",
53
+ status="error",
54
+ tool_name=tool_call.name,
55
+ )
56
+
57
+
58
+ @dataclass
59
+ class ToolExecutionCallStarted:
60
+ """Represents the start of a tool call execution."""
61
+
62
+ tool_call: model.ToolCallItem
63
+
64
+
65
+ @dataclass
66
+ class ToolExecutionResult:
67
+ """Represents the completion of a tool call with its result."""
68
+
69
+ tool_call: model.ToolCallItem
70
+ tool_result: model.ToolResultItem
71
+
72
+
73
+ @dataclass
74
+ class ToolExecutionTodoChange:
75
+ """Represents a todo list change side effect emitted by a tool."""
76
+
77
+ todos: list[model.TodoItem]
78
+
79
+
80
+ ToolExecutorEvent = ToolExecutionCallStarted | ToolExecutionResult | ToolExecutionTodoChange
81
+
82
+
83
+ class ToolExecutor:
84
+ """Execute and coordinate a batch of tool calls for a single turn.
85
+
86
+ The executor is responsible for:
87
+ - Partitioning tool calls into regular tools and sub-agent tools
88
+ - Running regular tools sequentially and sub-agent tools concurrently
89
+ - Emitting ToolCall/ToolResult events and tool side-effect events
90
+ - Tracking unfinished calls so `cancel()` can synthesize cancellation results
91
+ """
92
+
93
+ def __init__(
94
+ self,
95
+ *,
96
+ registry: dict[str, type[ToolABC]],
97
+ append_history: Callable[[Sequence[model.ConversationItem]], None],
98
+ ) -> None:
99
+ self._registry = registry
100
+ self._append_history = append_history
101
+
102
+ self._unfinished_calls: dict[str, model.ToolCallItem] = {}
103
+ self._call_event_emitted: set[str] = set()
104
+ self._sub_agent_tasks: set[asyncio.Task[list[ToolExecutorEvent]]] = set()
105
+
106
+ async def run_tools(self, tool_calls: list[model.ToolCallItem]) -> AsyncGenerator[ToolExecutorEvent, None]:
107
+ """Run the given tool calls and yield execution events.
108
+
109
+ Tool calls are partitioned into regular tools and sub-agent tools. Regular tools
110
+ run sequentially, while sub-agent tools run concurrently. All results are
111
+ appended to history via the injected `append_history` callback.
112
+ """
113
+
114
+ for tool_call in tool_calls:
115
+ self._unfinished_calls[tool_call.call_id] = tool_call
116
+
117
+ regular_tool_calls, sub_agent_tool_calls = self._partition_tool_calls(tool_calls)
118
+
119
+ # Run regular tools sequentially.
120
+ for tool_call in regular_tool_calls:
121
+ tool_call_event = self._build_tool_call_started(tool_call)
122
+ self._call_event_emitted.add(tool_call.call_id)
123
+ yield tool_call_event
124
+
125
+ try:
126
+ result_events = await self._run_single_tool_call(tool_call)
127
+ except asyncio.CancelledError:
128
+ # Propagate cooperative cancellation so the agent task can be stopped.
129
+ raise
130
+
131
+ for exec_event in result_events:
132
+ yield exec_event
133
+
134
+ # Run sub-agent tools concurrently.
135
+ if sub_agent_tool_calls:
136
+ execution_tasks: list[asyncio.Task[list[ToolExecutorEvent]]] = []
137
+ for tool_call in sub_agent_tool_calls:
138
+ tool_call_event = self._build_tool_call_started(tool_call)
139
+ self._call_event_emitted.add(tool_call.call_id)
140
+ yield tool_call_event
141
+
142
+ task = asyncio.create_task(self._run_single_tool_call(tool_call))
143
+ self._register_sub_agent_task(task)
144
+ execution_tasks.append(task)
145
+
146
+ for task in asyncio.as_completed(execution_tasks):
147
+ # Do not swallow asyncio.CancelledError here:
148
+ # - If the user interrupts the main agent, the executor cancels the
149
+ # outer agent task, which should propagate cancellation up through
150
+ # tool execution so the task can terminate and emit TaskFinishEvent.
151
+ # - Sub-agent tool tasks cancelled via ToolExecutor.cancel() are
152
+ # handled by synthesizing ToolExecutionResult events; any
153
+ # CancelledError raised here should still bubble up so the
154
+ # calling agent can stop cleanly, matching pre-refactor behavior.
155
+ result_events = await task
156
+
157
+ for exec_event in result_events:
158
+ yield exec_event
159
+
160
+ def cancel(self) -> Iterable[ToolExecutorEvent]:
161
+ """Cancel unfinished tool calls and synthesize error results.
162
+
163
+ - Cancels any running sub-agent tool tasks so they stop emitting events.
164
+ - For each unfinished tool call, yields a ToolExecutionCallStarted (if not
165
+ already emitted for this turn) followed by a ToolExecutionResult with
166
+ error status and a standard cancellation output. The corresponding
167
+ ToolResultItem is appended to history via `append_history`.
168
+ """
169
+
170
+ events_to_yield: list[ToolExecutorEvent] = []
171
+
172
+ # Cancel running sub-agent tool tasks.
173
+ for task in list(self._sub_agent_tasks):
174
+ if not task.done():
175
+ task.cancel()
176
+ self._sub_agent_tasks.clear()
177
+
178
+ if not self._unfinished_calls:
179
+ return events_to_yield
180
+
181
+ for call_id, tool_call in list(self._unfinished_calls.items()):
182
+ cancel_result = model.ToolResultItem(
183
+ call_id=tool_call.call_id,
184
+ output=const.CANCEL_OUTPUT,
185
+ status="error",
186
+ tool_name=tool_call.name,
187
+ ui_extra=None,
188
+ )
189
+
190
+ if call_id not in self._call_event_emitted:
191
+ events_to_yield.append(ToolExecutionCallStarted(tool_call=tool_call))
192
+ self._call_event_emitted.add(call_id)
193
+
194
+ events_to_yield.append(ToolExecutionResult(tool_call=tool_call, tool_result=cancel_result))
195
+
196
+ self._append_history([cancel_result])
197
+ self._unfinished_calls.pop(call_id, None)
198
+
199
+ return events_to_yield
200
+
201
+ def _register_sub_agent_task(self, task: asyncio.Task[list[ToolExecutorEvent]]) -> None:
202
+ self._sub_agent_tasks.add(task)
203
+
204
+ def _cleanup(completed: asyncio.Task[list[ToolExecutorEvent]]) -> None:
205
+ self._sub_agent_tasks.discard(completed)
206
+
207
+ task.add_done_callback(_cleanup)
208
+
209
+ @staticmethod
210
+ def _partition_tool_calls(
211
+ tool_calls: list[model.ToolCallItem],
212
+ ) -> tuple[list[model.ToolCallItem], list[model.ToolCallItem]]:
213
+ regular_tool_calls: list[model.ToolCallItem] = []
214
+ sub_agent_tool_calls: list[model.ToolCallItem] = []
215
+ for tool_call in tool_calls:
216
+ if is_sub_agent_tool(tool_call.name):
217
+ sub_agent_tool_calls.append(tool_call)
218
+ else:
219
+ regular_tool_calls.append(tool_call)
220
+ return regular_tool_calls, sub_agent_tool_calls
221
+
222
+ def _build_tool_call_started(self, tool_call: model.ToolCallItem) -> ToolExecutionCallStarted:
223
+ return ToolExecutionCallStarted(tool_call=tool_call)
224
+
225
+ async def _run_single_tool_call(self, tool_call: model.ToolCallItem) -> list[ToolExecutorEvent]:
226
+ tool_result: model.ToolResultItem = await run_tool(tool_call, self._registry)
227
+
228
+ self._append_history([tool_result])
229
+
230
+ result_event = ToolExecutionResult(tool_call=tool_call, tool_result=tool_result)
231
+
232
+ self._unfinished_calls.pop(tool_call.call_id, None)
233
+
234
+ extra_events = self._build_tool_side_effect_events(tool_result)
235
+ return [result_event, *extra_events]
236
+
237
+ def _build_tool_side_effect_events(self, tool_result: model.ToolResultItem) -> list[ToolExecutorEvent]:
238
+ side_effects = tool_result.side_effects
239
+ if not side_effects:
240
+ return []
241
+
242
+ side_effect_events: list[ToolExecutorEvent] = []
243
+
244
+ for side_effect in side_effects:
245
+ if side_effect == model.ToolSideEffect.TODO_CHANGE:
246
+ todos: list[model.TodoItem] | None = None
247
+ if tool_result.ui_extra is not None and tool_result.ui_extra.todo_list is not None:
248
+ todos = tool_result.ui_extra.todo_list.todos
249
+ if todos is not None:
250
+ side_effect_events.append(ToolExecutionTodoChange(todos=todos))
251
+
252
+ return side_effect_events