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
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,
|
|
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.
|
|
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
|
-
|
|
38
|
+
self._turn_count += 1
|
|
35
39
|
usage = turn_metadata.usage
|
|
36
40
|
|
|
37
41
|
if usage is not None:
|
|
38
|
-
if
|
|
39
|
-
|
|
40
|
-
acc_usage =
|
|
41
|
-
|
|
42
|
-
acc_usage
|
|
43
|
-
acc_usage.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if usage.
|
|
48
|
-
acc_usage.
|
|
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
|
-
|
|
52
|
-
|
|
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
|
-
|
|
65
|
+
self._main_agent.provider = turn_metadata.provider
|
|
77
66
|
if turn_metadata.model_name:
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
86
|
+
self._main_agent.usage.first_token_latency_ms = None
|
|
94
87
|
|
|
95
|
-
|
|
96
|
-
|
|
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
|
|
101
|
-
"""
|
|
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
|
-
|
|
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
|
|
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
|
|
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=
|
|
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
|
-
|
|
152
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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.
|
|
226
|
-
|
|
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=
|
|
229
|
-
task_result=
|
|
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.
|
|
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 .
|
|
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
|
-
"
|
|
37
|
+
"FileTracker",
|
|
36
38
|
"MermaidTool",
|
|
37
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
21
|
-
new_content:
|
|
22
|
-
move_path:
|
|
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,
|
|
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
|
-
|
|
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:
|
|
74
|
+
new_file: str | None = None
|
|
75
75
|
chunks: list[Chunk] = Field(default_factory=_new_chunk_list)
|
|
76
|
-
move_path:
|
|
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:
|
|
90
|
+
def is_done(self, prefixes: tuple[str, ...] | None = None) -> bool:
|
|
91
91
|
if self.index >= len(self.lines):
|
|
92
92
|
return True
|
|
93
|
-
|
|
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
|
-
|
|
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
|
-
|
|
172
|
-
|
|
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
|
|
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, "
|
|
460
|
+
with open(path, "w") as f:
|
|
469
461
|
f.write(content)
|
|
470
462
|
|
|
471
463
|
|