klaude-code 1.2.15__py3-none-any.whl → 1.2.16__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/cli/main.py +66 -42
- klaude_code/cli/runtime.py +24 -13
- klaude_code/command/export_cmd.py +2 -2
- klaude_code/command/prompt-handoff.md +33 -0
- klaude_code/command/thinking_cmd.py +5 -1
- klaude_code/config/config.py +5 -5
- klaude_code/config/list_model.py +1 -1
- klaude_code/const/__init__.py +3 -0
- klaude_code/core/executor.py +2 -2
- klaude_code/core/manager/llm_clients_builder.py +1 -1
- klaude_code/core/manager/sub_agent_manager.py +30 -6
- klaude_code/core/prompt.py +15 -13
- klaude_code/core/prompts/{prompt-subagent-explore.md → prompt-sub-agent-explore.md} +0 -1
- klaude_code/core/prompts/{prompt-subagent-oracle.md → prompt-sub-agent-oracle.md} +1 -1
- klaude_code/core/reminders.py +75 -32
- klaude_code/core/task.py +10 -22
- klaude_code/core/tool/__init__.py +2 -0
- klaude_code/core/tool/report_back_tool.py +58 -0
- klaude_code/core/tool/sub_agent_tool.py +6 -0
- klaude_code/core/tool/tool_runner.py +9 -1
- klaude_code/core/turn.py +45 -4
- klaude_code/llm/anthropic/input.py +14 -5
- klaude_code/llm/openrouter/input.py +14 -3
- klaude_code/llm/responses/input.py +19 -0
- klaude_code/protocol/events.py +1 -0
- klaude_code/protocol/model.py +24 -14
- klaude_code/protocol/sub_agent/__init__.py +117 -0
- klaude_code/protocol/sub_agent/explore.py +63 -0
- klaude_code/protocol/sub_agent/oracle.py +91 -0
- klaude_code/protocol/sub_agent/task.py +61 -0
- klaude_code/protocol/sub_agent/web_fetch.py +74 -0
- klaude_code/protocol/tools.py +1 -0
- klaude_code/session/export.py +12 -6
- klaude_code/session/session.py +12 -2
- klaude_code/session/templates/export_session.html +12 -12
- klaude_code/ui/modes/repl/completers.py +1 -1
- klaude_code/ui/modes/repl/event_handler.py +32 -2
- klaude_code/ui/modes/repl/renderer.py +8 -6
- klaude_code/ui/renderers/developer.py +18 -7
- klaude_code/ui/renderers/metadata.py +24 -12
- klaude_code/ui/renderers/sub_agent.py +59 -3
- klaude_code/ui/renderers/thinking.py +1 -1
- klaude_code/ui/renderers/tools.py +22 -29
- klaude_code/ui/rich/markdown.py +20 -48
- klaude_code/ui/rich/status.py +32 -14
- klaude_code/ui/rich/theme.py +8 -7
- {klaude_code-1.2.15.dist-info → klaude_code-1.2.16.dist-info}/METADATA +3 -2
- {klaude_code-1.2.15.dist-info → klaude_code-1.2.16.dist-info}/RECORD +52 -46
- klaude_code/protocol/sub_agent.py +0 -354
- /klaude_code/core/prompts/{prompt-subagent-webfetch.md → prompt-sub-agent-webfetch.md} +0 -0
- /klaude_code/core/prompts/{prompt-subagent.md → prompt-sub-agent.md} +0 -0
- {klaude_code-1.2.15.dist-info → klaude_code-1.2.16.dist-info}/WHEEL +0 -0
- {klaude_code-1.2.15.dist-info → klaude_code-1.2.16.dist-info}/entry_points.txt +0 -0
klaude_code/cli/main.py
CHANGED
|
@@ -13,7 +13,7 @@ from klaude_code.cli.debug import DEBUG_FILTER_HELP, open_log_file_in_editor, re
|
|
|
13
13
|
from klaude_code.cli.session_cmd import register_session_commands
|
|
14
14
|
from klaude_code.config import load_config
|
|
15
15
|
from klaude_code.session import Session, resume_select_session
|
|
16
|
-
from klaude_code.trace import prepare_debug_log_file
|
|
16
|
+
from klaude_code.trace import DebugType, prepare_debug_log_file
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
def set_terminal_title(title: str) -> None:
|
|
@@ -22,6 +22,63 @@ def set_terminal_title(title: str) -> None:
|
|
|
22
22
|
sys.stdout.flush()
|
|
23
23
|
|
|
24
24
|
|
|
25
|
+
def setup_terminal_title() -> None:
|
|
26
|
+
"""Set terminal title to current folder name."""
|
|
27
|
+
folder_name = os.path.basename(os.getcwd())
|
|
28
|
+
set_terminal_title(f"{folder_name}: klaude")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def prepare_debug_logging(debug: bool, debug_filter: str | None) -> tuple[bool, set[DebugType] | None, Path | None]:
|
|
32
|
+
"""Resolve debug settings and prepare log file if debugging is enabled.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
A tuple of (debug_enabled, debug_filters, log_path).
|
|
36
|
+
log_path is None if debugging is disabled.
|
|
37
|
+
"""
|
|
38
|
+
debug_enabled, debug_filters = resolve_debug_settings(debug, debug_filter)
|
|
39
|
+
log_path: Path | None = None
|
|
40
|
+
if debug_enabled:
|
|
41
|
+
log_path = prepare_debug_log_file()
|
|
42
|
+
return debug_enabled, debug_filters, log_path
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def read_input_content(cli_argument: str) -> str | None:
|
|
46
|
+
"""Read and merge input from stdin and CLI argument.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
cli_argument: The input content passed as CLI argument.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
The merged input content, or None if no input was provided.
|
|
53
|
+
"""
|
|
54
|
+
from klaude_code.trace import log
|
|
55
|
+
|
|
56
|
+
parts: list[str] = []
|
|
57
|
+
|
|
58
|
+
# Handle stdin input
|
|
59
|
+
if not sys.stdin.isatty():
|
|
60
|
+
try:
|
|
61
|
+
stdin = sys.stdin.read().rstrip("\n")
|
|
62
|
+
if stdin:
|
|
63
|
+
parts.append(stdin)
|
|
64
|
+
except (OSError, ValueError) as e:
|
|
65
|
+
# Expected I/O-related errors when reading from stdin (e.g. broken pipe, closed stream).
|
|
66
|
+
log((f"Error reading from stdin: {e}", "red"))
|
|
67
|
+
except Exception as e:
|
|
68
|
+
# Unexpected errors are still reported but kept from crashing the CLI.
|
|
69
|
+
log((f"Unexpected error reading from stdin: {e}", "red"))
|
|
70
|
+
|
|
71
|
+
if cli_argument:
|
|
72
|
+
parts.append(cli_argument)
|
|
73
|
+
|
|
74
|
+
content = "\n".join(parts)
|
|
75
|
+
if len(content) == 0:
|
|
76
|
+
log(("Error: No input content provided", "red"))
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
return content
|
|
80
|
+
|
|
81
|
+
|
|
25
82
|
def _version_callback(value: bool) -> None:
|
|
26
83
|
"""Show version and exit."""
|
|
27
84
|
if value:
|
|
@@ -90,33 +147,10 @@ def exec_command(
|
|
|
90
147
|
),
|
|
91
148
|
) -> None:
|
|
92
149
|
"""Execute non-interactively with provided input."""
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
# Set terminal title with current folder name
|
|
96
|
-
folder_name = os.path.basename(os.getcwd())
|
|
97
|
-
set_terminal_title(f"{folder_name}: klaude")
|
|
98
|
-
|
|
99
|
-
parts: list[str] = []
|
|
150
|
+
setup_terminal_title()
|
|
100
151
|
|
|
101
|
-
|
|
102
|
-
if
|
|
103
|
-
try:
|
|
104
|
-
stdin = sys.stdin.read().rstrip("\n")
|
|
105
|
-
if stdin:
|
|
106
|
-
parts.append(stdin)
|
|
107
|
-
except (OSError, ValueError) as e:
|
|
108
|
-
# Expected I/O-related errors when reading from stdin (e.g. broken pipe, closed stream).
|
|
109
|
-
log((f"Error reading from stdin: {e}", "red"))
|
|
110
|
-
except Exception as e:
|
|
111
|
-
# Unexpected errors are still reported but kept from crashing the CLI.
|
|
112
|
-
log((f"Unexpected error reading from stdin: {e}", "red"))
|
|
113
|
-
|
|
114
|
-
if input_content:
|
|
115
|
-
parts.append(input_content)
|
|
116
|
-
|
|
117
|
-
input_content = "\n".join(parts)
|
|
118
|
-
if len(input_content) == 0:
|
|
119
|
-
log(("Error: No input content provided", "red"))
|
|
152
|
+
merged_input = read_input_content(input_content)
|
|
153
|
+
if merged_input is None:
|
|
120
154
|
raise typer.Exit(1)
|
|
121
155
|
|
|
122
156
|
from klaude_code.cli.runtime import AppInitConfig, run_exec
|
|
@@ -133,11 +167,7 @@ def exec_command(
|
|
|
133
167
|
if chosen_model is None:
|
|
134
168
|
return
|
|
135
169
|
|
|
136
|
-
debug_enabled, debug_filters =
|
|
137
|
-
|
|
138
|
-
log_path: Path | None = None
|
|
139
|
-
if debug_enabled:
|
|
140
|
-
log_path = prepare_debug_log_file()
|
|
170
|
+
debug_enabled, debug_filters, log_path = prepare_debug_logging(debug, debug_filter)
|
|
141
171
|
|
|
142
172
|
init_config = AppInitConfig(
|
|
143
173
|
model=chosen_model,
|
|
@@ -154,7 +184,7 @@ def exec_command(
|
|
|
154
184
|
asyncio.run(
|
|
155
185
|
run_exec(
|
|
156
186
|
init_config=init_config,
|
|
157
|
-
input_content=
|
|
187
|
+
input_content=merged_input,
|
|
158
188
|
)
|
|
159
189
|
)
|
|
160
190
|
|
|
@@ -210,10 +240,8 @@ def main_callback(
|
|
|
210
240
|
from klaude_code.cli.runtime import AppInitConfig, run_interactive
|
|
211
241
|
from klaude_code.config.select_model import select_model_from_config
|
|
212
242
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
set_terminal_title(f"{folder_name}: klaude")
|
|
216
|
-
# Interactive mode
|
|
243
|
+
setup_terminal_title()
|
|
244
|
+
|
|
217
245
|
chosen_model = model
|
|
218
246
|
if select_model:
|
|
219
247
|
chosen_model = select_model_from_config(preferred=model)
|
|
@@ -232,11 +260,7 @@ def main_callback(
|
|
|
232
260
|
session_id = Session.most_recent_session_id()
|
|
233
261
|
# If still no session_id, leave as None to create a new session
|
|
234
262
|
|
|
235
|
-
debug_enabled, debug_filters =
|
|
236
|
-
|
|
237
|
-
log_path: Path | None = None
|
|
238
|
-
if debug_enabled:
|
|
239
|
-
log_path = prepare_debug_log_file()
|
|
263
|
+
debug_enabled, debug_filters, log_path = prepare_debug_logging(debug, debug_filter)
|
|
240
264
|
|
|
241
265
|
init_config = AppInitConfig(
|
|
242
266
|
model=chosen_model,
|
klaude_code/cli/runtime.py
CHANGED
|
@@ -127,6 +127,28 @@ async def initialize_app_components(init_config: AppInitConfig) -> AppComponents
|
|
|
127
127
|
)
|
|
128
128
|
|
|
129
129
|
|
|
130
|
+
async def initialize_session(
|
|
131
|
+
executor: Executor,
|
|
132
|
+
event_queue: asyncio.Queue[events.Event],
|
|
133
|
+
session_id: str | None = None,
|
|
134
|
+
) -> str | None:
|
|
135
|
+
"""Initialize a session and return the active session ID.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
executor: The executor to submit operations to.
|
|
139
|
+
event_queue: The event queue for synchronization.
|
|
140
|
+
session_id: Optional session ID to resume. If None, creates a new session.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
The active session ID, or None if no session is active.
|
|
144
|
+
"""
|
|
145
|
+
await executor.submit_and_wait(op.InitAgentOperation(session_id=session_id))
|
|
146
|
+
await event_queue.join()
|
|
147
|
+
|
|
148
|
+
active_session_ids = executor.context.agent_manager.active_session_ids()
|
|
149
|
+
return active_session_ids[0] if active_session_ids else session_id
|
|
150
|
+
|
|
151
|
+
|
|
130
152
|
async def cleanup_app_components(components: AppComponents) -> None:
|
|
131
153
|
"""Clean up all application components."""
|
|
132
154
|
try:
|
|
@@ -167,13 +189,7 @@ async def run_exec(init_config: AppInitConfig, input_content: str) -> None:
|
|
|
167
189
|
components = await initialize_app_components(init_config)
|
|
168
190
|
|
|
169
191
|
try:
|
|
170
|
-
|
|
171
|
-
await components.executor.submit_and_wait(op.InitAgentOperation())
|
|
172
|
-
await components.event_queue.join()
|
|
173
|
-
|
|
174
|
-
# Get the session_id from the newly created agent
|
|
175
|
-
session_ids = components.executor.context.agent_manager.active_session_ids()
|
|
176
|
-
session_id = session_ids[0] if session_ids else None
|
|
192
|
+
session_id = await initialize_session(components.executor, components.event_queue)
|
|
177
193
|
|
|
178
194
|
# Submit the input content directly
|
|
179
195
|
await components.executor.submit_and_wait(
|
|
@@ -245,12 +261,7 @@ async def run_interactive(init_config: AppInitConfig, session_id: str | None = N
|
|
|
245
261
|
restore_sigint = install_sigint_double_press_exit(_show_toast_once, _hide_progress)
|
|
246
262
|
|
|
247
263
|
try:
|
|
248
|
-
await components.executor.
|
|
249
|
-
await components.event_queue.join()
|
|
250
|
-
|
|
251
|
-
# Get the actual session_id (may have been auto-generated if None was passed)
|
|
252
|
-
active_session_ids = components.executor.context.agent_manager.active_session_ids()
|
|
253
|
-
active_session_id = active_session_ids[0] if active_session_ids else session_id
|
|
264
|
+
active_session_id = await initialize_session(components.executor, components.event_queue, session_id=session_id)
|
|
254
265
|
|
|
255
266
|
# Input
|
|
256
267
|
await input_provider.start()
|
|
@@ -25,7 +25,7 @@ class ExportCommand(CommandABC):
|
|
|
25
25
|
|
|
26
26
|
@property
|
|
27
27
|
def support_addition_params(self) -> bool:
|
|
28
|
-
return
|
|
28
|
+
return False
|
|
29
29
|
|
|
30
30
|
@property
|
|
31
31
|
def is_interactive(self) -> bool:
|
|
@@ -33,7 +33,7 @@ class ExportCommand(CommandABC):
|
|
|
33
33
|
|
|
34
34
|
async def run(self, raw: str, agent: Agent) -> CommandResult:
|
|
35
35
|
try:
|
|
36
|
-
output_path = self._resolve_output_path(
|
|
36
|
+
output_path = self._resolve_output_path("", agent)
|
|
37
37
|
html_doc = self._build_html(agent)
|
|
38
38
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
39
39
|
output_path.write_text(html_doc, encoding="utf-8")
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Write a HANDOFF.md for another agent to continue the conversation
|
|
3
|
+
from: amp-cli https://ampcode.com/manual#handoff
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Write a HANDOFF.md file in the current working directory for another agent to continue this conversation.
|
|
7
|
+
|
|
8
|
+
Extract relevant context from the conversation above to facilitate continuing this work. Write from my perspective (first person: "I did...", "I told you...").
|
|
9
|
+
|
|
10
|
+
Consider what would be useful to know based on my request below. Relevant questions include:
|
|
11
|
+
- What did I just do or implement?
|
|
12
|
+
- What instructions did I give you that are still relevant (e.g., follow patterns in the codebase)?
|
|
13
|
+
- Did I provide a plan or spec that should be included?
|
|
14
|
+
- What important information did I share (certain libraries, patterns, constraints, preferences)?
|
|
15
|
+
- What key technical details did I discover (APIs, methods, patterns)?
|
|
16
|
+
- What caveats, limitations, or open questions remain?
|
|
17
|
+
|
|
18
|
+
Extract only what matters for the specific request below. Skip irrelevant questions. Choose an appropriate length based on the complexity of the request.
|
|
19
|
+
|
|
20
|
+
Focus on capabilities and behavior, not file-by-file changes. Avoid excessive implementation details (variable names, storage keys, constants) unless critical.
|
|
21
|
+
|
|
22
|
+
Format: Plain text with bullets. No markdown headers, no bold/italic, no code fences. Use workspace-relative paths for files.
|
|
23
|
+
|
|
24
|
+
List file or directory paths (workspace-relative) relevant to accomplishing the goal in the following format:
|
|
25
|
+
<example>
|
|
26
|
+
@src/project/main.py
|
|
27
|
+
@src/project/llm/
|
|
28
|
+
</example>
|
|
29
|
+
|
|
30
|
+
My request:
|
|
31
|
+
$ARGUMENTS
|
|
32
|
+
|
|
33
|
+
<system>If the request section is empty, ask for clarification about the goal</system>
|
|
@@ -211,7 +211,11 @@ class ThinkingCommand(CommandABC):
|
|
|
211
211
|
content=f"Thinking changed: {current} -> {new_status}",
|
|
212
212
|
command_output=model.CommandOutput(command_name=self.name),
|
|
213
213
|
),
|
|
214
|
-
)
|
|
214
|
+
),
|
|
215
|
+
events.WelcomeEvent(
|
|
216
|
+
work_dir=str(agent.session.work_dir),
|
|
217
|
+
llm_config=config,
|
|
218
|
+
),
|
|
215
219
|
]
|
|
216
220
|
)
|
|
217
221
|
|
klaude_code/config/config.py
CHANGED
|
@@ -23,20 +23,20 @@ class Config(BaseModel):
|
|
|
23
23
|
provider_list: list[llm_param.LLMConfigProviderParameter]
|
|
24
24
|
model_list: list[ModelConfig]
|
|
25
25
|
main_model: str
|
|
26
|
-
|
|
26
|
+
sub_agent_models: dict[str, str] = Field(default_factory=dict)
|
|
27
27
|
theme: str | None = None
|
|
28
28
|
|
|
29
29
|
@model_validator(mode="before")
|
|
30
30
|
@classmethod
|
|
31
|
-
def
|
|
32
|
-
raw_val: Any = data.get("
|
|
31
|
+
def _normalize_sub_agent_models(cls, data: dict[str, Any]) -> dict[str, Any]:
|
|
32
|
+
raw_val: Any = data.get("sub_agent_models") or {}
|
|
33
33
|
raw_models: dict[str, Any] = cast(dict[str, Any], raw_val) if isinstance(raw_val, dict) else {}
|
|
34
34
|
normalized: dict[str, str] = {}
|
|
35
35
|
key_map = {p.name.lower(): p.name for p in iter_sub_agent_profiles()}
|
|
36
36
|
for key, value in dict(raw_models).items():
|
|
37
37
|
canonical = key_map.get(str(key).lower(), str(key))
|
|
38
38
|
normalized[canonical] = str(value)
|
|
39
|
-
data["
|
|
39
|
+
data["sub_agent_models"] = normalized
|
|
40
40
|
return data
|
|
41
41
|
|
|
42
42
|
def get_main_model_config(self) -> llm_param.LLMConfigParameter:
|
|
@@ -80,7 +80,7 @@ class Config(BaseModel):
|
|
|
80
80
|
def get_example_config() -> Config:
|
|
81
81
|
return Config(
|
|
82
82
|
main_model="gpt-5.1",
|
|
83
|
-
|
|
83
|
+
sub_agent_models={"explore": "haiku", "oracle": "gpt-5.1-high"},
|
|
84
84
|
provider_list=[
|
|
85
85
|
llm_param.LLMConfigProviderParameter(
|
|
86
86
|
provider_name="openai",
|
klaude_code/config/list_model.py
CHANGED
|
@@ -198,7 +198,7 @@ def display_models_and_providers(config: Config):
|
|
|
198
198
|
)
|
|
199
199
|
|
|
200
200
|
for profile in iter_sub_agent_profiles():
|
|
201
|
-
sub_model_name = config.
|
|
201
|
+
sub_model_name = config.sub_agent_models.get(profile.name)
|
|
202
202
|
if not sub_model_name:
|
|
203
203
|
continue
|
|
204
204
|
console.print(
|
klaude_code/const/__init__.py
CHANGED
|
@@ -105,6 +105,9 @@ UI_REFRESH_RATE_FPS = 20
|
|
|
105
105
|
# Number of lines to keep visible at bottom of markdown streaming window
|
|
106
106
|
MARKDOWN_STREAM_LIVE_WINDOW = 20
|
|
107
107
|
|
|
108
|
+
# Status hint text shown after spinner status
|
|
109
|
+
STATUS_HINT_TEXT = " (esc to interrupt)"
|
|
110
|
+
|
|
108
111
|
# Status shimmer animation
|
|
109
112
|
# Horizontal padding used when computing shimmer band position
|
|
110
113
|
STATUS_SHIMMER_PADDING = 10
|
klaude_code/core/executor.py
CHANGED
|
@@ -96,7 +96,7 @@ class ExecutorContext:
|
|
|
96
96
|
# Delegate responsibilities to helper components
|
|
97
97
|
self.agent_manager = AgentManager(event_queue, llm_clients, resolved_profile_provider)
|
|
98
98
|
self.task_manager = TaskManager()
|
|
99
|
-
self.
|
|
99
|
+
self.sub_agent_manager = SubAgentManager(event_queue, llm_clients, resolved_profile_provider)
|
|
100
100
|
|
|
101
101
|
async def emit_event(self, event: events.Event) -> None:
|
|
102
102
|
"""Emit an event to the UI display system."""
|
|
@@ -240,7 +240,7 @@ class ExecutorContext:
|
|
|
240
240
|
|
|
241
241
|
# Inject subtask runner into tool context for nested Task tool usage
|
|
242
242
|
async def _runner(state: model.SubAgentState) -> SubAgentResult:
|
|
243
|
-
return await self.
|
|
243
|
+
return await self.sub_agent_manager.run_sub_agent(agent, state)
|
|
244
244
|
|
|
245
245
|
token = current_run_subtask_callback.set(_runner)
|
|
246
246
|
try:
|
|
@@ -32,7 +32,7 @@ def build_llm_clients(
|
|
|
32
32
|
sub_clients: dict[SubAgentType, LLMClientABC] = {}
|
|
33
33
|
|
|
34
34
|
for profile in iter_sub_agent_profiles():
|
|
35
|
-
model_name = config.
|
|
35
|
+
model_name = config.sub_agent_models.get(profile.name)
|
|
36
36
|
if not model_name:
|
|
37
37
|
continue
|
|
38
38
|
|
|
@@ -9,8 +9,9 @@ from __future__ import annotations
|
|
|
9
9
|
|
|
10
10
|
import asyncio
|
|
11
11
|
|
|
12
|
-
from klaude_code.core.agent import Agent, ModelProfileProvider
|
|
12
|
+
from klaude_code.core.agent import Agent, AgentProfile, ModelProfileProvider
|
|
13
13
|
from klaude_code.core.manager.llm_clients import LLMClients
|
|
14
|
+
from klaude_code.core.tool import ReportBackTool
|
|
14
15
|
from klaude_code.protocol import events, model
|
|
15
16
|
from klaude_code.protocol.sub_agent import SubAgentResult
|
|
16
17
|
from klaude_code.session.session import Session
|
|
@@ -35,7 +36,7 @@ class SubAgentManager:
|
|
|
35
36
|
|
|
36
37
|
await self._event_queue.put(event)
|
|
37
38
|
|
|
38
|
-
async def
|
|
39
|
+
async def run_sub_agent(self, parent_agent: Agent, state: model.SubAgentState) -> SubAgentResult:
|
|
39
40
|
"""Run a nested sub-agent task and return its result."""
|
|
40
41
|
|
|
41
42
|
# Create a child session under the same workdir
|
|
@@ -47,6 +48,25 @@ class SubAgentManager:
|
|
|
47
48
|
self._llm_clients.get_client(state.sub_agent_type),
|
|
48
49
|
state.sub_agent_type,
|
|
49
50
|
)
|
|
51
|
+
|
|
52
|
+
# Inject report_back tool if output_schema is provided
|
|
53
|
+
if state.output_schema:
|
|
54
|
+
report_back_tool_class = ReportBackTool.for_schema(state.output_schema)
|
|
55
|
+
report_back_prompt = """\
|
|
56
|
+
|
|
57
|
+
# Structured Output
|
|
58
|
+
You have a `report_back` tool available. When you complete the task,\
|
|
59
|
+
you MUST call `report_back` with the structured result matching the required schema.\
|
|
60
|
+
This will end the task and return the structured data to the caller.
|
|
61
|
+
"""
|
|
62
|
+
base_prompt = child_profile.system_prompt or ""
|
|
63
|
+
child_profile = AgentProfile(
|
|
64
|
+
llm_client=child_profile.llm_client,
|
|
65
|
+
system_prompt=base_prompt + report_back_prompt,
|
|
66
|
+
tools=[*child_profile.tools, report_back_tool_class.schema()],
|
|
67
|
+
reminders=child_profile.reminders,
|
|
68
|
+
)
|
|
69
|
+
|
|
50
70
|
child_agent = Agent(session=child_session, profile=child_profile)
|
|
51
71
|
|
|
52
72
|
log_debug(
|
|
@@ -68,23 +88,27 @@ class SubAgentManager:
|
|
|
68
88
|
elif isinstance(event, events.TaskMetadataEvent):
|
|
69
89
|
task_metadata = event.metadata.main
|
|
70
90
|
await self.emit_event(event)
|
|
71
|
-
return SubAgentResult(
|
|
91
|
+
return SubAgentResult(
|
|
92
|
+
task_result=result,
|
|
93
|
+
session_id=child_session.id,
|
|
94
|
+
task_metadata=task_metadata,
|
|
95
|
+
)
|
|
72
96
|
except asyncio.CancelledError:
|
|
73
97
|
# Propagate cancellation so tooling can treat it as user interrupt
|
|
74
98
|
log_debug(
|
|
75
|
-
f"
|
|
99
|
+
f"Sub-agent task for {state.sub_agent_type} was cancelled",
|
|
76
100
|
style="yellow",
|
|
77
101
|
debug_type=DebugType.EXECUTION,
|
|
78
102
|
)
|
|
79
103
|
raise
|
|
80
104
|
except Exception as exc: # pragma: no cover - defensive logging
|
|
81
105
|
log_debug(
|
|
82
|
-
f"
|
|
106
|
+
f"Sub-agent task failed: [{exc.__class__.__name__}] {exc!s}",
|
|
83
107
|
style="red",
|
|
84
108
|
debug_type=DebugType.EXECUTION,
|
|
85
109
|
)
|
|
86
110
|
return SubAgentResult(
|
|
87
|
-
task_result=f"
|
|
111
|
+
task_result=f"Sub-agent task failed: [{exc.__class__.__name__}] {exc!s}",
|
|
88
112
|
session_id="",
|
|
89
113
|
error=True,
|
|
90
114
|
)
|
klaude_code/core/prompt.py
CHANGED
|
@@ -5,6 +5,7 @@ from importlib.resources import files
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
|
|
7
7
|
from klaude_code.protocol import llm_param
|
|
8
|
+
from klaude_code.protocol.sub_agent import get_sub_agent_profile
|
|
8
9
|
|
|
9
10
|
COMMAND_DESCRIPTIONS: dict[str, str] = {
|
|
10
11
|
"rg": "ripgrep - fast text search",
|
|
@@ -19,15 +20,15 @@ PROMPT_FILES: dict[str, str] = {
|
|
|
19
20
|
"main_gpt_5_1_codex_max": "prompts/prompt-codex-gpt-5-1-codex-max.md",
|
|
20
21
|
"main": "prompts/prompt-claude-code.md",
|
|
21
22
|
"main_gemini": "prompts/prompt-gemini.md", # https://ai.google.dev/gemini-api/docs/prompting-strategies?hl=zh-cn#agentic-si-template
|
|
22
|
-
# Sub-agent prompts keyed by their name
|
|
23
|
-
"Task": "prompts/prompt-subagent.md",
|
|
24
|
-
"Oracle": "prompts/prompt-subagent-oracle.md",
|
|
25
|
-
"Explore": "prompts/prompt-subagent-explore.md",
|
|
26
|
-
"WebFetchAgent": "prompts/prompt-subagent-webfetch.md",
|
|
27
23
|
}
|
|
28
24
|
|
|
29
25
|
|
|
30
26
|
@cache
|
|
27
|
+
def _load_prompt_by_path(prompt_path: str) -> str:
|
|
28
|
+
"""Load and cache prompt content from a file path relative to core package."""
|
|
29
|
+
return files(__package__).joinpath(prompt_path).read_text(encoding="utf-8").strip()
|
|
30
|
+
|
|
31
|
+
|
|
31
32
|
def _load_base_prompt(file_key: str) -> str:
|
|
32
33
|
"""Load and cache the base prompt content from file."""
|
|
33
34
|
try:
|
|
@@ -35,14 +36,11 @@ def _load_base_prompt(file_key: str) -> str:
|
|
|
35
36
|
except KeyError as exc:
|
|
36
37
|
raise ValueError(f"Unknown prompt key: {file_key}") from exc
|
|
37
38
|
|
|
38
|
-
return
|
|
39
|
+
return _load_prompt_by_path(prompt_path)
|
|
39
40
|
|
|
40
41
|
|
|
41
|
-
def _get_file_key(model_name: str, protocol: llm_param.LLMClientProtocol
|
|
42
|
-
"""Determine which prompt file to use based on model
|
|
43
|
-
if sub_agent_type is not None:
|
|
44
|
-
return sub_agent_type
|
|
45
|
-
|
|
42
|
+
def _get_file_key(model_name: str, protocol: llm_param.LLMClientProtocol) -> str:
|
|
43
|
+
"""Determine which prompt file to use based on model."""
|
|
46
44
|
match model_name:
|
|
47
45
|
case name if "gpt-5.1-codex-max" in name:
|
|
48
46
|
return "main_gpt_5_1_codex_max"
|
|
@@ -90,8 +88,12 @@ def load_system_prompt(
|
|
|
90
88
|
model_name: str, protocol: llm_param.LLMClientProtocol, sub_agent_type: str | None = None
|
|
91
89
|
) -> str:
|
|
92
90
|
"""Get system prompt content for the given model and sub-agent type."""
|
|
93
|
-
|
|
94
|
-
|
|
91
|
+
if sub_agent_type is not None:
|
|
92
|
+
profile = get_sub_agent_profile(sub_agent_type)
|
|
93
|
+
base_prompt = _load_prompt_by_path(profile.prompt_file)
|
|
94
|
+
else:
|
|
95
|
+
file_key = _get_file_key(model_name, protocol)
|
|
96
|
+
base_prompt = _load_base_prompt(file_key)
|
|
95
97
|
|
|
96
98
|
if protocol == llm_param.LLMClientProtocol.CODEX:
|
|
97
99
|
# Do not append environment info for Codex protocol
|
|
@@ -22,7 +22,6 @@ Guidelines:
|
|
|
22
22
|
|
|
23
23
|
Complete the user's search request efficiently and report your findings clearly.
|
|
24
24
|
|
|
25
|
-
|
|
26
25
|
Notes:
|
|
27
26
|
- Agent threads always have their cwd reset between bash calls, as a result please only use absolute file paths.
|
|
28
27
|
- In your final response always share relevant file names and code snippets. Any file paths you return in your response MUST be absolute. Do NOT use relative paths.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
You are the Oracle - an expert AI advisor with advanced reasoning capabilities
|
|
2
2
|
|
|
3
3
|
Your role is to provide high-quality technical guidance, code reviews, architectural advice, and strategic planning for software engineering tasks.
|
|
4
|
-
You are running inside an AI coding system in which you act as a
|
|
4
|
+
You are running inside an AI coding system in which you act as a sub-agent that's used when the main agent needs a smarter, more capable model to help out.
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
Key responsibilities:
|
klaude_code/core/reminders.py
CHANGED
|
@@ -14,7 +14,8 @@ from klaude_code.session import Session
|
|
|
14
14
|
type Reminder = Callable[[Session], Awaitable[model.DeveloperMessageItem | None]]
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
# Match @ preceded by whitespace, start of line, or → (ReadTool line number arrow)
|
|
18
|
+
AT_FILE_PATTERN = re.compile(r'(?:(?<!\S)|(?<=\u2192))@("(?P<quoted>[^\"]+)"|(?P<plain>\S+))')
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
def get_last_new_user_input(session: Session) -> str | None:
|
|
@@ -31,10 +32,74 @@ def get_last_new_user_input(session: Session) -> str | None:
|
|
|
31
32
|
return "\n\n".join(result)
|
|
32
33
|
|
|
33
34
|
|
|
35
|
+
async def _load_at_file_recursive(
|
|
36
|
+
session: Session,
|
|
37
|
+
pattern: str,
|
|
38
|
+
at_files: dict[str, model.AtPatternParseResult],
|
|
39
|
+
collected_images: list[model.ImageURLPart],
|
|
40
|
+
visited: set[str],
|
|
41
|
+
base_dir: Path | None = None,
|
|
42
|
+
mentioned_in: str | None = None,
|
|
43
|
+
) -> None:
|
|
44
|
+
"""Recursively load @ file references."""
|
|
45
|
+
path = (base_dir / pattern).resolve() if base_dir else Path(pattern).resolve()
|
|
46
|
+
path_str = str(path)
|
|
47
|
+
|
|
48
|
+
if path_str in visited:
|
|
49
|
+
return
|
|
50
|
+
visited.add(path_str)
|
|
51
|
+
|
|
52
|
+
context_token = set_tool_context_from_session(session)
|
|
53
|
+
try:
|
|
54
|
+
if path.exists() and path.is_file():
|
|
55
|
+
args = ReadTool.ReadArguments(file_path=path_str)
|
|
56
|
+
tool_result = await ReadTool.call_with_args(args)
|
|
57
|
+
at_files[path_str] = model.AtPatternParseResult(
|
|
58
|
+
path=path_str,
|
|
59
|
+
tool_name=tools.READ,
|
|
60
|
+
result=tool_result.output or "",
|
|
61
|
+
tool_args=args.model_dump_json(exclude_none=True),
|
|
62
|
+
operation="Read",
|
|
63
|
+
images=tool_result.images,
|
|
64
|
+
mentioned_in=mentioned_in,
|
|
65
|
+
)
|
|
66
|
+
if tool_result.images:
|
|
67
|
+
collected_images.extend(tool_result.images)
|
|
68
|
+
|
|
69
|
+
# Recursively parse @ references from ReadTool output
|
|
70
|
+
output = tool_result.output or ""
|
|
71
|
+
if "@" in output:
|
|
72
|
+
for match in AT_FILE_PATTERN.finditer(output):
|
|
73
|
+
nested = match.group("quoted") or match.group("plain")
|
|
74
|
+
if nested:
|
|
75
|
+
await _load_at_file_recursive(
|
|
76
|
+
session,
|
|
77
|
+
nested,
|
|
78
|
+
at_files,
|
|
79
|
+
collected_images,
|
|
80
|
+
visited,
|
|
81
|
+
base_dir=path.parent,
|
|
82
|
+
mentioned_in=path_str,
|
|
83
|
+
)
|
|
84
|
+
elif path.exists() and path.is_dir():
|
|
85
|
+
quoted_path = shlex.quote(path_str)
|
|
86
|
+
args = BashTool.BashArguments(command=f"ls {quoted_path}")
|
|
87
|
+
tool_result = await BashTool.call_with_args(args)
|
|
88
|
+
at_files[path_str] = model.AtPatternParseResult(
|
|
89
|
+
path=path_str + "/",
|
|
90
|
+
tool_name=tools.BASH,
|
|
91
|
+
result=tool_result.output or "",
|
|
92
|
+
tool_args=args.model_dump_json(exclude_none=True),
|
|
93
|
+
operation="List",
|
|
94
|
+
)
|
|
95
|
+
finally:
|
|
96
|
+
reset_tool_context(context_token)
|
|
97
|
+
|
|
98
|
+
|
|
34
99
|
async def at_file_reader_reminder(
|
|
35
100
|
session: Session,
|
|
36
101
|
) -> model.DeveloperMessageItem | None:
|
|
37
|
-
"""Parse @foo/bar to read"""
|
|
102
|
+
"""Parse @foo/bar to read, with recursive loading of nested @ references"""
|
|
38
103
|
last_user_input = get_last_new_user_input(session)
|
|
39
104
|
if not last_user_input or "@" not in last_user_input:
|
|
40
105
|
return None
|
|
@@ -53,38 +118,16 @@ async def at_file_reader_reminder(
|
|
|
53
118
|
|
|
54
119
|
at_files: dict[str, model.AtPatternParseResult] = {} # path -> content
|
|
55
120
|
collected_images: list[model.ImageURLPart] = []
|
|
121
|
+
visited: set[str] = set()
|
|
56
122
|
|
|
57
123
|
for pattern in at_patterns:
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
path=str(path),
|
|
66
|
-
tool_name=tools.READ,
|
|
67
|
-
result=tool_result.output or "",
|
|
68
|
-
tool_args=args.model_dump_json(exclude_none=True),
|
|
69
|
-
operation="Read",
|
|
70
|
-
images=tool_result.images,
|
|
71
|
-
)
|
|
72
|
-
at_files[str(path)] = at_result
|
|
73
|
-
if tool_result.images:
|
|
74
|
-
collected_images.extend(tool_result.images)
|
|
75
|
-
elif path.exists() and path.is_dir():
|
|
76
|
-
quoted_path = shlex.quote(str(path))
|
|
77
|
-
args = BashTool.BashArguments(command=f"ls {quoted_path}")
|
|
78
|
-
tool_result = await BashTool.call_with_args(args)
|
|
79
|
-
at_files[str(path)] = model.AtPatternParseResult(
|
|
80
|
-
path=str(path) + "/",
|
|
81
|
-
tool_name=tools.BASH,
|
|
82
|
-
result=tool_result.output or "",
|
|
83
|
-
tool_args=args.model_dump_json(exclude_none=True),
|
|
84
|
-
operation="List",
|
|
85
|
-
)
|
|
86
|
-
finally:
|
|
87
|
-
reset_tool_context(context_token)
|
|
124
|
+
await _load_at_file_recursive(
|
|
125
|
+
session,
|
|
126
|
+
pattern,
|
|
127
|
+
at_files,
|
|
128
|
+
collected_images,
|
|
129
|
+
visited,
|
|
130
|
+
)
|
|
88
131
|
|
|
89
132
|
if len(at_files) == 0:
|
|
90
133
|
return None
|