klaude-code 1.2.6__py3-none-any.whl → 1.8.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/auth/__init__.py +24 -0
- klaude_code/auth/codex/__init__.py +20 -0
- klaude_code/auth/codex/exceptions.py +17 -0
- klaude_code/auth/codex/jwt_utils.py +45 -0
- klaude_code/auth/codex/oauth.py +229 -0
- klaude_code/auth/codex/token_manager.py +84 -0
- klaude_code/cli/auth_cmd.py +73 -0
- klaude_code/cli/config_cmd.py +91 -0
- klaude_code/cli/cost_cmd.py +338 -0
- klaude_code/cli/debug.py +78 -0
- klaude_code/cli/list_model.py +307 -0
- klaude_code/cli/main.py +233 -134
- klaude_code/cli/runtime.py +309 -117
- klaude_code/{version.py → cli/self_update.py} +114 -5
- klaude_code/cli/session_cmd.py +37 -21
- klaude_code/command/__init__.py +88 -27
- klaude_code/command/clear_cmd.py +8 -7
- klaude_code/command/command_abc.py +31 -31
- klaude_code/command/debug_cmd.py +79 -0
- klaude_code/command/export_cmd.py +19 -53
- klaude_code/command/export_online_cmd.py +154 -0
- klaude_code/command/fork_session_cmd.py +267 -0
- klaude_code/command/help_cmd.py +7 -8
- klaude_code/command/model_cmd.py +60 -10
- klaude_code/command/model_select.py +84 -0
- klaude_code/command/prompt-jj-describe.md +32 -0
- klaude_code/command/prompt_command.py +19 -11
- klaude_code/command/refresh_cmd.py +8 -10
- klaude_code/command/registry.py +139 -40
- klaude_code/command/release_notes_cmd.py +84 -0
- klaude_code/command/resume_cmd.py +111 -0
- klaude_code/command/status_cmd.py +104 -60
- klaude_code/command/terminal_setup_cmd.py +7 -9
- klaude_code/command/thinking_cmd.py +98 -0
- klaude_code/config/__init__.py +14 -6
- klaude_code/config/assets/__init__.py +1 -0
- klaude_code/config/assets/builtin_config.yaml +303 -0
- klaude_code/config/builtin_config.py +38 -0
- klaude_code/config/config.py +378 -109
- klaude_code/config/select_model.py +117 -53
- klaude_code/config/thinking.py +269 -0
- klaude_code/{const/__init__.py → const.py} +50 -19
- klaude_code/core/agent.py +20 -28
- klaude_code/core/executor.py +327 -112
- klaude_code/core/manager/__init__.py +2 -4
- klaude_code/core/manager/llm_clients.py +1 -15
- klaude_code/core/manager/llm_clients_builder.py +10 -11
- klaude_code/core/manager/sub_agent_manager.py +37 -6
- klaude_code/core/prompt.py +63 -44
- klaude_code/core/prompts/prompt-claude-code.md +2 -13
- klaude_code/core/prompts/prompt-codex-gpt-5-1-codex-max.md +117 -0
- klaude_code/core/prompts/prompt-codex-gpt-5-2-codex.md +117 -0
- klaude_code/core/prompts/prompt-codex.md +9 -42
- klaude_code/core/prompts/prompt-minimal.md +12 -0
- klaude_code/core/prompts/{prompt-subagent-explore.md → prompt-sub-agent-explore.md} +16 -3
- klaude_code/core/prompts/{prompt-subagent-oracle.md → prompt-sub-agent-oracle.md} +1 -2
- klaude_code/core/prompts/prompt-sub-agent-web.md +51 -0
- klaude_code/core/reminders.py +283 -95
- klaude_code/core/task.py +113 -75
- klaude_code/core/tool/__init__.py +24 -31
- klaude_code/core/tool/file/_utils.py +36 -0
- klaude_code/core/tool/file/apply_patch.py +17 -25
- klaude_code/core/tool/file/apply_patch_tool.py +57 -77
- klaude_code/core/tool/file/diff_builder.py +151 -0
- klaude_code/core/tool/file/edit_tool.py +50 -63
- klaude_code/core/tool/file/move_tool.md +41 -0
- klaude_code/core/tool/file/move_tool.py +435 -0
- klaude_code/core/tool/file/read_tool.md +1 -1
- klaude_code/core/tool/file/read_tool.py +86 -86
- klaude_code/core/tool/file/write_tool.py +59 -69
- klaude_code/core/tool/report_back_tool.py +84 -0
- klaude_code/core/tool/shell/bash_tool.py +265 -22
- klaude_code/core/tool/shell/command_safety.py +3 -6
- klaude_code/core/tool/{memory → skill}/skill_tool.py +16 -26
- klaude_code/core/tool/sub_agent_tool.py +13 -2
- klaude_code/core/tool/todo/todo_write_tool.md +0 -157
- klaude_code/core/tool/todo/todo_write_tool.py +1 -1
- klaude_code/core/tool/todo/todo_write_tool_raw.md +182 -0
- klaude_code/core/tool/todo/update_plan_tool.py +1 -1
- klaude_code/core/tool/tool_abc.py +18 -0
- klaude_code/core/tool/tool_context.py +27 -12
- klaude_code/core/tool/tool_registry.py +7 -7
- klaude_code/core/tool/tool_runner.py +44 -36
- klaude_code/core/tool/truncation.py +29 -14
- klaude_code/core/tool/web/mermaid_tool.md +43 -0
- klaude_code/core/tool/web/mermaid_tool.py +2 -5
- klaude_code/core/tool/web/web_fetch_tool.md +1 -1
- klaude_code/core/tool/web/web_fetch_tool.py +112 -22
- klaude_code/core/tool/web/web_search_tool.md +23 -0
- klaude_code/core/tool/web/web_search_tool.py +130 -0
- klaude_code/core/turn.py +168 -66
- klaude_code/llm/__init__.py +2 -10
- klaude_code/llm/anthropic/client.py +190 -178
- klaude_code/llm/anthropic/input.py +39 -15
- klaude_code/llm/bedrock/__init__.py +3 -0
- klaude_code/llm/bedrock/client.py +60 -0
- klaude_code/llm/client.py +7 -21
- klaude_code/llm/codex/__init__.py +5 -0
- klaude_code/llm/codex/client.py +149 -0
- klaude_code/llm/google/__init__.py +3 -0
- klaude_code/llm/google/client.py +309 -0
- klaude_code/llm/google/input.py +215 -0
- klaude_code/llm/input_common.py +3 -9
- klaude_code/llm/openai_compatible/client.py +72 -164
- klaude_code/llm/openai_compatible/input.py +6 -4
- klaude_code/llm/openai_compatible/stream.py +273 -0
- klaude_code/llm/openai_compatible/tool_call_accumulator.py +17 -1
- klaude_code/llm/openrouter/client.py +89 -160
- klaude_code/llm/openrouter/input.py +18 -30
- klaude_code/llm/openrouter/reasoning.py +118 -0
- klaude_code/llm/registry.py +39 -7
- klaude_code/llm/responses/client.py +184 -171
- klaude_code/llm/responses/input.py +20 -1
- klaude_code/llm/usage.py +17 -12
- klaude_code/protocol/commands.py +17 -1
- klaude_code/protocol/events.py +31 -4
- klaude_code/protocol/llm_param.py +13 -10
- klaude_code/protocol/model.py +232 -29
- klaude_code/protocol/op.py +90 -1
- klaude_code/protocol/op_handler.py +35 -1
- 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.py +79 -0
- klaude_code/protocol/tools.py +4 -2
- klaude_code/session/__init__.py +2 -2
- klaude_code/session/codec.py +71 -0
- klaude_code/session/export.py +293 -86
- klaude_code/session/selector.py +89 -67
- klaude_code/session/session.py +320 -309
- klaude_code/session/store.py +220 -0
- klaude_code/session/templates/export_session.html +595 -83
- klaude_code/session/templates/mermaid_viewer.html +926 -0
- klaude_code/skill/__init__.py +27 -0
- klaude_code/skill/assets/deslop/SKILL.md +17 -0
- klaude_code/skill/assets/dev-docs/SKILL.md +108 -0
- klaude_code/skill/assets/handoff/SKILL.md +39 -0
- klaude_code/skill/assets/jj-workspace/SKILL.md +20 -0
- klaude_code/skill/assets/skill-creator/SKILL.md +139 -0
- klaude_code/{core/tool/memory/skill_loader.py → skill/loader.py} +55 -15
- klaude_code/skill/manager.py +70 -0
- klaude_code/skill/system_skills.py +192 -0
- klaude_code/trace/__init__.py +20 -2
- klaude_code/trace/log.py +150 -5
- klaude_code/ui/__init__.py +4 -9
- klaude_code/ui/core/input.py +1 -1
- klaude_code/ui/core/stage_manager.py +7 -7
- klaude_code/ui/modes/debug/display.py +2 -1
- klaude_code/ui/modes/repl/__init__.py +3 -48
- klaude_code/ui/modes/repl/clipboard.py +5 -5
- klaude_code/ui/modes/repl/completers.py +487 -123
- klaude_code/ui/modes/repl/display.py +5 -4
- klaude_code/ui/modes/repl/event_handler.py +370 -117
- klaude_code/ui/modes/repl/input_prompt_toolkit.py +552 -105
- klaude_code/ui/modes/repl/key_bindings.py +146 -23
- klaude_code/ui/modes/repl/renderer.py +189 -99
- klaude_code/ui/renderers/assistant.py +9 -2
- klaude_code/ui/renderers/bash_syntax.py +178 -0
- klaude_code/ui/renderers/common.py +78 -0
- klaude_code/ui/renderers/developer.py +104 -48
- klaude_code/ui/renderers/diffs.py +87 -6
- klaude_code/ui/renderers/errors.py +11 -6
- klaude_code/ui/renderers/mermaid_viewer.py +57 -0
- klaude_code/ui/renderers/metadata.py +112 -76
- klaude_code/ui/renderers/sub_agent.py +92 -7
- klaude_code/ui/renderers/thinking.py +40 -18
- klaude_code/ui/renderers/tools.py +405 -227
- klaude_code/ui/renderers/user_input.py +73 -13
- klaude_code/ui/rich/__init__.py +10 -1
- klaude_code/ui/rich/cjk_wrap.py +228 -0
- klaude_code/ui/rich/code_panel.py +131 -0
- klaude_code/ui/rich/live.py +17 -0
- klaude_code/ui/rich/markdown.py +305 -170
- klaude_code/ui/rich/searchable_text.py +10 -13
- klaude_code/ui/rich/status.py +190 -49
- klaude_code/ui/rich/theme.py +135 -39
- klaude_code/ui/terminal/__init__.py +55 -0
- klaude_code/ui/terminal/color.py +1 -1
- klaude_code/ui/terminal/control.py +13 -22
- klaude_code/ui/terminal/notifier.py +44 -4
- klaude_code/ui/terminal/selector.py +658 -0
- klaude_code/ui/utils/common.py +0 -18
- klaude_code-1.8.0.dist-info/METADATA +377 -0
- klaude_code-1.8.0.dist-info/RECORD +219 -0
- {klaude_code-1.2.6.dist-info → klaude_code-1.8.0.dist-info}/entry_points.txt +1 -0
- klaude_code/command/diff_cmd.py +0 -138
- klaude_code/command/prompt-dev-docs-update.md +0 -56
- klaude_code/command/prompt-dev-docs.md +0 -46
- klaude_code/config/list_model.py +0 -162
- klaude_code/core/manager/agent_manager.py +0 -127
- klaude_code/core/prompts/prompt-subagent-webfetch.md +0 -46
- klaude_code/core/tool/file/multi_edit_tool.md +0 -42
- klaude_code/core/tool/file/multi_edit_tool.py +0 -199
- klaude_code/core/tool/memory/memory_tool.md +0 -16
- klaude_code/core/tool/memory/memory_tool.py +0 -462
- klaude_code/llm/openrouter/reasoning_handler.py +0 -209
- klaude_code/protocol/sub_agent.py +0 -348
- klaude_code/ui/utils/debouncer.py +0 -42
- klaude_code-1.2.6.dist-info/METADATA +0 -178
- klaude_code-1.2.6.dist-info/RECORD +0 -167
- /klaude_code/core/prompts/{prompt-subagent.md → prompt-sub-agent.md} +0 -0
- /klaude_code/core/tool/{memory → skill}/__init__.py +0 -0
- /klaude_code/core/tool/{memory → skill}/skill_tool.md +0 -0
- {klaude_code-1.2.6.dist-info → klaude_code-1.8.0.dist-info}/WHEEL +0 -0
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
from enum import Enum
|
|
2
|
-
|
|
3
|
-
from pydantic import BaseModel
|
|
4
|
-
|
|
5
|
-
from klaude_code.protocol import model
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class ReasoningDetail(BaseModel):
|
|
9
|
-
"""OpenRouter's https://openrouter.ai/docs/use-cases/reasoning-tokens#reasoning_details-array-structure"""
|
|
10
|
-
|
|
11
|
-
type: str
|
|
12
|
-
format: str
|
|
13
|
-
index: int
|
|
14
|
-
id: str | None = None
|
|
15
|
-
data: str | None = None # OpenAI's encrypted content
|
|
16
|
-
summary: str | None = None
|
|
17
|
-
text: str | None = None
|
|
18
|
-
signature: str | None = None # Claude's signature
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class ReasoningMode(str, Enum):
|
|
22
|
-
COMPLETE_CHUNK = "complete_chunk"
|
|
23
|
-
GPT5_SECTIONS = "gpt5_sections"
|
|
24
|
-
ACCUMULATE = "accumulate"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
class ReasoningStreamHandler:
|
|
28
|
-
"""Encapsulates reasoning stream handling across different model behaviors."""
|
|
29
|
-
|
|
30
|
-
def __init__(
|
|
31
|
-
self,
|
|
32
|
-
param_model: str,
|
|
33
|
-
response_id: str | None,
|
|
34
|
-
) -> None:
|
|
35
|
-
self._param_model = param_model
|
|
36
|
-
self._response_id = response_id
|
|
37
|
-
|
|
38
|
-
self._reasoning_id: str | None = None
|
|
39
|
-
self._accumulated_reasoning: list[str] = []
|
|
40
|
-
self._gpt5_line_buffer: str = ""
|
|
41
|
-
self._gpt5_section_lines: list[str] = []
|
|
42
|
-
|
|
43
|
-
def set_response_id(self, response_id: str | None) -> None:
|
|
44
|
-
"""Update the response identifier used for emitted items."""
|
|
45
|
-
|
|
46
|
-
self._response_id = response_id
|
|
47
|
-
|
|
48
|
-
def on_detail(self, detail: ReasoningDetail) -> list[model.ConversationItem]:
|
|
49
|
-
"""Process a single reasoning detail and return streamable items."""
|
|
50
|
-
|
|
51
|
-
items: list[model.ConversationItem] = []
|
|
52
|
-
|
|
53
|
-
if detail.type == "reasoning.encrypted":
|
|
54
|
-
self._reasoning_id = detail.id
|
|
55
|
-
if encrypted_item := self._build_encrypted_item(detail.data, detail):
|
|
56
|
-
items.append(encrypted_item)
|
|
57
|
-
return items
|
|
58
|
-
|
|
59
|
-
if detail.type in ("reasoning.text", "reasoning.summary"):
|
|
60
|
-
self._reasoning_id = detail.id
|
|
61
|
-
if encrypted_item := self._build_encrypted_item(detail.signature, detail):
|
|
62
|
-
items.append(encrypted_item)
|
|
63
|
-
text = detail.text if detail.type == "reasoning.text" else detail.summary
|
|
64
|
-
if text:
|
|
65
|
-
items.extend(self._handle_text(text))
|
|
66
|
-
|
|
67
|
-
return items
|
|
68
|
-
|
|
69
|
-
def flush(self) -> list[model.ConversationItem]:
|
|
70
|
-
"""Flush buffered reasoning text and encrypted payloads."""
|
|
71
|
-
|
|
72
|
-
items: list[model.ConversationItem] = []
|
|
73
|
-
mode = self._resolve_mode()
|
|
74
|
-
|
|
75
|
-
if mode is ReasoningMode.GPT5_SECTIONS:
|
|
76
|
-
for section in self._drain_gpt5_sections():
|
|
77
|
-
items.append(self._build_text_item(section))
|
|
78
|
-
elif self._accumulated_reasoning and mode is ReasoningMode.ACCUMULATE:
|
|
79
|
-
items.append(self._build_text_item("".join(self._accumulated_reasoning)))
|
|
80
|
-
self._accumulated_reasoning = []
|
|
81
|
-
|
|
82
|
-
return items
|
|
83
|
-
|
|
84
|
-
def _handle_text(self, text: str) -> list[model.ReasoningTextItem]:
|
|
85
|
-
mode = self._resolve_mode()
|
|
86
|
-
if mode is ReasoningMode.COMPLETE_CHUNK:
|
|
87
|
-
return [self._build_text_item(text)]
|
|
88
|
-
if mode is ReasoningMode.GPT5_SECTIONS:
|
|
89
|
-
sections = self._process_gpt5_text(text)
|
|
90
|
-
return [self._build_text_item(section) for section in sections]
|
|
91
|
-
self._accumulated_reasoning.append(text)
|
|
92
|
-
return []
|
|
93
|
-
|
|
94
|
-
def _build_text_item(self, content: str) -> model.ReasoningTextItem:
|
|
95
|
-
return model.ReasoningTextItem(
|
|
96
|
-
id=self._reasoning_id,
|
|
97
|
-
content=content,
|
|
98
|
-
response_id=self._response_id,
|
|
99
|
-
model=self._param_model,
|
|
100
|
-
)
|
|
101
|
-
|
|
102
|
-
def _build_encrypted_item(
|
|
103
|
-
self,
|
|
104
|
-
content: str | None,
|
|
105
|
-
detail: ReasoningDetail,
|
|
106
|
-
) -> model.ReasoningEncryptedItem | None:
|
|
107
|
-
if not content:
|
|
108
|
-
return None
|
|
109
|
-
return model.ReasoningEncryptedItem(
|
|
110
|
-
id=detail.id,
|
|
111
|
-
encrypted_content=content,
|
|
112
|
-
format=detail.format,
|
|
113
|
-
response_id=self._response_id,
|
|
114
|
-
model=self._param_model,
|
|
115
|
-
)
|
|
116
|
-
|
|
117
|
-
def _process_gpt5_text(self, text: str) -> list[str]:
|
|
118
|
-
emitted_sections: list[str] = []
|
|
119
|
-
self._gpt5_line_buffer += text
|
|
120
|
-
while True:
|
|
121
|
-
newline_index = self._gpt5_line_buffer.find("\n")
|
|
122
|
-
if newline_index == -1:
|
|
123
|
-
break
|
|
124
|
-
line = self._gpt5_line_buffer[:newline_index]
|
|
125
|
-
self._gpt5_line_buffer = self._gpt5_line_buffer[newline_index + 1 :]
|
|
126
|
-
remainder = line
|
|
127
|
-
while True:
|
|
128
|
-
split_result = self._split_gpt5_title_line(remainder)
|
|
129
|
-
if split_result is None:
|
|
130
|
-
break
|
|
131
|
-
prefix_segment, title_segment, remainder = split_result
|
|
132
|
-
if prefix_segment:
|
|
133
|
-
if not self._gpt5_section_lines:
|
|
134
|
-
self._gpt5_section_lines = []
|
|
135
|
-
self._gpt5_section_lines.append(f"{prefix_segment}\n")
|
|
136
|
-
if self._gpt5_section_lines:
|
|
137
|
-
emitted_sections.append("".join(self._gpt5_section_lines))
|
|
138
|
-
self._gpt5_section_lines = [f"{title_segment} \n"] # Add two spaces for markdown line break
|
|
139
|
-
if remainder:
|
|
140
|
-
if not self._gpt5_section_lines:
|
|
141
|
-
self._gpt5_section_lines = []
|
|
142
|
-
self._gpt5_section_lines.append(f"{remainder}\n")
|
|
143
|
-
return emitted_sections
|
|
144
|
-
|
|
145
|
-
def _drain_gpt5_sections(self) -> list[str]:
|
|
146
|
-
sections: list[str] = []
|
|
147
|
-
if self._gpt5_line_buffer:
|
|
148
|
-
if not self._gpt5_section_lines:
|
|
149
|
-
self._gpt5_section_lines = [self._gpt5_line_buffer]
|
|
150
|
-
else:
|
|
151
|
-
self._gpt5_section_lines.append(self._gpt5_line_buffer)
|
|
152
|
-
self._gpt5_line_buffer = ""
|
|
153
|
-
if self._gpt5_section_lines:
|
|
154
|
-
sections.append("".join(self._gpt5_section_lines))
|
|
155
|
-
self._gpt5_section_lines = []
|
|
156
|
-
return sections
|
|
157
|
-
|
|
158
|
-
def _is_gpt5(self) -> bool:
|
|
159
|
-
return "gpt-5" in self._param_model.lower()
|
|
160
|
-
|
|
161
|
-
def _is_complete_chunk_reasoning_model(self) -> bool:
|
|
162
|
-
"""Whether the current model emits reasoning in complete chunks (e.g. Gemini)."""
|
|
163
|
-
|
|
164
|
-
return self._param_model.startswith("google/gemini")
|
|
165
|
-
|
|
166
|
-
def _resolve_mode(self) -> ReasoningMode:
|
|
167
|
-
if self._is_complete_chunk_reasoning_model():
|
|
168
|
-
return ReasoningMode.COMPLETE_CHUNK
|
|
169
|
-
if self._is_gpt5():
|
|
170
|
-
return ReasoningMode.GPT5_SECTIONS
|
|
171
|
-
return ReasoningMode.ACCUMULATE
|
|
172
|
-
|
|
173
|
-
def _is_gpt5_title_line(self, line: str) -> bool:
|
|
174
|
-
stripped = line.strip()
|
|
175
|
-
if not stripped:
|
|
176
|
-
return False
|
|
177
|
-
return stripped.startswith("**") and stripped.endswith("**") and stripped.count("**") >= 2
|
|
178
|
-
|
|
179
|
-
def _split_gpt5_title_line(self, line: str) -> tuple[str | None, str, str] | None:
|
|
180
|
-
if not line:
|
|
181
|
-
return None
|
|
182
|
-
search_start = 0
|
|
183
|
-
while True:
|
|
184
|
-
opening_index = line.find("**", search_start)
|
|
185
|
-
if opening_index == -1:
|
|
186
|
-
return None
|
|
187
|
-
closing_index = line.find("**", opening_index + 2)
|
|
188
|
-
if closing_index == -1:
|
|
189
|
-
return None
|
|
190
|
-
title_candidate = line[opening_index : closing_index + 2]
|
|
191
|
-
stripped_title = title_candidate.strip()
|
|
192
|
-
if self._is_gpt5_title_line(stripped_title):
|
|
193
|
-
# Treat as a GPT-5 title only when everything after the
|
|
194
|
-
# bold segment is either whitespace or starts a new bold
|
|
195
|
-
# title. This prevents inline bold like `**xxx**yyyy`
|
|
196
|
-
# from being misclassified as a section title while
|
|
197
|
-
# preserving support for consecutive titles in one line.
|
|
198
|
-
after = line[closing_index + 2 :]
|
|
199
|
-
if after.strip() and not after.lstrip().startswith("**"):
|
|
200
|
-
search_start = closing_index + 2
|
|
201
|
-
continue
|
|
202
|
-
prefix_segment = line[:opening_index]
|
|
203
|
-
remainder_segment = after
|
|
204
|
-
return (
|
|
205
|
-
prefix_segment if prefix_segment else None,
|
|
206
|
-
stripped_title,
|
|
207
|
-
remainder_segment,
|
|
208
|
-
)
|
|
209
|
-
search_start = closing_index + 2
|
|
@@ -1,348 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from dataclasses import dataclass, field
|
|
4
|
-
from typing import Any, Callable
|
|
5
|
-
|
|
6
|
-
from klaude_code.protocol import tools
|
|
7
|
-
|
|
8
|
-
AvailabilityPredicate = Callable[[str], bool]
|
|
9
|
-
PromptBuilder = Callable[[dict[str, Any]], str]
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
@dataclass
|
|
13
|
-
class SubAgentResult:
|
|
14
|
-
task_result: str
|
|
15
|
-
session_id: str
|
|
16
|
-
error: bool = False
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def _default_prompt_builder(args: dict[str, Any]) -> str:
|
|
20
|
-
"""Default prompt builder that just returns the 'prompt' field."""
|
|
21
|
-
return args.get("prompt", "")
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
@dataclass(frozen=True)
|
|
25
|
-
class SubAgentProfile:
|
|
26
|
-
"""Metadata describing a sub agent and how it integrates with the system.
|
|
27
|
-
|
|
28
|
-
This dataclass contains all the information needed to:
|
|
29
|
-
1. Register the sub agent with the system
|
|
30
|
-
2. Generate the tool schema for the main agent
|
|
31
|
-
3. Build the prompt for the sub agent
|
|
32
|
-
"""
|
|
33
|
-
|
|
34
|
-
# Identity - single name used for type, tool_name, config_key, and prompt_key
|
|
35
|
-
name: str # e.g., "Task", "Oracle", "Explore"
|
|
36
|
-
|
|
37
|
-
# Tool schema
|
|
38
|
-
description: str # Tool description shown to the main agent
|
|
39
|
-
parameters: dict[str, Any] = field(
|
|
40
|
-
default_factory=lambda: dict[str, Any](), hash=False
|
|
41
|
-
) # JSON Schema for tool parameters
|
|
42
|
-
|
|
43
|
-
# Sub agent configuration
|
|
44
|
-
tool_set: tuple[str, ...] = () # Tools available to this sub agent
|
|
45
|
-
prompt_builder: PromptBuilder = _default_prompt_builder # Builds the sub agent prompt from tool arguments
|
|
46
|
-
|
|
47
|
-
# UI display
|
|
48
|
-
active_form: str = "" # Active form for spinner status (e.g., "Tasking", "Exploring")
|
|
49
|
-
|
|
50
|
-
# Availability
|
|
51
|
-
enabled_by_default: bool = True
|
|
52
|
-
show_in_main_agent: bool = True
|
|
53
|
-
target_model_filter: AvailabilityPredicate | None = None
|
|
54
|
-
|
|
55
|
-
def enabled_for_model(self, model_name: str | None) -> bool:
|
|
56
|
-
if not self.enabled_by_default:
|
|
57
|
-
return False
|
|
58
|
-
if model_name is None or self.target_model_filter is None:
|
|
59
|
-
return True
|
|
60
|
-
return self.target_model_filter(model_name)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
_PROFILES: dict[str, SubAgentProfile] = {}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def register_sub_agent(profile: SubAgentProfile) -> None:
|
|
67
|
-
if profile.name in _PROFILES:
|
|
68
|
-
raise ValueError(f"Duplicate sub agent profile: {profile.name}")
|
|
69
|
-
_PROFILES[profile.name] = profile
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
def get_sub_agent_profile(sub_agent_type: tools.SubAgentType) -> SubAgentProfile:
|
|
73
|
-
try:
|
|
74
|
-
return _PROFILES[sub_agent_type]
|
|
75
|
-
except KeyError as exc:
|
|
76
|
-
raise KeyError(f"Unknown sub agent type: {sub_agent_type}") from exc
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def iter_sub_agent_profiles(enabled_only: bool = False, model_name: str | None = None) -> list[SubAgentProfile]:
|
|
80
|
-
profiles = list(_PROFILES.values())
|
|
81
|
-
if not enabled_only:
|
|
82
|
-
return profiles
|
|
83
|
-
return [p for p in profiles if p.enabled_for_model(model_name)]
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def get_sub_agent_profile_by_tool(tool_name: str) -> SubAgentProfile | None:
|
|
87
|
-
return _PROFILES.get(tool_name)
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
def is_sub_agent_tool(tool_name: str) -> bool:
|
|
91
|
-
return tool_name in _PROFILES
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
def sub_agent_tool_names(enabled_only: bool = False, model_name: str | None = None) -> list[str]:
|
|
95
|
-
return [
|
|
96
|
-
profile.name
|
|
97
|
-
for profile in iter_sub_agent_profiles(enabled_only=enabled_only, model_name=model_name)
|
|
98
|
-
if profile.show_in_main_agent
|
|
99
|
-
]
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
# -----------------------------------------------------------------------------
|
|
103
|
-
# Sub Agent Definitions
|
|
104
|
-
# -----------------------------------------------------------------------------
|
|
105
|
-
|
|
106
|
-
TASK_DESCRIPTION = """\
|
|
107
|
-
Launch a new agent to handle complex, multi-step tasks autonomously. \
|
|
108
|
-
|
|
109
|
-
When NOT to use the Task tool:
|
|
110
|
-
- If you want to read a specific file path, use the Read or Bash tool for `rg` instead of the Task tool, to find the match more quickly
|
|
111
|
-
- If you are searching for a specific class definition like "class Foo", use the Bash tool for `rg` instead, to find the match more quickly
|
|
112
|
-
- If you are searching for code within a specific file or set of 2-3 files, use the Read tool instead of the Task tool, to find the match more quickly
|
|
113
|
-
- Other tasks that are not related to the agent descriptions above
|
|
114
|
-
|
|
115
|
-
Usage notes:
|
|
116
|
-
- Launch multiple agents concurrently whenever possible, to maximize performance; to do that, use a single message with multiple tool uses
|
|
117
|
-
- When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result.
|
|
118
|
-
- Each agent invocation is stateless. You will not be able to send additional messages to the agent, nor will the agent be able to communicate with you outside of its final report. Therefore, your prompt should contain a highly detailed task description for the agent to perform autonomously and you should specify exactly what information the agent should return back to you in its final and only message to you.
|
|
119
|
-
- The agent's outputs should generally be trusted
|
|
120
|
-
- Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, etc.), since it is not aware of the user's intent
|
|
121
|
-
- If the agent description mentions that it should be used proactively, then you should try your best to use it without the user having to ask for it first. Use your judgement.
|
|
122
|
-
- If the user specifies that they want you to run agents "in parallel", you MUST send a single message with multiple Task tool use content blocks. For example, if you need to launch both a code-reviewer agent and a test-runner agent in parallel, send a single message with both tool calls.\
|
|
123
|
-
"""
|
|
124
|
-
|
|
125
|
-
TASK_PARAMETERS = {
|
|
126
|
-
"type": "object",
|
|
127
|
-
"properties": {
|
|
128
|
-
"description": {
|
|
129
|
-
"type": "string",
|
|
130
|
-
"description": "A short (3-5 word) description of the task",
|
|
131
|
-
},
|
|
132
|
-
"prompt": {
|
|
133
|
-
"type": "string",
|
|
134
|
-
"description": "The task for the agent to perform",
|
|
135
|
-
},
|
|
136
|
-
},
|
|
137
|
-
"required": ["description", "prompt"],
|
|
138
|
-
"additionalProperties": False,
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
register_sub_agent(
|
|
142
|
-
SubAgentProfile(
|
|
143
|
-
name="Task",
|
|
144
|
-
description=TASK_DESCRIPTION,
|
|
145
|
-
parameters=TASK_PARAMETERS,
|
|
146
|
-
tool_set=(tools.BASH, tools.READ, tools.EDIT, tools.WRITE),
|
|
147
|
-
active_form="Tasking",
|
|
148
|
-
)
|
|
149
|
-
)
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
# -----------------------------------------------------------------------------
|
|
153
|
-
# Oracle Sub Agent
|
|
154
|
-
# -----------------------------------------------------------------------------
|
|
155
|
-
|
|
156
|
-
ORACLE_DESCRIPTION = """\
|
|
157
|
-
Consult the Oracle - an AI advisor powered by OpenAI's premium reasoning model that can plan, review, and provide expert guidance.
|
|
158
|
-
|
|
159
|
-
The Oracle has access to the following tools: Read, Bash.
|
|
160
|
-
|
|
161
|
-
The Oracle acts as your senior engineering advisor and can help with:
|
|
162
|
-
|
|
163
|
-
WHEN TO USE THE ORACLE:
|
|
164
|
-
- Code reviews and architecture feedback
|
|
165
|
-
- Finding a bug in multiple files
|
|
166
|
-
- Planning complex implementations or refactoring
|
|
167
|
-
- Analyzing code quality and suggesting improvements
|
|
168
|
-
- Answering complex technical questions that require deep reasoning
|
|
169
|
-
|
|
170
|
-
WHEN NOT TO USE THE ORACLE:
|
|
171
|
-
- Simple file reading or searching tasks (use Read or Grep directly)
|
|
172
|
-
- Codebase searches (use Task)
|
|
173
|
-
- Basic code modifications and when you need to execute code changes (do it yourself or use Task)
|
|
174
|
-
|
|
175
|
-
USAGE GUIDELINES:
|
|
176
|
-
1. Be specific about what you want the Oracle to review, plan, or debug
|
|
177
|
-
2. Provide relevant context about what you're trying to achieve. If you know that any files are involved, list them and they will be attached.
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
EXAMPLES:
|
|
181
|
-
- "Review the authentication system architecture and suggest improvements"
|
|
182
|
-
- "Plan the implementation of real-time collaboration features"
|
|
183
|
-
- "Analyze the performance bottlenecks in the data processing pipeline"
|
|
184
|
-
- "Review this API design and suggest better patterns"\
|
|
185
|
-
"""
|
|
186
|
-
|
|
187
|
-
ORACLE_PARAMETERS = {
|
|
188
|
-
"properties": {
|
|
189
|
-
"context": {
|
|
190
|
-
"description": "Optional context about the current situation, what you've tried, or background information that would help the Oracle provide better guidance.",
|
|
191
|
-
"type": "string",
|
|
192
|
-
},
|
|
193
|
-
"files": {
|
|
194
|
-
"description": "Optional list of specific file paths (text files, images) that the Oracle should examine as part of its analysis. These files will be attached to the Oracle input.",
|
|
195
|
-
"items": {"type": "string"},
|
|
196
|
-
"type": "array",
|
|
197
|
-
},
|
|
198
|
-
"task": {
|
|
199
|
-
"description": "The task or question you want the Oracle to help with. Be specific about what kind of guidance, review, or planning you need.",
|
|
200
|
-
"type": "string",
|
|
201
|
-
},
|
|
202
|
-
"description": {
|
|
203
|
-
"description": "A short (3-5 word) description of the task",
|
|
204
|
-
"type": "string",
|
|
205
|
-
},
|
|
206
|
-
},
|
|
207
|
-
"required": ["task", "description"],
|
|
208
|
-
"type": "object",
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
def _oracle_prompt_builder(args: dict[str, Any]) -> str:
|
|
213
|
-
"""Build the Oracle prompt from tool arguments."""
|
|
214
|
-
context = args.get("context", "")
|
|
215
|
-
task = args.get("task", "")
|
|
216
|
-
files = args.get("files", [])
|
|
217
|
-
|
|
218
|
-
prompt = f"""Context: {context}
|
|
219
|
-
|
|
220
|
-
Task: {task}
|
|
221
|
-
"""
|
|
222
|
-
if files:
|
|
223
|
-
files_str = "\n".join(f"@{file}" for file in files)
|
|
224
|
-
prompt += f"\nRelated files to review:\n{files_str}"
|
|
225
|
-
return prompt
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
register_sub_agent(
|
|
229
|
-
SubAgentProfile(
|
|
230
|
-
name="Oracle",
|
|
231
|
-
description=ORACLE_DESCRIPTION,
|
|
232
|
-
parameters=ORACLE_PARAMETERS,
|
|
233
|
-
tool_set=(tools.READ, tools.BASH),
|
|
234
|
-
prompt_builder=_oracle_prompt_builder,
|
|
235
|
-
active_form="Consulting Oracle",
|
|
236
|
-
target_model_filter=lambda model: ("gpt-5" not in model) and ("gemini-3" not in model),
|
|
237
|
-
)
|
|
238
|
-
)
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
# -----------------------------------------------------------------------------
|
|
242
|
-
# Explore Sub Agent
|
|
243
|
-
# -----------------------------------------------------------------------------
|
|
244
|
-
|
|
245
|
-
EXPLORE_DESCRIPTION = """\
|
|
246
|
-
Spin up a fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (eg. "src/components/**/*.tsx"), \
|
|
247
|
-
search code for keywords (eg. "API endpoints"), or answer questions about the codebase (eg. "how do API endpoints work?")\
|
|
248
|
-
When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "very thorough" for comprehensive analysis across multiple locations and naming conventions\
|
|
249
|
-
"""
|
|
250
|
-
|
|
251
|
-
EXPLORE_PARAMETERS = {
|
|
252
|
-
"type": "object",
|
|
253
|
-
"properties": {
|
|
254
|
-
"description": {
|
|
255
|
-
"type": "string",
|
|
256
|
-
"description": "Short (3-5 words) label for the exploration goal",
|
|
257
|
-
},
|
|
258
|
-
"prompt": {
|
|
259
|
-
"type": "string",
|
|
260
|
-
"description": "The task for the agent to perform",
|
|
261
|
-
},
|
|
262
|
-
"thoroughness": {
|
|
263
|
-
"type": "string",
|
|
264
|
-
"enum": ["quick", "medium", "very thorough"],
|
|
265
|
-
"description": "Controls how deep the sub-agent should search the repo",
|
|
266
|
-
},
|
|
267
|
-
},
|
|
268
|
-
"required": ["description", "prompt"],
|
|
269
|
-
"additionalProperties": False,
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
def _explore_prompt_builder(args: dict[str, Any]) -> str:
|
|
274
|
-
"""Build the Explore prompt from tool arguments."""
|
|
275
|
-
prompt = args.get("prompt", "").strip()
|
|
276
|
-
thoroughness = args.get("thoroughness", "medium")
|
|
277
|
-
return f"{prompt}\nthoroughness: {thoroughness}"
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
register_sub_agent(
|
|
281
|
-
SubAgentProfile(
|
|
282
|
-
name="Explore",
|
|
283
|
-
description=EXPLORE_DESCRIPTION,
|
|
284
|
-
parameters=EXPLORE_PARAMETERS,
|
|
285
|
-
tool_set=(tools.BASH, tools.READ),
|
|
286
|
-
prompt_builder=_explore_prompt_builder,
|
|
287
|
-
active_form="Exploring",
|
|
288
|
-
)
|
|
289
|
-
)
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
# -----------------------------------------------------------------------------
|
|
293
|
-
# WebFetchAgent Sub Agent
|
|
294
|
-
# -----------------------------------------------------------------------------
|
|
295
|
-
|
|
296
|
-
WEB_FETCH_AGENT_DESCRIPTION = """\
|
|
297
|
-
Launch a sub-agent to fetch and analyze web content. Use this when you need to:
|
|
298
|
-
- Retrieve and extract information from a webpage
|
|
299
|
-
- Analyze web page content based on specific instructions
|
|
300
|
-
- Get structured data from URLs
|
|
301
|
-
|
|
302
|
-
The agent will fetch the URL content, handle HTML-to-Markdown conversion automatically, \
|
|
303
|
-
and can use tools like rg to search through large responses that were truncated and saved to files.
|
|
304
|
-
|
|
305
|
-
Usage notes:
|
|
306
|
-
- Provide a clear prompt describing what information to extract or analyze
|
|
307
|
-
- The agent will return a summary of the findings
|
|
308
|
-
- For large web pages, the content may be truncated and saved to a file; the agent can search through it\
|
|
309
|
-
"""
|
|
310
|
-
|
|
311
|
-
WEB_FETCH_AGENT_PARAMETERS = {
|
|
312
|
-
"type": "object",
|
|
313
|
-
"properties": {
|
|
314
|
-
"description": {
|
|
315
|
-
"type": "string",
|
|
316
|
-
"description": "A short (3-5 word) description of the task",
|
|
317
|
-
},
|
|
318
|
-
"url": {
|
|
319
|
-
"type": "string",
|
|
320
|
-
"description": "The URL to fetch and analyze",
|
|
321
|
-
},
|
|
322
|
-
"prompt": {
|
|
323
|
-
"type": "string",
|
|
324
|
-
"description": "Instructions for analyzing or extracting content from the web page",
|
|
325
|
-
},
|
|
326
|
-
},
|
|
327
|
-
"required": ["description", "url", "prompt"],
|
|
328
|
-
"additionalProperties": False,
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
def _web_fetch_prompt_builder(args: dict[str, Any]) -> str:
|
|
333
|
-
"""Build the WebFetchAgent prompt from tool arguments."""
|
|
334
|
-
url = args.get("url", "")
|
|
335
|
-
prompt = args.get("prompt", "")
|
|
336
|
-
return f"URL to fetch: {url}\nTask: {prompt}"
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
register_sub_agent(
|
|
340
|
-
SubAgentProfile(
|
|
341
|
-
name="WebFetchAgent",
|
|
342
|
-
description=WEB_FETCH_AGENT_DESCRIPTION,
|
|
343
|
-
parameters=WEB_FETCH_AGENT_PARAMETERS,
|
|
344
|
-
tool_set=(tools.BASH, tools.READ, tools.WEB_FETCH),
|
|
345
|
-
prompt_builder=_web_fetch_prompt_builder,
|
|
346
|
-
active_form="Fetching Web",
|
|
347
|
-
)
|
|
348
|
-
)
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
from typing import Awaitable, Callable, Optional
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
class Debouncer:
|
|
6
|
-
"""Debouncing mechanism"""
|
|
7
|
-
|
|
8
|
-
def __init__(self, interval: float, callback: Callable[[], Awaitable[None]]):
|
|
9
|
-
"""
|
|
10
|
-
Initialize debouncer
|
|
11
|
-
|
|
12
|
-
Args:
|
|
13
|
-
interval: Debounce interval in seconds
|
|
14
|
-
callback: Async callback function to execute after debouncing
|
|
15
|
-
"""
|
|
16
|
-
self.interval = interval
|
|
17
|
-
self.callback = callback
|
|
18
|
-
self._task: Optional[asyncio.Task[None]] = None
|
|
19
|
-
|
|
20
|
-
def cancel(self) -> None:
|
|
21
|
-
"""Cancel current debounce task"""
|
|
22
|
-
if self._task is not None and not self._task.done():
|
|
23
|
-
self._task.cancel()
|
|
24
|
-
self._task = None
|
|
25
|
-
|
|
26
|
-
def schedule(self) -> None:
|
|
27
|
-
"""Schedule debounce task"""
|
|
28
|
-
self.cancel()
|
|
29
|
-
self._task = asyncio.create_task(self._debounced_execute())
|
|
30
|
-
|
|
31
|
-
async def _debounced_execute(self) -> None:
|
|
32
|
-
"""Execute debounced callback function"""
|
|
33
|
-
try:
|
|
34
|
-
await asyncio.sleep(self.interval)
|
|
35
|
-
await self.callback()
|
|
36
|
-
except asyncio.CancelledError:
|
|
37
|
-
return
|
|
38
|
-
|
|
39
|
-
async def flush(self) -> None:
|
|
40
|
-
"""Immediately execute debounce task (without waiting)"""
|
|
41
|
-
self.cancel()
|
|
42
|
-
await self.callback()
|