klaude-code 1.2.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- klaude_code/__init__.py +0 -0
- klaude_code/cli/__init__.py +1 -0
- klaude_code/cli/main.py +298 -0
- klaude_code/cli/runtime.py +331 -0
- klaude_code/cli/session_cmd.py +80 -0
- klaude_code/command/__init__.py +43 -0
- klaude_code/command/clear_cmd.py +20 -0
- klaude_code/command/command_abc.py +92 -0
- klaude_code/command/diff_cmd.py +138 -0
- klaude_code/command/export_cmd.py +86 -0
- klaude_code/command/help_cmd.py +51 -0
- klaude_code/command/model_cmd.py +43 -0
- klaude_code/command/prompt-dev-docs-update.md +56 -0
- klaude_code/command/prompt-dev-docs.md +46 -0
- klaude_code/command/prompt-init.md +45 -0
- klaude_code/command/prompt_command.py +69 -0
- klaude_code/command/refresh_cmd.py +43 -0
- klaude_code/command/registry.py +110 -0
- klaude_code/command/status_cmd.py +111 -0
- klaude_code/command/terminal_setup_cmd.py +252 -0
- klaude_code/config/__init__.py +11 -0
- klaude_code/config/config.py +177 -0
- klaude_code/config/list_model.py +162 -0
- klaude_code/config/select_model.py +67 -0
- klaude_code/const/__init__.py +133 -0
- klaude_code/core/__init__.py +0 -0
- klaude_code/core/agent.py +165 -0
- klaude_code/core/executor.py +485 -0
- klaude_code/core/manager/__init__.py +19 -0
- klaude_code/core/manager/agent_manager.py +127 -0
- klaude_code/core/manager/llm_clients.py +42 -0
- klaude_code/core/manager/llm_clients_builder.py +49 -0
- klaude_code/core/manager/sub_agent_manager.py +86 -0
- klaude_code/core/prompt.py +89 -0
- klaude_code/core/prompts/prompt-claude-code.md +98 -0
- klaude_code/core/prompts/prompt-codex.md +331 -0
- klaude_code/core/prompts/prompt-gemini.md +43 -0
- klaude_code/core/prompts/prompt-subagent-explore.md +27 -0
- klaude_code/core/prompts/prompt-subagent-oracle.md +23 -0
- klaude_code/core/prompts/prompt-subagent-webfetch.md +46 -0
- klaude_code/core/prompts/prompt-subagent.md +8 -0
- klaude_code/core/reminders.py +445 -0
- klaude_code/core/task.py +237 -0
- klaude_code/core/tool/__init__.py +75 -0
- klaude_code/core/tool/file/__init__.py +0 -0
- klaude_code/core/tool/file/apply_patch.py +492 -0
- klaude_code/core/tool/file/apply_patch_tool.md +1 -0
- klaude_code/core/tool/file/apply_patch_tool.py +204 -0
- klaude_code/core/tool/file/edit_tool.md +9 -0
- klaude_code/core/tool/file/edit_tool.py +274 -0
- klaude_code/core/tool/file/multi_edit_tool.md +42 -0
- klaude_code/core/tool/file/multi_edit_tool.py +199 -0
- klaude_code/core/tool/file/read_tool.md +14 -0
- klaude_code/core/tool/file/read_tool.py +326 -0
- klaude_code/core/tool/file/write_tool.md +8 -0
- klaude_code/core/tool/file/write_tool.py +146 -0
- klaude_code/core/tool/memory/__init__.py +0 -0
- klaude_code/core/tool/memory/memory_tool.md +16 -0
- klaude_code/core/tool/memory/memory_tool.py +462 -0
- klaude_code/core/tool/memory/skill_loader.py +245 -0
- klaude_code/core/tool/memory/skill_tool.md +24 -0
- klaude_code/core/tool/memory/skill_tool.py +97 -0
- klaude_code/core/tool/shell/__init__.py +0 -0
- klaude_code/core/tool/shell/bash_tool.md +43 -0
- klaude_code/core/tool/shell/bash_tool.py +123 -0
- klaude_code/core/tool/shell/command_safety.py +363 -0
- klaude_code/core/tool/sub_agent_tool.py +83 -0
- klaude_code/core/tool/todo/__init__.py +0 -0
- klaude_code/core/tool/todo/todo_write_tool.md +182 -0
- klaude_code/core/tool/todo/todo_write_tool.py +121 -0
- klaude_code/core/tool/todo/update_plan_tool.md +3 -0
- klaude_code/core/tool/todo/update_plan_tool.py +104 -0
- klaude_code/core/tool/tool_abc.py +25 -0
- klaude_code/core/tool/tool_context.py +106 -0
- klaude_code/core/tool/tool_registry.py +78 -0
- klaude_code/core/tool/tool_runner.py +252 -0
- klaude_code/core/tool/truncation.py +170 -0
- klaude_code/core/tool/web/__init__.py +0 -0
- klaude_code/core/tool/web/mermaid_tool.md +21 -0
- klaude_code/core/tool/web/mermaid_tool.py +76 -0
- klaude_code/core/tool/web/web_fetch_tool.md +8 -0
- klaude_code/core/tool/web/web_fetch_tool.py +159 -0
- klaude_code/core/turn.py +220 -0
- klaude_code/llm/__init__.py +21 -0
- klaude_code/llm/anthropic/__init__.py +3 -0
- klaude_code/llm/anthropic/client.py +221 -0
- klaude_code/llm/anthropic/input.py +200 -0
- klaude_code/llm/client.py +49 -0
- klaude_code/llm/input_common.py +239 -0
- klaude_code/llm/openai_compatible/__init__.py +3 -0
- klaude_code/llm/openai_compatible/client.py +211 -0
- klaude_code/llm/openai_compatible/input.py +109 -0
- klaude_code/llm/openai_compatible/tool_call_accumulator.py +80 -0
- klaude_code/llm/openrouter/__init__.py +3 -0
- klaude_code/llm/openrouter/client.py +200 -0
- klaude_code/llm/openrouter/input.py +160 -0
- klaude_code/llm/openrouter/reasoning_handler.py +209 -0
- klaude_code/llm/registry.py +22 -0
- klaude_code/llm/responses/__init__.py +3 -0
- klaude_code/llm/responses/client.py +216 -0
- klaude_code/llm/responses/input.py +167 -0
- klaude_code/llm/usage.py +109 -0
- klaude_code/protocol/__init__.py +4 -0
- klaude_code/protocol/commands.py +21 -0
- klaude_code/protocol/events.py +163 -0
- klaude_code/protocol/llm_param.py +147 -0
- klaude_code/protocol/model.py +287 -0
- klaude_code/protocol/op.py +89 -0
- klaude_code/protocol/op_handler.py +28 -0
- klaude_code/protocol/sub_agent.py +348 -0
- klaude_code/protocol/tools.py +15 -0
- klaude_code/session/__init__.py +4 -0
- klaude_code/session/export.py +624 -0
- klaude_code/session/selector.py +76 -0
- klaude_code/session/session.py +474 -0
- klaude_code/session/templates/export_session.html +1434 -0
- klaude_code/trace/__init__.py +3 -0
- klaude_code/trace/log.py +168 -0
- klaude_code/ui/__init__.py +91 -0
- klaude_code/ui/core/__init__.py +1 -0
- klaude_code/ui/core/display.py +103 -0
- klaude_code/ui/core/input.py +71 -0
- klaude_code/ui/core/stage_manager.py +55 -0
- klaude_code/ui/modes/__init__.py +1 -0
- klaude_code/ui/modes/debug/__init__.py +1 -0
- klaude_code/ui/modes/debug/display.py +36 -0
- klaude_code/ui/modes/exec/__init__.py +1 -0
- klaude_code/ui/modes/exec/display.py +63 -0
- klaude_code/ui/modes/repl/__init__.py +51 -0
- klaude_code/ui/modes/repl/clipboard.py +152 -0
- klaude_code/ui/modes/repl/completers.py +429 -0
- klaude_code/ui/modes/repl/display.py +60 -0
- klaude_code/ui/modes/repl/event_handler.py +375 -0
- klaude_code/ui/modes/repl/input_prompt_toolkit.py +198 -0
- klaude_code/ui/modes/repl/key_bindings.py +170 -0
- klaude_code/ui/modes/repl/renderer.py +281 -0
- klaude_code/ui/renderers/__init__.py +0 -0
- klaude_code/ui/renderers/assistant.py +21 -0
- klaude_code/ui/renderers/common.py +8 -0
- klaude_code/ui/renderers/developer.py +158 -0
- klaude_code/ui/renderers/diffs.py +215 -0
- klaude_code/ui/renderers/errors.py +16 -0
- klaude_code/ui/renderers/metadata.py +190 -0
- klaude_code/ui/renderers/sub_agent.py +71 -0
- klaude_code/ui/renderers/thinking.py +39 -0
- klaude_code/ui/renderers/tools.py +551 -0
- klaude_code/ui/renderers/user_input.py +65 -0
- klaude_code/ui/rich/__init__.py +1 -0
- klaude_code/ui/rich/live.py +65 -0
- klaude_code/ui/rich/markdown.py +308 -0
- klaude_code/ui/rich/quote.py +34 -0
- klaude_code/ui/rich/searchable_text.py +71 -0
- klaude_code/ui/rich/status.py +240 -0
- klaude_code/ui/rich/theme.py +274 -0
- klaude_code/ui/terminal/__init__.py +1 -0
- klaude_code/ui/terminal/color.py +244 -0
- klaude_code/ui/terminal/control.py +147 -0
- klaude_code/ui/terminal/notifier.py +107 -0
- klaude_code/ui/terminal/progress_bar.py +87 -0
- klaude_code/ui/utils/__init__.py +1 -0
- klaude_code/ui/utils/common.py +108 -0
- klaude_code/ui/utils/debouncer.py +42 -0
- klaude_code/version.py +163 -0
- klaude_code-1.2.6.dist-info/METADATA +178 -0
- klaude_code-1.2.6.dist-info/RECORD +167 -0
- klaude_code-1.2.6.dist-info/WHEEL +4 -0
- klaude_code-1.2.6.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,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,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
|