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.
Files changed (205) hide show
  1. klaude_code/auth/__init__.py +24 -0
  2. klaude_code/auth/codex/__init__.py +20 -0
  3. klaude_code/auth/codex/exceptions.py +17 -0
  4. klaude_code/auth/codex/jwt_utils.py +45 -0
  5. klaude_code/auth/codex/oauth.py +229 -0
  6. klaude_code/auth/codex/token_manager.py +84 -0
  7. klaude_code/cli/auth_cmd.py +73 -0
  8. klaude_code/cli/config_cmd.py +91 -0
  9. klaude_code/cli/cost_cmd.py +338 -0
  10. klaude_code/cli/debug.py +78 -0
  11. klaude_code/cli/list_model.py +307 -0
  12. klaude_code/cli/main.py +233 -134
  13. klaude_code/cli/runtime.py +309 -117
  14. klaude_code/{version.py → cli/self_update.py} +114 -5
  15. klaude_code/cli/session_cmd.py +37 -21
  16. klaude_code/command/__init__.py +88 -27
  17. klaude_code/command/clear_cmd.py +8 -7
  18. klaude_code/command/command_abc.py +31 -31
  19. klaude_code/command/debug_cmd.py +79 -0
  20. klaude_code/command/export_cmd.py +19 -53
  21. klaude_code/command/export_online_cmd.py +154 -0
  22. klaude_code/command/fork_session_cmd.py +267 -0
  23. klaude_code/command/help_cmd.py +7 -8
  24. klaude_code/command/model_cmd.py +60 -10
  25. klaude_code/command/model_select.py +84 -0
  26. klaude_code/command/prompt-jj-describe.md +32 -0
  27. klaude_code/command/prompt_command.py +19 -11
  28. klaude_code/command/refresh_cmd.py +8 -10
  29. klaude_code/command/registry.py +139 -40
  30. klaude_code/command/release_notes_cmd.py +84 -0
  31. klaude_code/command/resume_cmd.py +111 -0
  32. klaude_code/command/status_cmd.py +104 -60
  33. klaude_code/command/terminal_setup_cmd.py +7 -9
  34. klaude_code/command/thinking_cmd.py +98 -0
  35. klaude_code/config/__init__.py +14 -6
  36. klaude_code/config/assets/__init__.py +1 -0
  37. klaude_code/config/assets/builtin_config.yaml +303 -0
  38. klaude_code/config/builtin_config.py +38 -0
  39. klaude_code/config/config.py +378 -109
  40. klaude_code/config/select_model.py +117 -53
  41. klaude_code/config/thinking.py +269 -0
  42. klaude_code/{const/__init__.py → const.py} +50 -19
  43. klaude_code/core/agent.py +20 -28
  44. klaude_code/core/executor.py +327 -112
  45. klaude_code/core/manager/__init__.py +2 -4
  46. klaude_code/core/manager/llm_clients.py +1 -15
  47. klaude_code/core/manager/llm_clients_builder.py +10 -11
  48. klaude_code/core/manager/sub_agent_manager.py +37 -6
  49. klaude_code/core/prompt.py +63 -44
  50. klaude_code/core/prompts/prompt-claude-code.md +2 -13
  51. klaude_code/core/prompts/prompt-codex-gpt-5-1-codex-max.md +117 -0
  52. klaude_code/core/prompts/prompt-codex-gpt-5-2-codex.md +117 -0
  53. klaude_code/core/prompts/prompt-codex.md +9 -42
  54. klaude_code/core/prompts/prompt-minimal.md +12 -0
  55. klaude_code/core/prompts/{prompt-subagent-explore.md → prompt-sub-agent-explore.md} +16 -3
  56. klaude_code/core/prompts/{prompt-subagent-oracle.md → prompt-sub-agent-oracle.md} +1 -2
  57. klaude_code/core/prompts/prompt-sub-agent-web.md +51 -0
  58. klaude_code/core/reminders.py +283 -95
  59. klaude_code/core/task.py +113 -75
  60. klaude_code/core/tool/__init__.py +24 -31
  61. klaude_code/core/tool/file/_utils.py +36 -0
  62. klaude_code/core/tool/file/apply_patch.py +17 -25
  63. klaude_code/core/tool/file/apply_patch_tool.py +57 -77
  64. klaude_code/core/tool/file/diff_builder.py +151 -0
  65. klaude_code/core/tool/file/edit_tool.py +50 -63
  66. klaude_code/core/tool/file/move_tool.md +41 -0
  67. klaude_code/core/tool/file/move_tool.py +435 -0
  68. klaude_code/core/tool/file/read_tool.md +1 -1
  69. klaude_code/core/tool/file/read_tool.py +86 -86
  70. klaude_code/core/tool/file/write_tool.py +59 -69
  71. klaude_code/core/tool/report_back_tool.py +84 -0
  72. klaude_code/core/tool/shell/bash_tool.py +265 -22
  73. klaude_code/core/tool/shell/command_safety.py +3 -6
  74. klaude_code/core/tool/{memory → skill}/skill_tool.py +16 -26
  75. klaude_code/core/tool/sub_agent_tool.py +13 -2
  76. klaude_code/core/tool/todo/todo_write_tool.md +0 -157
  77. klaude_code/core/tool/todo/todo_write_tool.py +1 -1
  78. klaude_code/core/tool/todo/todo_write_tool_raw.md +182 -0
  79. klaude_code/core/tool/todo/update_plan_tool.py +1 -1
  80. klaude_code/core/tool/tool_abc.py +18 -0
  81. klaude_code/core/tool/tool_context.py +27 -12
  82. klaude_code/core/tool/tool_registry.py +7 -7
  83. klaude_code/core/tool/tool_runner.py +44 -36
  84. klaude_code/core/tool/truncation.py +29 -14
  85. klaude_code/core/tool/web/mermaid_tool.md +43 -0
  86. klaude_code/core/tool/web/mermaid_tool.py +2 -5
  87. klaude_code/core/tool/web/web_fetch_tool.md +1 -1
  88. klaude_code/core/tool/web/web_fetch_tool.py +112 -22
  89. klaude_code/core/tool/web/web_search_tool.md +23 -0
  90. klaude_code/core/tool/web/web_search_tool.py +130 -0
  91. klaude_code/core/turn.py +168 -66
  92. klaude_code/llm/__init__.py +2 -10
  93. klaude_code/llm/anthropic/client.py +190 -178
  94. klaude_code/llm/anthropic/input.py +39 -15
  95. klaude_code/llm/bedrock/__init__.py +3 -0
  96. klaude_code/llm/bedrock/client.py +60 -0
  97. klaude_code/llm/client.py +7 -21
  98. klaude_code/llm/codex/__init__.py +5 -0
  99. klaude_code/llm/codex/client.py +149 -0
  100. klaude_code/llm/google/__init__.py +3 -0
  101. klaude_code/llm/google/client.py +309 -0
  102. klaude_code/llm/google/input.py +215 -0
  103. klaude_code/llm/input_common.py +3 -9
  104. klaude_code/llm/openai_compatible/client.py +72 -164
  105. klaude_code/llm/openai_compatible/input.py +6 -4
  106. klaude_code/llm/openai_compatible/stream.py +273 -0
  107. klaude_code/llm/openai_compatible/tool_call_accumulator.py +17 -1
  108. klaude_code/llm/openrouter/client.py +89 -160
  109. klaude_code/llm/openrouter/input.py +18 -30
  110. klaude_code/llm/openrouter/reasoning.py +118 -0
  111. klaude_code/llm/registry.py +39 -7
  112. klaude_code/llm/responses/client.py +184 -171
  113. klaude_code/llm/responses/input.py +20 -1
  114. klaude_code/llm/usage.py +17 -12
  115. klaude_code/protocol/commands.py +17 -1
  116. klaude_code/protocol/events.py +31 -4
  117. klaude_code/protocol/llm_param.py +13 -10
  118. klaude_code/protocol/model.py +232 -29
  119. klaude_code/protocol/op.py +90 -1
  120. klaude_code/protocol/op_handler.py +35 -1
  121. klaude_code/protocol/sub_agent/__init__.py +117 -0
  122. klaude_code/protocol/sub_agent/explore.py +63 -0
  123. klaude_code/protocol/sub_agent/oracle.py +91 -0
  124. klaude_code/protocol/sub_agent/task.py +61 -0
  125. klaude_code/protocol/sub_agent/web.py +79 -0
  126. klaude_code/protocol/tools.py +4 -2
  127. klaude_code/session/__init__.py +2 -2
  128. klaude_code/session/codec.py +71 -0
  129. klaude_code/session/export.py +293 -86
  130. klaude_code/session/selector.py +89 -67
  131. klaude_code/session/session.py +320 -309
  132. klaude_code/session/store.py +220 -0
  133. klaude_code/session/templates/export_session.html +595 -83
  134. klaude_code/session/templates/mermaid_viewer.html +926 -0
  135. klaude_code/skill/__init__.py +27 -0
  136. klaude_code/skill/assets/deslop/SKILL.md +17 -0
  137. klaude_code/skill/assets/dev-docs/SKILL.md +108 -0
  138. klaude_code/skill/assets/handoff/SKILL.md +39 -0
  139. klaude_code/skill/assets/jj-workspace/SKILL.md +20 -0
  140. klaude_code/skill/assets/skill-creator/SKILL.md +139 -0
  141. klaude_code/{core/tool/memory/skill_loader.py → skill/loader.py} +55 -15
  142. klaude_code/skill/manager.py +70 -0
  143. klaude_code/skill/system_skills.py +192 -0
  144. klaude_code/trace/__init__.py +20 -2
  145. klaude_code/trace/log.py +150 -5
  146. klaude_code/ui/__init__.py +4 -9
  147. klaude_code/ui/core/input.py +1 -1
  148. klaude_code/ui/core/stage_manager.py +7 -7
  149. klaude_code/ui/modes/debug/display.py +2 -1
  150. klaude_code/ui/modes/repl/__init__.py +3 -48
  151. klaude_code/ui/modes/repl/clipboard.py +5 -5
  152. klaude_code/ui/modes/repl/completers.py +487 -123
  153. klaude_code/ui/modes/repl/display.py +5 -4
  154. klaude_code/ui/modes/repl/event_handler.py +370 -117
  155. klaude_code/ui/modes/repl/input_prompt_toolkit.py +552 -105
  156. klaude_code/ui/modes/repl/key_bindings.py +146 -23
  157. klaude_code/ui/modes/repl/renderer.py +189 -99
  158. klaude_code/ui/renderers/assistant.py +9 -2
  159. klaude_code/ui/renderers/bash_syntax.py +178 -0
  160. klaude_code/ui/renderers/common.py +78 -0
  161. klaude_code/ui/renderers/developer.py +104 -48
  162. klaude_code/ui/renderers/diffs.py +87 -6
  163. klaude_code/ui/renderers/errors.py +11 -6
  164. klaude_code/ui/renderers/mermaid_viewer.py +57 -0
  165. klaude_code/ui/renderers/metadata.py +112 -76
  166. klaude_code/ui/renderers/sub_agent.py +92 -7
  167. klaude_code/ui/renderers/thinking.py +40 -18
  168. klaude_code/ui/renderers/tools.py +405 -227
  169. klaude_code/ui/renderers/user_input.py +73 -13
  170. klaude_code/ui/rich/__init__.py +10 -1
  171. klaude_code/ui/rich/cjk_wrap.py +228 -0
  172. klaude_code/ui/rich/code_panel.py +131 -0
  173. klaude_code/ui/rich/live.py +17 -0
  174. klaude_code/ui/rich/markdown.py +305 -170
  175. klaude_code/ui/rich/searchable_text.py +10 -13
  176. klaude_code/ui/rich/status.py +190 -49
  177. klaude_code/ui/rich/theme.py +135 -39
  178. klaude_code/ui/terminal/__init__.py +55 -0
  179. klaude_code/ui/terminal/color.py +1 -1
  180. klaude_code/ui/terminal/control.py +13 -22
  181. klaude_code/ui/terminal/notifier.py +44 -4
  182. klaude_code/ui/terminal/selector.py +658 -0
  183. klaude_code/ui/utils/common.py +0 -18
  184. klaude_code-1.8.0.dist-info/METADATA +377 -0
  185. klaude_code-1.8.0.dist-info/RECORD +219 -0
  186. {klaude_code-1.2.6.dist-info → klaude_code-1.8.0.dist-info}/entry_points.txt +1 -0
  187. klaude_code/command/diff_cmd.py +0 -138
  188. klaude_code/command/prompt-dev-docs-update.md +0 -56
  189. klaude_code/command/prompt-dev-docs.md +0 -46
  190. klaude_code/config/list_model.py +0 -162
  191. klaude_code/core/manager/agent_manager.py +0 -127
  192. klaude_code/core/prompts/prompt-subagent-webfetch.md +0 -46
  193. klaude_code/core/tool/file/multi_edit_tool.md +0 -42
  194. klaude_code/core/tool/file/multi_edit_tool.py +0 -199
  195. klaude_code/core/tool/memory/memory_tool.md +0 -16
  196. klaude_code/core/tool/memory/memory_tool.py +0 -462
  197. klaude_code/llm/openrouter/reasoning_handler.py +0 -209
  198. klaude_code/protocol/sub_agent.py +0 -348
  199. klaude_code/ui/utils/debouncer.py +0 -42
  200. klaude_code-1.2.6.dist-info/METADATA +0 -178
  201. klaude_code-1.2.6.dist-info/RECORD +0 -167
  202. /klaude_code/core/prompts/{prompt-subagent.md → prompt-sub-agent.md} +0 -0
  203. /klaude_code/core/tool/{memory → skill}/__init__.py +0 -0
  204. /klaude_code/core/tool/{memory → skill}/skill_tool.md +0 -0
  205. {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()