klaude-code 2.0.2__py3-none-any.whl → 2.1.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 +12 -0
- klaude_code/app/runtime.py +215 -0
- klaude_code/cli/auth_cmd.py +2 -2
- klaude_code/cli/config_cmd.py +2 -2
- klaude_code/cli/cost_cmd.py +1 -1
- klaude_code/cli/debug.py +12 -36
- klaude_code/cli/list_model.py +3 -3
- klaude_code/cli/main.py +17 -60
- klaude_code/cli/self_update.py +2 -187
- klaude_code/cli/session_cmd.py +2 -2
- klaude_code/config/config.py +1 -1
- klaude_code/config/select_model.py +1 -1
- klaude_code/const.py +9 -1
- klaude_code/core/agent.py +9 -62
- klaude_code/core/agent_profile.py +284 -0
- klaude_code/core/executor.py +335 -230
- klaude_code/core/manager/llm_clients_builder.py +1 -1
- klaude_code/core/manager/sub_agent_manager.py +16 -29
- klaude_code/core/reminders.py +64 -99
- klaude_code/core/task.py +12 -20
- klaude_code/core/tool/__init__.py +5 -17
- klaude_code/core/tool/context.py +84 -0
- klaude_code/core/tool/file/apply_patch_tool.py +18 -21
- klaude_code/core/tool/file/edit_tool.py +39 -42
- klaude_code/core/tool/file/read_tool.py +14 -9
- klaude_code/core/tool/file/write_tool.py +12 -13
- klaude_code/core/tool/report_back_tool.py +4 -1
- klaude_code/core/tool/shell/bash_tool.py +6 -11
- klaude_code/core/tool/skill/skill_tool.py +3 -1
- klaude_code/core/tool/sub_agent_tool.py +8 -7
- klaude_code/core/tool/todo/todo_write_tool.py +3 -9
- klaude_code/core/tool/todo/update_plan_tool.py +3 -5
- klaude_code/core/tool/tool_abc.py +2 -1
- klaude_code/core/tool/tool_registry.py +2 -33
- klaude_code/core/tool/tool_runner.py +13 -10
- klaude_code/core/tool/web/mermaid_tool.py +3 -1
- klaude_code/core/tool/web/web_fetch_tool.py +5 -3
- klaude_code/core/tool/web/web_search_tool.py +5 -3
- klaude_code/core/turn.py +86 -26
- klaude_code/llm/anthropic/client.py +1 -1
- klaude_code/llm/bedrock/client.py +1 -1
- klaude_code/llm/claude/client.py +1 -1
- klaude_code/llm/codex/client.py +1 -1
- klaude_code/llm/google/client.py +1 -1
- klaude_code/llm/openai_compatible/client.py +1 -1
- klaude_code/llm/openai_compatible/tool_call_accumulator.py +1 -1
- klaude_code/llm/openrouter/client.py +1 -1
- klaude_code/llm/openrouter/reasoning.py +1 -1
- klaude_code/llm/responses/client.py +1 -1
- klaude_code/protocol/events/__init__.py +57 -0
- klaude_code/protocol/events/base.py +18 -0
- klaude_code/protocol/events/chat.py +20 -0
- klaude_code/protocol/events/lifecycle.py +22 -0
- klaude_code/protocol/events/metadata.py +15 -0
- klaude_code/protocol/events/streaming.py +43 -0
- klaude_code/protocol/events/system.py +53 -0
- klaude_code/protocol/events/tools.py +23 -0
- klaude_code/protocol/op.py +5 -0
- klaude_code/session/session.py +6 -5
- klaude_code/skill/assets/create-plan/SKILL.md +76 -0
- klaude_code/skill/loader.py +1 -1
- klaude_code/skill/system_skills.py +1 -1
- klaude_code/tui/__init__.py +8 -0
- klaude_code/{command → tui/command}/clear_cmd.py +2 -1
- klaude_code/{command → tui/command}/debug_cmd.py +3 -2
- klaude_code/{command → tui/command}/export_cmd.py +2 -1
- klaude_code/{command → tui/command}/export_online_cmd.py +2 -1
- klaude_code/{command → tui/command}/fork_session_cmd.py +4 -3
- klaude_code/{command → tui/command}/help_cmd.py +2 -1
- klaude_code/{command → tui/command}/model_cmd.py +4 -3
- klaude_code/{command → tui/command}/model_select.py +2 -2
- klaude_code/{command → tui/command}/prompt_command.py +4 -3
- klaude_code/{command → tui/command}/refresh_cmd.py +3 -1
- klaude_code/{command → tui/command}/registry.py +6 -5
- klaude_code/{command → tui/command}/release_notes_cmd.py +2 -1
- klaude_code/{command → tui/command}/resume_cmd.py +4 -3
- klaude_code/{command → tui/command}/status_cmd.py +2 -1
- klaude_code/{command → tui/command}/terminal_setup_cmd.py +2 -1
- klaude_code/{command → tui/command}/thinking_cmd.py +3 -2
- klaude_code/tui/commands.py +164 -0
- klaude_code/{ui/renderers → tui/components}/assistant.py +3 -3
- klaude_code/{ui/renderers → tui/components}/bash_syntax.py +2 -2
- klaude_code/{ui/renderers → tui/components}/common.py +1 -1
- klaude_code/{ui/renderers → tui/components}/developer.py +4 -4
- klaude_code/{ui/renderers → tui/components}/diffs.py +2 -2
- klaude_code/{ui/renderers → tui/components}/errors.py +2 -2
- klaude_code/{ui/renderers → tui/components}/metadata.py +7 -7
- klaude_code/{ui → tui/components}/rich/markdown.py +9 -23
- klaude_code/{ui → tui/components}/rich/status.py +2 -2
- klaude_code/{ui → tui/components}/rich/theme.py +3 -1
- klaude_code/{ui/renderers → tui/components}/sub_agent.py +23 -43
- klaude_code/{ui/renderers → tui/components}/thinking.py +3 -3
- klaude_code/{ui/renderers → tui/components}/tools.py +9 -9
- klaude_code/{ui/renderers → tui/components}/user_input.py +3 -20
- klaude_code/tui/display.py +85 -0
- klaude_code/{ui/modes/repl → tui/input}/__init__.py +1 -1
- klaude_code/{ui/modes/repl → tui/input}/completers.py +1 -1
- klaude_code/{ui/modes/repl/input_prompt_toolkit.py → tui/input/prompt_toolkit.py} +6 -6
- klaude_code/tui/machine.py +606 -0
- klaude_code/tui/renderer.py +707 -0
- klaude_code/tui/runner.py +321 -0
- klaude_code/tui/terminal/__init__.py +56 -0
- klaude_code/{ui → tui}/terminal/color.py +1 -1
- klaude_code/{ui → tui}/terminal/control.py +1 -1
- klaude_code/{ui → tui}/terminal/notifier.py +1 -1
- klaude_code/ui/__init__.py +6 -50
- klaude_code/ui/core/display.py +3 -3
- klaude_code/ui/core/input.py +2 -1
- klaude_code/ui/{modes/debug/display.py → debug_mode.py} +1 -1
- klaude_code/ui/{modes/exec/display.py → exec_mode.py} +0 -2
- klaude_code/ui/terminal/__init__.py +6 -54
- klaude_code/ui/terminal/title.py +31 -0
- klaude_code/update.py +163 -0
- {klaude_code-2.0.2.dist-info → klaude_code-2.1.0.dist-info}/METADATA +1 -1
- klaude_code-2.1.0.dist-info/RECORD +235 -0
- klaude_code/cli/runtime.py +0 -518
- klaude_code/core/prompt.py +0 -108
- klaude_code/core/tool/tool_context.py +0 -148
- klaude_code/protocol/events.py +0 -195
- klaude_code/skill/assets/dev-docs/SKILL.md +0 -108
- klaude_code/trace/__init__.py +0 -21
- klaude_code/ui/core/stage_manager.py +0 -48
- klaude_code/ui/modes/__init__.py +0 -1
- klaude_code/ui/modes/debug/__init__.py +0 -1
- klaude_code/ui/modes/exec/__init__.py +0 -1
- klaude_code/ui/modes/repl/display.py +0 -61
- klaude_code/ui/modes/repl/event_handler.py +0 -629
- klaude_code/ui/modes/repl/renderer.py +0 -464
- klaude_code/ui/utils/__init__.py +0 -1
- klaude_code-2.0.2.dist-info/RECORD +0 -227
- /klaude_code/{trace/log.py → log.py} +0 -0
- /klaude_code/{command → tui/command}/__init__.py +0 -0
- /klaude_code/{command → tui/command}/command_abc.py +0 -0
- /klaude_code/{command → tui/command}/prompt-commit.md +0 -0
- /klaude_code/{command → tui/command}/prompt-init.md +0 -0
- /klaude_code/{ui/renderers → tui/components}/__init__.py +0 -0
- /klaude_code/{ui/renderers → tui/components}/mermaid_viewer.py +0 -0
- /klaude_code/{ui → tui/components}/rich/__init__.py +0 -0
- /klaude_code/{ui → tui/components}/rich/cjk_wrap.py +0 -0
- /klaude_code/{ui → tui/components}/rich/code_panel.py +0 -0
- /klaude_code/{ui → tui/components}/rich/live.py +0 -0
- /klaude_code/{ui → tui/components}/rich/quote.py +0 -0
- /klaude_code/{ui → tui/components}/rich/searchable_text.py +0 -0
- /klaude_code/{ui/modes/repl → tui/input}/clipboard.py +0 -0
- /klaude_code/{ui/modes/repl → tui/input}/key_bindings.py +0 -0
- /klaude_code/{ui → tui}/terminal/image.py +0 -0
- /klaude_code/{ui → tui}/terminal/progress_bar.py +0 -0
- /klaude_code/{ui → tui}/terminal/selector.py +0 -0
- /klaude_code/ui/{utils/common.py → common.py} +0 -0
- {klaude_code-2.0.2.dist-info → klaude_code-2.1.0.dist-info}/WHEEL +0 -0
- {klaude_code-2.0.2.dist-info → klaude_code-2.1.0.dist-info}/entry_points.txt +0 -0
|
@@ -6,9 +6,9 @@ from klaude_code.config import Config
|
|
|
6
6
|
from klaude_code.core.manager.llm_clients import LLMClients
|
|
7
7
|
from klaude_code.llm.client import LLMClientABC
|
|
8
8
|
from klaude_code.llm.registry import create_llm_client
|
|
9
|
+
from klaude_code.log import DebugType, log_debug
|
|
9
10
|
from klaude_code.protocol.sub_agent import iter_sub_agent_profiles
|
|
10
11
|
from klaude_code.protocol.tools import SubAgentType
|
|
11
|
-
from klaude_code.trace import DebugType, log_debug
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
def build_llm_clients(
|
|
@@ -9,15 +9,15 @@ from __future__ import annotations
|
|
|
9
9
|
|
|
10
10
|
import asyncio
|
|
11
11
|
import json
|
|
12
|
+
from collections.abc import Callable
|
|
12
13
|
|
|
13
|
-
from klaude_code.core.agent import Agent
|
|
14
|
+
from klaude_code.core.agent import Agent
|
|
15
|
+
from klaude_code.core.agent_profile import ModelProfileProvider
|
|
14
16
|
from klaude_code.core.manager.llm_clients import LLMClients
|
|
15
|
-
from klaude_code.
|
|
16
|
-
from klaude_code.core.tool.tool_context import record_sub_agent_session_id
|
|
17
|
+
from klaude_code.log import DebugType, log_debug
|
|
17
18
|
from klaude_code.protocol import events, message, model
|
|
18
19
|
from klaude_code.protocol.sub_agent import SubAgentResult
|
|
19
20
|
from klaude_code.session.session import Session
|
|
20
|
-
from klaude_code.trace import DebugType, log_debug
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
class SubAgentManager:
|
|
@@ -38,7 +38,13 @@ class SubAgentManager:
|
|
|
38
38
|
|
|
39
39
|
await self._event_queue.put(event)
|
|
40
40
|
|
|
41
|
-
async def run_sub_agent(
|
|
41
|
+
async def run_sub_agent(
|
|
42
|
+
self,
|
|
43
|
+
parent_agent: Agent,
|
|
44
|
+
state: model.SubAgentState,
|
|
45
|
+
*,
|
|
46
|
+
record_session_id: Callable[[str], None] | None = None,
|
|
47
|
+
) -> SubAgentResult:
|
|
42
48
|
"""Run a nested sub-agent task and return its result."""
|
|
43
49
|
|
|
44
50
|
parent_session = parent_agent.session
|
|
@@ -77,9 +83,8 @@ class SubAgentManager:
|
|
|
77
83
|
error=True,
|
|
78
84
|
)
|
|
79
85
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
record_sub_agent_session_id(child_session.id)
|
|
86
|
+
if record_session_id is not None:
|
|
87
|
+
record_session_id(child_session.id)
|
|
83
88
|
|
|
84
89
|
# Update persisted sub-agent state to reflect the current invocation.
|
|
85
90
|
child_session.sub_agent_state.sub_agent_desc = state.sub_agent_desc
|
|
@@ -92,33 +97,15 @@ class SubAgentManager:
|
|
|
92
97
|
child_session = Session(work_dir=parent_session.work_dir)
|
|
93
98
|
child_session.sub_agent_state = state
|
|
94
99
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
record_sub_agent_session_id(child_session.id)
|
|
100
|
+
if record_session_id is not None:
|
|
101
|
+
record_session_id(child_session.id)
|
|
98
102
|
|
|
99
103
|
child_profile = self._model_profile_provider.build_profile(
|
|
100
104
|
self._llm_clients.get_client(state.sub_agent_type),
|
|
101
105
|
state.sub_agent_type,
|
|
106
|
+
output_schema=state.output_schema,
|
|
102
107
|
)
|
|
103
108
|
|
|
104
|
-
# Inject report_back tool if output_schema is provided
|
|
105
|
-
if state.output_schema:
|
|
106
|
-
report_back_tool_class = ReportBackTool.for_schema(state.output_schema)
|
|
107
|
-
report_back_prompt = """\
|
|
108
|
-
|
|
109
|
-
# Structured Output
|
|
110
|
-
You have a `report_back` tool available. When you complete the task,\
|
|
111
|
-
you MUST call `report_back` with the structured result matching the required schema.\
|
|
112
|
-
Only the content passed to `report_back` will be returned to user.\
|
|
113
|
-
"""
|
|
114
|
-
base_prompt = child_profile.system_prompt or ""
|
|
115
|
-
child_profile = AgentProfile(
|
|
116
|
-
llm_client=child_profile.llm_client,
|
|
117
|
-
system_prompt=base_prompt + report_back_prompt,
|
|
118
|
-
tools=[*child_profile.tools, report_back_tool_class.schema()],
|
|
119
|
-
reminders=child_profile.reminders,
|
|
120
|
-
)
|
|
121
|
-
|
|
122
109
|
child_agent = Agent(session=child_session, profile=child_profile)
|
|
123
110
|
|
|
124
111
|
log_debug(
|
klaude_code/core/reminders.py
CHANGED
|
@@ -1,22 +1,19 @@
|
|
|
1
1
|
import hashlib
|
|
2
2
|
import re
|
|
3
3
|
import shlex
|
|
4
|
-
from collections.abc import Awaitable, Callable
|
|
5
4
|
from dataclasses import dataclass
|
|
6
5
|
from pathlib import Path
|
|
7
6
|
|
|
8
7
|
from pydantic import BaseModel
|
|
9
8
|
|
|
10
9
|
from klaude_code.const import MEMORY_FILE_NAMES, REMINDER_COOLDOWN_TURNS, TODO_REMINDER_TOOL_CALL_THRESHOLD
|
|
11
|
-
from klaude_code.core.tool import BashTool, ReadTool,
|
|
10
|
+
from klaude_code.core.tool import BashTool, ReadTool, build_todo_context
|
|
11
|
+
from klaude_code.core.tool.context import ToolContext
|
|
12
12
|
from klaude_code.core.tool.file._utils import hash_text_sha256
|
|
13
13
|
from klaude_code.protocol import message, model, tools
|
|
14
14
|
from klaude_code.session import Session
|
|
15
15
|
from klaude_code.skill import get_skill
|
|
16
16
|
|
|
17
|
-
type Reminder = Callable[[Session], Awaitable[message.DeveloperMessage | None]]
|
|
18
|
-
|
|
19
|
-
|
|
20
17
|
# Match @ preceded by whitespace, start of line, or → (ReadTool line number arrow)
|
|
21
18
|
AT_FILE_PATTERN = re.compile(r'(?:(?<!\S)|(?<=\u2192))@("(?P<quoted>[^\"]+)"|(?P<plain>\S+))')
|
|
22
19
|
|
|
@@ -131,57 +128,59 @@ async def _load_at_file_recursive(
|
|
|
131
128
|
return
|
|
132
129
|
visited.add(path_str)
|
|
133
130
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
131
|
+
tool_context = ToolContext(
|
|
132
|
+
file_tracker=session.file_tracker,
|
|
133
|
+
todo_context=build_todo_context(session),
|
|
134
|
+
session_id=session.id,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
if path.exists() and path.is_file():
|
|
138
|
+
if _is_tracked_file_unchanged(session, path_str):
|
|
139
|
+
return
|
|
140
|
+
args = ReadTool.ReadArguments(file_path=path_str)
|
|
141
|
+
tool_result = await ReadTool.call_with_args(args, tool_context)
|
|
142
|
+
images = [part for part in tool_result.parts if isinstance(part, message.ImageURLPart)]
|
|
143
|
+
|
|
144
|
+
tool_args = args.model_dump_json(exclude_none=True)
|
|
145
|
+
formatted_blocks.append(
|
|
146
|
+
f"""Called the {tools.READ} tool with the following input: {tool_args}
|
|
146
147
|
Result of calling the {tools.READ} tool:
|
|
147
148
|
{tool_result.output_text}
|
|
148
149
|
"""
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
150
|
+
)
|
|
151
|
+
at_ops.append(model.AtFileOp(operation="Read", path=path_str, mentioned_in=mentioned_in))
|
|
152
|
+
if images:
|
|
153
|
+
collected_images.extend(images)
|
|
154
|
+
|
|
155
|
+
# Recursively parse @ references from ReadTool output
|
|
156
|
+
output = tool_result.output_text
|
|
157
|
+
if "@" in output:
|
|
158
|
+
for match in AT_FILE_PATTERN.finditer(output):
|
|
159
|
+
nested = match.group("quoted") or match.group("plain")
|
|
160
|
+
if nested:
|
|
161
|
+
await _load_at_file_recursive(
|
|
162
|
+
session,
|
|
163
|
+
nested,
|
|
164
|
+
at_ops,
|
|
165
|
+
formatted_blocks,
|
|
166
|
+
collected_images,
|
|
167
|
+
visited,
|
|
168
|
+
base_dir=path.parent,
|
|
169
|
+
mentioned_in=path_str,
|
|
170
|
+
)
|
|
171
|
+
elif path.exists() and path.is_dir():
|
|
172
|
+
quoted_path = shlex.quote(path_str)
|
|
173
|
+
args = BashTool.BashArguments(command=f"ls {quoted_path}")
|
|
174
|
+
tool_result = await BashTool.call_with_args(args, tool_context)
|
|
175
|
+
|
|
176
|
+
tool_args = args.model_dump_json(exclude_none=True)
|
|
177
|
+
formatted_blocks.append(
|
|
178
|
+
f"""Called the {tools.BASH} tool with the following input: {tool_args}
|
|
178
179
|
Result of calling the {tools.BASH} tool:
|
|
179
180
|
{tool_result.output_text}
|
|
180
181
|
"""
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
finally:
|
|
184
|
-
reset_tool_context(context_token)
|
|
182
|
+
)
|
|
183
|
+
at_ops.append(model.AtFileOp(operation="List", path=path_str + "/", mentioned_in=mentioned_in))
|
|
185
184
|
|
|
186
185
|
|
|
187
186
|
async def at_file_reader_reminder(
|
|
@@ -329,18 +328,20 @@ async def file_changed_externally_reminder(
|
|
|
329
328
|
changed = current_mtime != status.mtime
|
|
330
329
|
|
|
331
330
|
if changed:
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
331
|
+
tool_context = ToolContext(
|
|
332
|
+
file_tracker=session.file_tracker,
|
|
333
|
+
todo_context=build_todo_context(session),
|
|
334
|
+
session_id=session.id,
|
|
335
|
+
)
|
|
336
|
+
tool_result = await ReadTool.call_with_args(
|
|
337
|
+
ReadTool.ReadArguments(file_path=path),
|
|
338
|
+
tool_context,
|
|
339
|
+
) # This tool will update file tracker
|
|
340
|
+
if tool_result.status == "success":
|
|
341
|
+
images = [part for part in tool_result.parts if isinstance(part, message.ImageURLPart)]
|
|
342
|
+
changed_files.append((path, tool_result.output_text, images or None))
|
|
343
|
+
if images:
|
|
344
|
+
collected_images.extend(images)
|
|
344
345
|
except (
|
|
345
346
|
FileNotFoundError,
|
|
346
347
|
IsADirectoryError,
|
|
@@ -427,9 +428,7 @@ async def image_reminder(session: Session) -> message.DeveloperMessage | None:
|
|
|
427
428
|
return None
|
|
428
429
|
|
|
429
430
|
return message.DeveloperMessage(
|
|
430
|
-
parts=
|
|
431
|
-
f"<system-reminder>User attached {image_count} image{'s' if image_count > 1 else ''} in their message. Make sure to analyze and reference these images as needed.</system-reminder>"
|
|
432
|
-
),
|
|
431
|
+
parts=[],
|
|
433
432
|
ui_extra=model.DeveloperUIExtra(items=[model.UserImagesUIItem(count=image_count)]),
|
|
434
433
|
)
|
|
435
434
|
|
|
@@ -603,37 +602,3 @@ ALL_REMINDERS = [
|
|
|
603
602
|
image_reminder,
|
|
604
603
|
skill_reminder,
|
|
605
604
|
]
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
def load_agent_reminders(
|
|
609
|
-
model_name: str, sub_agent_type: str | None = None, *, vanilla: bool = False
|
|
610
|
-
) -> list[Reminder]:
|
|
611
|
-
"""Get reminders for an agent based on model and agent type.
|
|
612
|
-
|
|
613
|
-
Args:
|
|
614
|
-
model_name: The model name.
|
|
615
|
-
sub_agent_type: If None, returns main agent reminders. Otherwise returns sub-agent reminders.
|
|
616
|
-
vanilla: If True, returns minimal vanilla reminders (ignores sub_agent_type).
|
|
617
|
-
"""
|
|
618
|
-
if vanilla:
|
|
619
|
-
return [at_file_reader_reminder]
|
|
620
|
-
|
|
621
|
-
reminders: list[Reminder] = []
|
|
622
|
-
|
|
623
|
-
# Only main agent (not sub-agent) gets todo reminders, and not for GPT-5
|
|
624
|
-
if sub_agent_type is None and "gpt-5" not in model_name:
|
|
625
|
-
reminders.append(empty_todo_reminder)
|
|
626
|
-
reminders.append(todo_not_used_recently_reminder)
|
|
627
|
-
|
|
628
|
-
reminders.extend(
|
|
629
|
-
[
|
|
630
|
-
memory_reminder,
|
|
631
|
-
at_file_reader_reminder,
|
|
632
|
-
last_path_memory_reminder,
|
|
633
|
-
file_changed_externally_reminder,
|
|
634
|
-
image_reminder,
|
|
635
|
-
skill_reminder,
|
|
636
|
-
]
|
|
637
|
-
)
|
|
638
|
-
|
|
639
|
-
return reminders
|
klaude_code/core/task.py
CHANGED
|
@@ -4,17 +4,14 @@ import asyncio
|
|
|
4
4
|
import time
|
|
5
5
|
from collections.abc import AsyncGenerator, Callable, Sequence
|
|
6
6
|
from dataclasses import dataclass
|
|
7
|
-
from typing import TYPE_CHECKING
|
|
8
7
|
|
|
9
8
|
from klaude_code.const import INITIAL_RETRY_DELAY_S, MAX_FAILED_TURN_RETRIES, MAX_RETRY_DELAY_S
|
|
10
|
-
from klaude_code.core.
|
|
9
|
+
from klaude_code.core.agent_profile import AgentProfile, Reminder
|
|
11
10
|
from klaude_code.core.tool import FileTracker, TodoContext, ToolABC
|
|
11
|
+
from klaude_code.core.tool.context import RunSubtask
|
|
12
12
|
from klaude_code.core.turn import TurnError, TurnExecutionContext, TurnExecutor
|
|
13
|
+
from klaude_code.log import DebugType, log_debug
|
|
13
14
|
from klaude_code.protocol import events, message, model
|
|
14
|
-
from klaude_code.trace import DebugType, log_debug
|
|
15
|
-
|
|
16
|
-
if TYPE_CHECKING:
|
|
17
|
-
from klaude_code.core.agent import AgentProfile
|
|
18
15
|
|
|
19
16
|
|
|
20
17
|
class MetadataAccumulator:
|
|
@@ -101,6 +98,7 @@ class SessionContext:
|
|
|
101
98
|
append_history: Callable[[Sequence[message.HistoryEvent]], None]
|
|
102
99
|
file_tracker: FileTracker
|
|
103
100
|
todo_context: TodoContext
|
|
101
|
+
run_subtask: RunSubtask | None
|
|
104
102
|
|
|
105
103
|
|
|
106
104
|
@dataclass
|
|
@@ -189,26 +187,20 @@ class TaskExecutor:
|
|
|
189
187
|
self._current_turn = turn
|
|
190
188
|
|
|
191
189
|
try:
|
|
192
|
-
async for
|
|
193
|
-
match
|
|
194
|
-
case events.
|
|
190
|
+
async for e in turn.run():
|
|
191
|
+
match e:
|
|
192
|
+
case events.ResponseCompleteEvent() as am:
|
|
195
193
|
yield am
|
|
196
|
-
case events.
|
|
197
|
-
metadata_accumulator.add(e.
|
|
198
|
-
|
|
199
|
-
context_percent = e.metadata.context_usage_percent
|
|
200
|
-
if context_percent is not None:
|
|
201
|
-
yield events.ContextUsageEvent(
|
|
202
|
-
session_id=session_ctx.session_id,
|
|
203
|
-
context_percent=context_percent,
|
|
204
|
-
)
|
|
194
|
+
case events.UsageEvent() as e:
|
|
195
|
+
metadata_accumulator.add(e.usage)
|
|
196
|
+
yield e
|
|
205
197
|
case events.ToolResultEvent() as e:
|
|
206
198
|
# Collect sub-agent task metadata from tool results
|
|
207
199
|
if e.task_metadata is not None:
|
|
208
200
|
metadata_accumulator.add_sub_agent_metadata(e.task_metadata)
|
|
209
|
-
yield
|
|
201
|
+
yield e
|
|
210
202
|
case _:
|
|
211
|
-
yield
|
|
203
|
+
yield e
|
|
212
204
|
|
|
213
205
|
turn_succeeded = True
|
|
214
206
|
break
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from .context import FileTracker, RunSubtask, SubAgentResumeClaims, TodoContext, ToolContext, build_todo_context
|
|
1
2
|
from .file.apply_patch import DiffError, process_patch
|
|
2
3
|
from .file.apply_patch_tool import ApplyPatchTool
|
|
3
4
|
from .file.edit_tool import EditTool
|
|
@@ -11,17 +12,7 @@ from .sub_agent_tool import SubAgentTool
|
|
|
11
12
|
from .todo.todo_write_tool import TodoWriteTool
|
|
12
13
|
from .todo.update_plan_tool import UpdatePlanTool
|
|
13
14
|
from .tool_abc import ToolABC
|
|
14
|
-
from .
|
|
15
|
-
FileTracker,
|
|
16
|
-
TodoContext,
|
|
17
|
-
ToolContextToken,
|
|
18
|
-
build_todo_context,
|
|
19
|
-
current_run_subtask_callback,
|
|
20
|
-
reset_tool_context,
|
|
21
|
-
set_tool_context_from_session,
|
|
22
|
-
tool_context,
|
|
23
|
-
)
|
|
24
|
-
from .tool_registry import get_registry, get_tool_schemas, load_agent_tools
|
|
15
|
+
from .tool_registry import get_registry, get_tool_schemas
|
|
25
16
|
from .tool_runner import run_tool
|
|
26
17
|
from .truncation import SimpleTruncationStrategy, TruncationStrategy, get_truncation_strategy, set_truncation_strategy
|
|
27
18
|
from .web.mermaid_tool import MermaidTool
|
|
@@ -37,30 +28,27 @@ __all__ = [
|
|
|
37
28
|
"MermaidTool",
|
|
38
29
|
"ReadTool",
|
|
39
30
|
"ReportBackTool",
|
|
31
|
+
"RunSubtask",
|
|
40
32
|
"SafetyCheckResult",
|
|
41
33
|
"SimpleTruncationStrategy",
|
|
42
34
|
"SkillTool",
|
|
35
|
+
"SubAgentResumeClaims",
|
|
43
36
|
"SubAgentTool",
|
|
44
37
|
"TodoContext",
|
|
45
38
|
"TodoWriteTool",
|
|
46
39
|
"ToolABC",
|
|
47
|
-
"
|
|
40
|
+
"ToolContext",
|
|
48
41
|
"TruncationStrategy",
|
|
49
42
|
"UpdatePlanTool",
|
|
50
43
|
"WebFetchTool",
|
|
51
44
|
"WebSearchTool",
|
|
52
45
|
"WriteTool",
|
|
53
46
|
"build_todo_context",
|
|
54
|
-
"current_run_subtask_callback",
|
|
55
47
|
"get_registry",
|
|
56
48
|
"get_tool_schemas",
|
|
57
49
|
"get_truncation_strategy",
|
|
58
50
|
"is_safe_command",
|
|
59
|
-
"load_agent_tools",
|
|
60
51
|
"process_patch",
|
|
61
|
-
"reset_tool_context",
|
|
62
52
|
"run_tool",
|
|
63
|
-
"set_tool_context_from_session",
|
|
64
53
|
"set_truncation_strategy",
|
|
65
|
-
"tool_context",
|
|
66
54
|
]
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from collections.abc import Awaitable, Callable, MutableMapping
|
|
5
|
+
from dataclasses import dataclass, replace
|
|
6
|
+
|
|
7
|
+
from klaude_code.protocol import model
|
|
8
|
+
from klaude_code.protocol.sub_agent import SubAgentResult
|
|
9
|
+
from klaude_code.session.session import Session
|
|
10
|
+
|
|
11
|
+
type FileTracker = MutableMapping[str, model.FileStatus]
|
|
12
|
+
|
|
13
|
+
RunSubtask = Callable[[model.SubAgentState, Callable[[str], None] | None], Awaitable[SubAgentResult]]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class TodoContext:
|
|
18
|
+
"""Todo access interface exposed to tools.
|
|
19
|
+
|
|
20
|
+
Tools can only read the current todo list and replace it with
|
|
21
|
+
a new list; they cannot access the full Session object.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
get_todos: Callable[[], list[model.TodoItem]]
|
|
25
|
+
set_todos: Callable[[list[model.TodoItem]], None]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class SessionTodoStore:
|
|
30
|
+
"""Adapter exposing session todos through an explicit interface."""
|
|
31
|
+
|
|
32
|
+
session: Session
|
|
33
|
+
|
|
34
|
+
def get(self) -> list[model.TodoItem]:
|
|
35
|
+
return self.session.todos
|
|
36
|
+
|
|
37
|
+
def set(self, todos: list[model.TodoItem]) -> None:
|
|
38
|
+
self.session.todos = todos
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def build_todo_context(session: Session) -> TodoContext:
|
|
42
|
+
"""Create a TodoContext backed by the given session."""
|
|
43
|
+
|
|
44
|
+
store = SessionTodoStore(session)
|
|
45
|
+
return TodoContext(get_todos=store.get, set_todos=store.set)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class SubAgentResumeClaims:
|
|
49
|
+
"""Track sub-agent resume claims for a single turn.
|
|
50
|
+
|
|
51
|
+
Multiple concurrent sub-agent tool calls can attempt to resume the same
|
|
52
|
+
session id in a single model response. This class provides an atomic
|
|
53
|
+
`claim()` operation to reject duplicates.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(self) -> None:
|
|
57
|
+
self._claims: set[str] = set()
|
|
58
|
+
self._lock = asyncio.Lock()
|
|
59
|
+
|
|
60
|
+
async def claim(self, session_id: str) -> bool:
|
|
61
|
+
async with self._lock:
|
|
62
|
+
if session_id in self._claims:
|
|
63
|
+
return False
|
|
64
|
+
self._claims.add(session_id)
|
|
65
|
+
return True
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass(frozen=True)
|
|
69
|
+
class ToolContext:
|
|
70
|
+
"""Tool execution context.
|
|
71
|
+
|
|
72
|
+
This object is shallow-immutable: fields cannot be reassigned, but fields
|
|
73
|
+
may reference mutable objects (e.g., FileTracker).
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
file_tracker: FileTracker
|
|
77
|
+
todo_context: TodoContext
|
|
78
|
+
session_id: str
|
|
79
|
+
run_subtask: RunSubtask | None = None
|
|
80
|
+
sub_agent_resume_claims: SubAgentResumeClaims | None = None
|
|
81
|
+
record_sub_agent_session_id: Callable[[str], None] | None = None
|
|
82
|
+
|
|
83
|
+
def with_record_sub_agent_session_id(self, callback: Callable[[str], None] | None) -> ToolContext:
|
|
84
|
+
return replace(self, record_sub_agent_session_id=callback)
|
|
@@ -7,20 +7,20 @@ from pathlib import Path
|
|
|
7
7
|
|
|
8
8
|
from pydantic import BaseModel
|
|
9
9
|
|
|
10
|
+
from klaude_code.core.tool.context import FileTracker, ToolContext
|
|
10
11
|
from klaude_code.core.tool.file import apply_patch as apply_patch_module
|
|
11
12
|
from klaude_code.core.tool.file._utils import hash_text_sha256
|
|
12
13
|
from klaude_code.core.tool.file.diff_builder import build_structured_file_diff
|
|
13
14
|
from klaude_code.core.tool.tool_abc import ToolABC, load_desc
|
|
14
|
-
from klaude_code.core.tool.tool_context import get_current_file_tracker
|
|
15
15
|
from klaude_code.core.tool.tool_registry import register
|
|
16
16
|
from klaude_code.protocol import llm_param, message, model, tools
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
class ApplyPatchHandler:
|
|
20
20
|
@classmethod
|
|
21
|
-
async def handle_apply_patch(cls, patch_text: str) -> message.ToolResultMessage:
|
|
21
|
+
async def handle_apply_patch(cls, patch_text: str, context: ToolContext) -> message.ToolResultMessage:
|
|
22
22
|
try:
|
|
23
|
-
output, ui_extra = await asyncio.to_thread(cls._apply_patch_in_thread, patch_text)
|
|
23
|
+
output, ui_extra = await asyncio.to_thread(cls._apply_patch_in_thread, patch_text, context.file_tracker)
|
|
24
24
|
except apply_patch_module.DiffError as error:
|
|
25
25
|
return message.ToolResultMessage(status="error", output_text=str(error))
|
|
26
26
|
except Exception as error: # pragma: no cover # unexpected errors bubbled to tool result
|
|
@@ -32,14 +32,13 @@ class ApplyPatchHandler:
|
|
|
32
32
|
)
|
|
33
33
|
|
|
34
34
|
@staticmethod
|
|
35
|
-
def _apply_patch_in_thread(patch_text: str) -> tuple[str, model.ToolResultUIExtra]:
|
|
35
|
+
def _apply_patch_in_thread(patch_text: str, file_tracker: FileTracker) -> tuple[str, model.ToolResultUIExtra]:
|
|
36
36
|
ap = apply_patch_module
|
|
37
37
|
normalized_start = patch_text.lstrip()
|
|
38
38
|
if not normalized_start.startswith("*** Begin Patch"):
|
|
39
39
|
raise ap.DiffError("apply_patch content must start with *** Begin Patch")
|
|
40
40
|
|
|
41
41
|
workspace_root = os.path.realpath(os.getcwd())
|
|
42
|
-
file_tracker = get_current_file_tracker()
|
|
43
42
|
|
|
44
43
|
def resolve_path(path: str) -> str:
|
|
45
44
|
candidate = os.path.realpath(path if os.path.isabs(path) else os.path.join(workspace_root, path))
|
|
@@ -89,15 +88,14 @@ class ApplyPatchHandler:
|
|
|
89
88
|
with open(resolved, "w", encoding="utf-8") as handle:
|
|
90
89
|
handle.write(content)
|
|
91
90
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
)
|
|
91
|
+
with contextlib.suppress(Exception): # pragma: no cover - file tracker best-effort
|
|
92
|
+
existing = file_tracker.get(resolved)
|
|
93
|
+
is_mem = existing.is_memory if existing else False
|
|
94
|
+
file_tracker[resolved] = model.FileStatus(
|
|
95
|
+
mtime=Path(resolved).stat().st_mtime,
|
|
96
|
+
content_sha256=hash_text_sha256(content),
|
|
97
|
+
is_memory=is_mem,
|
|
98
|
+
)
|
|
101
99
|
|
|
102
100
|
def remove_fn(path: str) -> None:
|
|
103
101
|
resolved = resolve_path(path)
|
|
@@ -107,9 +105,8 @@ class ApplyPatchHandler:
|
|
|
107
105
|
raise ap.DiffError(f"Cannot delete directory: {path}")
|
|
108
106
|
os.remove(resolved)
|
|
109
107
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
file_tracker.pop(resolved, None)
|
|
108
|
+
with contextlib.suppress(Exception): # pragma: no cover - file tracker best-effort
|
|
109
|
+
file_tracker.pop(resolved, None)
|
|
113
110
|
|
|
114
111
|
ap.apply_commit(commit, write_fn, remove_fn)
|
|
115
112
|
|
|
@@ -172,13 +169,13 @@ class ApplyPatchTool(ToolABC):
|
|
|
172
169
|
)
|
|
173
170
|
|
|
174
171
|
@classmethod
|
|
175
|
-
async def call(cls, arguments: str) -> message.ToolResultMessage:
|
|
172
|
+
async def call(cls, arguments: str, context: ToolContext) -> message.ToolResultMessage:
|
|
176
173
|
try:
|
|
177
174
|
args = cls.ApplyPatchArguments.model_validate_json(arguments)
|
|
178
175
|
except ValueError as exc:
|
|
179
176
|
return message.ToolResultMessage(status="error", output_text=f"Invalid arguments: {exc}")
|
|
180
|
-
return await cls.call_with_args(args)
|
|
177
|
+
return await cls.call_with_args(args, context)
|
|
181
178
|
|
|
182
179
|
@classmethod
|
|
183
|
-
async def call_with_args(cls, args: ApplyPatchArguments) -> message.ToolResultMessage:
|
|
184
|
-
return await ApplyPatchHandler.handle_apply_patch(args.patch)
|
|
180
|
+
async def call_with_args(cls, args: ApplyPatchArguments, context: ToolContext) -> message.ToolResultMessage:
|
|
181
|
+
return await ApplyPatchHandler.handle_apply_patch(args.patch, context)
|