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
klaude_code/core/task.py CHANGED
@@ -2,13 +2,13 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
  import time
5
- from collections.abc import AsyncGenerator, Callable, MutableMapping, Sequence
5
+ from collections.abc import AsyncGenerator, Callable, Sequence
6
6
  from dataclasses import dataclass
7
7
  from typing import TYPE_CHECKING
8
8
 
9
9
  from klaude_code import const
10
10
  from klaude_code.core.reminders import Reminder
11
- from klaude_code.core.tool import TodoContext, ToolABC
11
+ from klaude_code.core.tool import FileTracker, TodoContext, ToolABC
12
12
  from klaude_code.core.turn import TurnError, TurnExecutionContext, TurnExecutor
13
13
  from klaude_code.protocol import events, model
14
14
  from klaude_code.trace import DebugType, log_debug
@@ -25,36 +25,35 @@ class MetadataAccumulator:
25
25
  """
26
26
 
27
27
  def __init__(self, model_name: str) -> None:
28
- self._accumulated = model.ResponseMetadataItem(model_name=model_name)
28
+ self._main_agent = model.TaskMetadata(model_name=model_name) # Main agent metadata
29
+ self._sub_agent_metadata: list[model.TaskMetadata] = []
29
30
  self._throughput_weighted_sum: float = 0.0
30
31
  self._throughput_tracked_tokens: int = 0
32
+ self._first_token_latency_sum: float = 0.0
33
+ self._first_token_latency_count: int = 0
34
+ self._turn_count: int = 0
31
35
 
32
36
  def add(self, turn_metadata: model.ResponseMetadataItem) -> None:
33
37
  """Merge a turn's metadata into the accumulated state."""
34
- accumulated = self._accumulated
38
+ self._turn_count += 1
35
39
  usage = turn_metadata.usage
36
40
 
37
41
  if usage is not None:
38
- if accumulated.usage is None:
39
- accumulated.usage = model.Usage()
40
- acc_usage = accumulated.usage
41
- acc_usage.input_tokens += usage.input_tokens
42
- acc_usage.cached_tokens += usage.cached_tokens
43
- acc_usage.reasoning_tokens += usage.reasoning_tokens
44
- acc_usage.output_tokens += usage.output_tokens
45
- acc_usage.total_tokens += usage.total_tokens
46
-
47
- if usage.context_usage_percent is not None:
48
- acc_usage.context_usage_percent = usage.context_usage_percent
42
+ if self._main_agent.usage is None:
43
+ self._main_agent.usage = model.Usage()
44
+ acc_usage = self._main_agent.usage
45
+
46
+ model.TaskMetadata.merge_usage(acc_usage, usage)
47
+ acc_usage.currency = usage.currency
48
+
49
+ if usage.context_size is not None:
50
+ acc_usage.context_size = usage.context_size
51
+ if usage.context_limit is not None:
52
+ acc_usage.context_limit = usage.context_limit
49
53
 
50
54
  if usage.first_token_latency_ms is not None:
51
- if acc_usage.first_token_latency_ms is None:
52
- acc_usage.first_token_latency_ms = usage.first_token_latency_ms
53
- else:
54
- acc_usage.first_token_latency_ms = min(
55
- acc_usage.first_token_latency_ms,
56
- usage.first_token_latency_ms,
57
- )
55
+ self._first_token_latency_sum += usage.first_token_latency_ms
56
+ self._first_token_latency_count += 1
58
57
 
59
58
  if usage.throughput_tps is not None:
60
59
  current_output = usage.output_tokens
@@ -62,53 +61,58 @@ class MetadataAccumulator:
62
61
  self._throughput_weighted_sum += usage.throughput_tps * current_output
63
62
  self._throughput_tracked_tokens += current_output
64
63
 
65
- # Accumulate costs
66
- if usage.input_cost is not None:
67
- acc_usage.input_cost = (acc_usage.input_cost or 0.0) + usage.input_cost
68
- if usage.output_cost is not None:
69
- acc_usage.output_cost = (acc_usage.output_cost or 0.0) + usage.output_cost
70
- if usage.cache_read_cost is not None:
71
- acc_usage.cache_read_cost = (acc_usage.cache_read_cost or 0.0) + usage.cache_read_cost
72
- if usage.total_cost is not None:
73
- acc_usage.total_cost = (acc_usage.total_cost or 0.0) + usage.total_cost
74
-
75
64
  if turn_metadata.provider is not None:
76
- accumulated.provider = turn_metadata.provider
65
+ self._main_agent.provider = turn_metadata.provider
77
66
  if turn_metadata.model_name:
78
- accumulated.model_name = turn_metadata.model_name
79
- if turn_metadata.response_id:
80
- accumulated.response_id = turn_metadata.response_id
81
- if turn_metadata.status is not None:
82
- accumulated.status = turn_metadata.status
83
- if turn_metadata.error_reason is not None:
84
- accumulated.error_reason = turn_metadata.error_reason
85
-
86
- def finalize(self, task_duration_s: float) -> model.ResponseMetadataItem:
67
+ self._main_agent.model_name = turn_metadata.model_name
68
+
69
+ def add_sub_agent_metadata(self, sub_agent_metadata: model.TaskMetadata) -> None:
70
+ """Add sub-agent task metadata to the accumulated state."""
71
+ self._sub_agent_metadata.append(sub_agent_metadata)
72
+
73
+ def finalize(self, task_duration_s: float) -> model.TaskMetadataItem:
87
74
  """Return the final accumulated metadata with computed throughput and duration."""
88
- accumulated = self._accumulated
89
- if accumulated.usage is not None:
75
+ if self._main_agent.usage is not None:
90
76
  if self._throughput_tracked_tokens > 0:
91
- accumulated.usage.throughput_tps = self._throughput_weighted_sum / self._throughput_tracked_tokens
77
+ self._main_agent.usage.throughput_tps = self._throughput_weighted_sum / self._throughput_tracked_tokens
78
+ else:
79
+ self._main_agent.usage.throughput_tps = None
80
+
81
+ if self._first_token_latency_count > 0:
82
+ self._main_agent.usage.first_token_latency_ms = (
83
+ self._first_token_latency_sum / self._first_token_latency_count
84
+ )
92
85
  else:
93
- accumulated.usage.throughput_tps = None
86
+ self._main_agent.usage.first_token_latency_ms = None
94
87
 
95
- accumulated.task_duration_s = task_duration_s
96
- return accumulated
88
+ self._main_agent.task_duration_s = task_duration_s
89
+ self._main_agent.turn_count = self._turn_count
90
+ return model.TaskMetadataItem(main_agent=self._main_agent, sub_agent_task_metadata=self._sub_agent_metadata)
97
91
 
98
92
 
99
93
  @dataclass
100
- class TaskExecutionContext:
101
- """Execution context required to run a task."""
94
+ class SessionContext:
95
+ """Shared session-level context for task and turn execution.
96
+
97
+ Contains common fields that both TaskExecutionContext and TurnExecutionContext need.
98
+ """
102
99
 
103
100
  session_id: str
104
- profile: AgentProfile
105
101
  get_conversation_history: Callable[[], list[model.ConversationItem]]
106
102
  append_history: Callable[[Sequence[model.ConversationItem]], None]
107
- tool_registry: dict[str, type[ToolABC]]
108
- file_tracker: MutableMapping[str, float]
103
+ file_tracker: FileTracker
109
104
  todo_context: TodoContext
105
+
106
+
107
+ @dataclass
108
+ class TaskExecutionContext:
109
+ """Execution context required to run a task."""
110
+
111
+ session_ctx: SessionContext
112
+ profile: AgentProfile
113
+ tool_registry: dict[str, type[ToolABC]]
110
114
  # For reminder processing - needs access to session
111
- process_reminder: Callable[[Reminder], AsyncGenerator[events.DeveloperMessageEvent, None]]
115
+ process_reminder: Callable[[Reminder], AsyncGenerator[events.DeveloperMessageEvent]]
112
116
  sub_agent_state: model.SubAgentState | None
113
117
 
114
118
 
@@ -122,34 +126,45 @@ class TaskExecutor:
122
126
  self._context = context
123
127
  self._current_turn: TurnExecutor | None = None
124
128
  self._started_at: float = 0.0
129
+ self._metadata_accumulator: MetadataAccumulator | None = None
125
130
 
126
131
  @property
127
132
  def current_turn(self) -> TurnExecutor | None:
128
133
  return self._current_turn
129
134
 
130
135
  def cancel(self) -> list[events.Event]:
131
- """Cancel the current turn and return any resulting events."""
136
+ """Cancel the current turn and return any resulting events including metadata."""
132
137
  ui_events: list[events.Event] = []
133
138
  if self._current_turn is not None:
134
139
  ui_events.extend(self._current_turn.cancel())
135
140
  self._current_turn = None
141
+
142
+ # Emit partial metadata on cancellation
143
+ if self._metadata_accumulator is not None and self._started_at > 0:
144
+ task_duration_s = time.perf_counter() - self._started_at
145
+ accumulated = self._metadata_accumulator.finalize(task_duration_s)
146
+ if accumulated.main_agent.usage is not None:
147
+ session_id = self._context.session_ctx.session_id
148
+ ui_events.append(events.TaskMetadataEvent(metadata=accumulated, session_id=session_id))
149
+ self._context.session_ctx.append_history([accumulated])
150
+
136
151
  return ui_events
137
152
 
138
- async def run(self, user_input: model.UserInputPayload) -> AsyncGenerator[events.Event, None]:
153
+ async def run(self, user_input: model.UserInputPayload) -> AsyncGenerator[events.Event]:
139
154
  """Execute the task, yielding events as they occur."""
140
155
  ctx = self._context
156
+ session_ctx = ctx.session_ctx
141
157
  self._started_at = time.perf_counter()
142
158
 
143
159
  yield events.TaskStartEvent(
144
- session_id=ctx.session_id,
160
+ session_id=session_ctx.session_id,
145
161
  sub_agent_state=ctx.sub_agent_state,
146
162
  )
147
-
148
- ctx.append_history([model.UserMessageItem(content=user_input.text, images=user_input.images)])
163
+ del user_input # Persisted by the operation handler before launching the task.
149
164
 
150
165
  profile = ctx.profile
151
- metadata_accumulator = MetadataAccumulator(model_name=profile.llm_client.model_name)
152
- last_assistant_message: events.AssistantMessageEvent | None = None
166
+ self._metadata_accumulator = MetadataAccumulator(model_name=profile.llm_client.model_name)
167
+ metadata_accumulator = self._metadata_accumulator
153
168
 
154
169
  while True:
155
170
  # Process reminders at the start of each turn
@@ -158,15 +173,11 @@ class TaskExecutor:
158
173
  yield event
159
174
 
160
175
  turn_context = TurnExecutionContext(
161
- session_id=ctx.session_id,
162
- get_conversation_history=ctx.get_conversation_history,
163
- append_history=ctx.append_history,
176
+ session_ctx=session_ctx,
164
177
  llm_client=profile.llm_client,
165
178
  system_prompt=profile.system_prompt,
166
179
  tools=profile.tools,
167
180
  tool_registry=ctx.tool_registry,
168
- file_tracker=ctx.file_tracker,
169
- todo_context=ctx.todo_context,
170
181
  )
171
182
 
172
183
  turn: TurnExecutor | None = None
@@ -181,11 +192,22 @@ class TaskExecutor:
181
192
  async for turn_event in turn.run():
182
193
  match turn_event:
183
194
  case events.AssistantMessageEvent() as am:
184
- if am.content.strip() != "":
185
- last_assistant_message = am
186
195
  yield am
187
196
  case events.ResponseMetadataEvent() as e:
188
197
  metadata_accumulator.add(e.metadata)
198
+ # Emit context usage event if available
199
+ if e.metadata.usage is not None:
200
+ context_percent = e.metadata.usage.context_usage_percent
201
+ if context_percent is not None:
202
+ yield events.ContextUsageEvent(
203
+ session_id=session_ctx.session_id,
204
+ context_percent=context_percent,
205
+ )
206
+ case events.ToolResultEvent() as e:
207
+ # Collect sub-agent task metadata from tool results
208
+ if e.task_metadata is not None:
209
+ metadata_accumulator.add_sub_agent_metadata(e.task_metadata)
210
+ yield turn_event
189
211
  case _:
190
212
  yield turn_event
191
213
 
@@ -198,7 +220,9 @@ class TaskExecutor:
198
220
  error_msg = f"Retrying {attempt + 1}/{const.MAX_FAILED_TURN_RETRIES} in {delay:.1f}s"
199
221
  if last_error_message:
200
222
  error_msg = f"{error_msg} - {last_error_message}"
201
- yield events.ErrorEvent(error_message=error_msg, can_retry=True)
223
+ yield events.ErrorEvent(
224
+ error_message=error_msg, can_retry=True, session_id=session_ctx.session_id
225
+ )
202
226
  await asyncio.sleep(delay)
203
227
  finally:
204
228
  self._current_turn = None
@@ -212,21 +236,35 @@ class TaskExecutor:
212
236
  final_error = f"Turn failed after {const.MAX_FAILED_TURN_RETRIES} retries."
213
237
  if last_error_message:
214
238
  final_error = f"{last_error_message}\n{final_error}"
215
- yield events.ErrorEvent(error_message=final_error, can_retry=False)
239
+ yield events.ErrorEvent(error_message=final_error, can_retry=False, session_id=session_ctx.session_id)
216
240
  return
217
241
 
218
- if turn is None or not turn.has_tool_call:
242
+ if turn is None or turn.task_finished:
243
+ # Empty result should retry instead of finishing
244
+ if turn is not None and not turn.task_result.strip():
245
+ if ctx.sub_agent_state is not None:
246
+ error_msg = "Sub-agent returned empty result, retrying..."
247
+ else:
248
+ error_msg = "Agent returned empty result, retrying..."
249
+ yield events.ErrorEvent(error_message=error_msg, can_retry=True, session_id=session_ctx.session_id)
250
+ continue
219
251
  break
220
252
 
221
253
  # Finalize metadata
222
254
  task_duration_s = time.perf_counter() - self._started_at
223
255
  accumulated = metadata_accumulator.finalize(task_duration_s)
224
256
 
225
- yield events.ResponseMetadataEvent(metadata=accumulated, session_id=ctx.session_id)
226
- ctx.append_history([accumulated])
257
+ yield events.TaskMetadataEvent(metadata=accumulated, session_id=session_ctx.session_id)
258
+ session_ctx.append_history([accumulated])
259
+
260
+ # Get task result from turn
261
+ task_result = turn.task_result if turn is not None else ""
262
+ has_structured_output = turn.has_structured_output if turn is not None else False
263
+
227
264
  yield events.TaskFinishEvent(
228
- session_id=ctx.session_id,
229
- task_result=last_assistant_message.content if last_assistant_message else "",
265
+ session_id=session_ctx.session_id,
266
+ task_result=task_result,
267
+ has_structured_output=has_structured_output,
230
268
  )
231
269
 
232
270
 
@@ -1,21 +1,22 @@
1
1
  from .file.apply_patch import DiffError, process_patch
2
2
  from .file.apply_patch_tool import ApplyPatchTool
3
3
  from .file.edit_tool import EditTool
4
- from .file.multi_edit_tool import MultiEditTool
4
+ from .file.move_tool import MoveTool
5
5
  from .file.read_tool import ReadTool
6
6
  from .file.write_tool import WriteTool
7
- from .memory.memory_tool import MEMORY_DIR_NAME, MemoryTool
8
- from .memory.skill_loader import Skill, SkillLoader
9
- from .memory.skill_tool import SkillTool
7
+ from .report_back_tool import ReportBackTool
10
8
  from .shell.bash_tool import BashTool
11
9
  from .shell.command_safety import SafetyCheckResult, is_safe_command
10
+ from .skill.skill_tool import SkillTool
12
11
  from .sub_agent_tool import SubAgentTool
13
12
  from .todo.todo_write_tool import TodoWriteTool
14
13
  from .todo.update_plan_tool import UpdatePlanTool
15
14
  from .tool_abc import ToolABC
16
15
  from .tool_context import (
16
+ FileTracker,
17
17
  TodoContext,
18
18
  ToolContextToken,
19
+ build_todo_context,
19
20
  current_run_subtask_callback,
20
21
  reset_tool_context,
21
22
  set_tool_context_from_session,
@@ -26,50 +27,42 @@ from .tool_runner import run_tool
26
27
  from .truncation import SimpleTruncationStrategy, TruncationStrategy, get_truncation_strategy, set_truncation_strategy
27
28
  from .web.mermaid_tool import MermaidTool
28
29
  from .web.web_fetch_tool import WebFetchTool
30
+ from .web.web_search_tool import WebSearchTool
29
31
 
30
32
  __all__ = [
31
- # Tools
32
33
  "ApplyPatchTool",
33
34
  "BashTool",
35
+ "DiffError",
34
36
  "EditTool",
35
- "MemoryTool",
37
+ "FileTracker",
36
38
  "MermaidTool",
37
- "MultiEditTool",
39
+ "MoveTool",
38
40
  "ReadTool",
41
+ "ReportBackTool",
42
+ "SafetyCheckResult",
43
+ "SimpleTruncationStrategy",
39
44
  "SkillTool",
40
45
  "SubAgentTool",
46
+ "TodoContext",
41
47
  "TodoWriteTool",
48
+ "ToolABC",
49
+ "ToolContextToken",
50
+ "TruncationStrategy",
42
51
  "UpdatePlanTool",
43
52
  "WebFetchTool",
53
+ "WebSearchTool",
44
54
  "WriteTool",
45
- # Tool ABC
46
- "ToolABC",
47
- # Tool context
48
- "TodoContext",
49
- "ToolContextToken",
55
+ "build_todo_context",
50
56
  "current_run_subtask_callback",
51
- "reset_tool_context",
52
- "set_tool_context_from_session",
53
- "tool_context",
54
- # Tool registry
55
- "load_agent_tools",
56
57
  "get_registry",
57
58
  "get_tool_schemas",
58
- "run_tool",
59
- # Truncation
60
- "SimpleTruncationStrategy",
61
- "TruncationStrategy",
62
59
  "get_truncation_strategy",
63
- "set_truncation_strategy",
64
- # Command safety
65
- "SafetyCheckResult",
66
60
  "is_safe_command",
67
- # Skill
68
- "Skill",
69
- "SkillLoader",
70
- # Memory
71
- "MEMORY_DIR_NAME",
72
- # Apply patch
73
- "DiffError",
61
+ "load_agent_tools",
74
62
  "process_patch",
63
+ "reset_tool_context",
64
+ "run_tool",
65
+ "set_tool_context_from_session",
66
+ "set_truncation_strategy",
67
+ "tool_context",
75
68
  ]
@@ -0,0 +1,36 @@
1
+ """Shared utility functions for file tools."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import os
7
+ from pathlib import Path
8
+
9
+
10
+ def is_directory(path: str) -> bool:
11
+ """Check if path is a directory."""
12
+ return os.path.isdir(path)
13
+
14
+
15
+ def file_exists(path: str) -> bool:
16
+ """Check if path exists."""
17
+ return os.path.exists(path)
18
+
19
+
20
+ def read_text(path: str) -> str:
21
+ """Read text from file with UTF-8 encoding."""
22
+ with open(path, encoding="utf-8", errors="replace") as f:
23
+ return f.read()
24
+
25
+
26
+ def write_text(path: str, content: str) -> None:
27
+ """Write text to file, creating parent directories if needed."""
28
+ parent = Path(path).parent
29
+ parent.mkdir(parents=True, exist_ok=True)
30
+ with open(path, "w", encoding="utf-8") as f:
31
+ f.write(content)
32
+
33
+
34
+ def hash_text_sha256(content: str) -> str:
35
+ """Return SHA-256 for the given text content encoded as UTF-8."""
36
+ return hashlib.sha256(content.encode("utf-8")).hexdigest()
@@ -3,8 +3,8 @@ https://github.com/openai/openai-cookbook/blob/main/examples/gpt-5/apply_patch.p
3
3
  """
4
4
 
5
5
  import os
6
+ from collections.abc import Callable
6
7
  from enum import Enum
7
- from typing import Callable, Optional
8
8
 
9
9
  from pydantic import BaseModel, Field
10
10
 
@@ -17,16 +17,16 @@ class ActionType(str, Enum):
17
17
 
18
18
  class FileChange(BaseModel):
19
19
  type: ActionType
20
- old_content: Optional[str] = None
21
- new_content: Optional[str] = None
22
- move_path: Optional[str] = None
20
+ old_content: str | None = None
21
+ new_content: str | None = None
22
+ move_path: str | None = None
23
23
 
24
24
 
25
25
  class Commit(BaseModel):
26
26
  changes: dict[str, FileChange] = Field(default_factory=dict)
27
27
 
28
28
 
29
- def assemble_changes(orig: dict[str, Optional[str]], dest: dict[str, Optional[str]]) -> Commit:
29
+ def assemble_changes(orig: dict[str, str | None], dest: dict[str, str | None]) -> Commit:
30
30
  commit = Commit()
31
31
  for path in sorted(set(orig.keys()).union(dest.keys())):
32
32
  old_content = orig.get(path)
@@ -49,7 +49,7 @@ def assemble_changes(orig: dict[str, Optional[str]], dest: dict[str, Optional[st
49
49
  old_content=old_content,
50
50
  )
51
51
  else:
52
- assert False
52
+ raise AssertionError()
53
53
  return commit
54
54
 
55
55
 
@@ -71,9 +71,9 @@ def _new_chunk_list() -> list["Chunk"]:
71
71
 
72
72
  class PatchAction(BaseModel):
73
73
  type: ActionType
74
- new_file: Optional[str] = None
74
+ new_file: str | None = None
75
75
  chunks: list[Chunk] = Field(default_factory=_new_chunk_list)
76
- move_path: Optional[str] = None
76
+ move_path: str | None = None
77
77
 
78
78
 
79
79
  class Patch(BaseModel):
@@ -87,26 +87,19 @@ class Parser(BaseModel):
87
87
  patch: Patch = Field(default_factory=Patch)
88
88
  fuzz: int = 0
89
89
 
90
- def is_done(self, prefixes: Optional[tuple[str, ...]] = None) -> bool:
90
+ def is_done(self, prefixes: tuple[str, ...] | None = None) -> bool:
91
91
  if self.index >= len(self.lines):
92
92
  return True
93
- if prefixes and self.lines[self.index].startswith(prefixes):
94
- return True
95
- return False
93
+ return bool(prefixes and self.lines[self.index].startswith(prefixes))
96
94
 
97
95
  def startswith(self, prefix: str | tuple[str, ...]) -> bool:
98
96
  assert self.index < len(self.lines), f"Index: {self.index} >= {len(self.lines)}"
99
- if self.lines[self.index].startswith(prefix):
100
- return True
101
- return False
97
+ return self.lines[self.index].startswith(prefix)
102
98
 
103
99
  def read_str(self, prefix: str = "", return_everything: bool = False) -> str:
104
100
  assert self.index < len(self.lines), f"Index: {self.index} >= {len(self.lines)}"
105
101
  if self.lines[self.index].startswith(prefix):
106
- if return_everything:
107
- text = self.lines[self.index]
108
- else:
109
- text = self.lines[self.index][len(prefix) :]
102
+ text = self.lines[self.index] if return_everything else self.lines[self.index][len(prefix) :]
110
103
  self.index += 1
111
104
  return text
112
105
  return ""
@@ -167,10 +160,9 @@ class Parser(BaseModel):
167
160
  ):
168
161
  def_str = self.read_str("@@ ")
169
162
  section_str = ""
170
- if not def_str:
171
- if self.lines[self.index] == "@@":
172
- section_str = self.lines[self.index]
173
- self.index += 1
163
+ if not def_str and self.lines[self.index] == "@@":
164
+ section_str = self.lines[self.index]
165
+ self.index += 1
174
166
  if not (def_str or section_str or index == 0):
175
167
  raise DiffError(f"Invalid Line:\n{self.lines[self.index]}")
176
168
  if def_str.strip():
@@ -457,7 +449,7 @@ def process_patch(
457
449
 
458
450
 
459
451
  def open_file(path: str) -> str:
460
- with open(path, "rt") as f:
452
+ with open(path) as f:
461
453
  return f.read()
462
454
 
463
455
 
@@ -465,7 +457,7 @@ def write_file(path: str, content: str) -> None:
465
457
  if "/" in path:
466
458
  parent = "/".join(path.split("/")[:-1])
467
459
  os.makedirs(parent, exist_ok=True)
468
- with open(path, "wt") as f:
460
+ with open(path, "w") as f:
469
461
  f.write(content)
470
462
 
471
463