bone-agent 1.4.0 → 2.0.1
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.
- package/bin/bone.js +39 -0
- package/package.json +25 -39
- package/LICENSE +0 -21
- package/README.md +0 -201
- package/bin/npm-wrapper.js +0 -235
- package/bin/rg +0 -0
- package/bin/rg.exe +0 -0
- package/config.yaml.example +0 -144
- package/prompts/main/ask_questions.md +0 -31
- package/prompts/main/batch_independent_calls.md +0 -5
- package/prompts/main/casual_interactions.md +0 -11
- package/prompts/main/code_references.md +0 -8
- package/prompts/main/communication_style.md +0 -12
- package/prompts/main/context_reliability.md +0 -12
- package/prompts/main/conversational_tool_calling.md +0 -15
- package/prompts/main/dream.md +0 -50
- package/prompts/main/editing_pattern.md +0 -13
- package/prompts/main/error_handling.md +0 -6
- package/prompts/main/exploration_pattern.md +0 -21
- package/prompts/main/intro.md +0 -1
- package/prompts/main/obsidian.md +0 -16
- package/prompts/main/obsidian_project.md +0 -79
- package/prompts/main/professional_objectivity.md +0 -3
- package/prompts/main/skills.md +0 -3
- package/prompts/main/targeted_searching.md +0 -10
- package/prompts/main/task_lists_pattern.md +0 -8
- package/prompts/main/temp_folder.md +0 -9
- package/prompts/main/think_before_acting.md +0 -10
- package/prompts/main/tone_and_style.md +0 -4
- package/prompts/main/tool_preferences.md +0 -24
- package/prompts/main/trust_subagent_context.md +0 -21
- package/prompts/main/when_to_use_sub_agent.md +0 -7
- package/prompts/micro/ask_questions.md +0 -1
- package/prompts/micro/batch_independent_calls.md +0 -1
- package/prompts/micro/casual_interactions.md +0 -1
- package/prompts/micro/code_references.md +0 -1
- package/prompts/micro/communication_style.md +0 -1
- package/prompts/micro/context_reliability.md +0 -1
- package/prompts/micro/conversational_tool_calling.md +0 -1
- package/prompts/micro/editing_pattern.md +0 -1
- package/prompts/micro/error_handling.md +0 -1
- package/prompts/micro/exploration_pattern.md +0 -1
- package/prompts/micro/intro.md +0 -1
- package/prompts/micro/obsidian.md +0 -4
- package/prompts/micro/obsidian_project.md +0 -5
- package/prompts/micro/professional_objectivity.md +0 -1
- package/prompts/micro/skills.md +0 -1
- package/prompts/micro/targeted_searching.md +0 -1
- package/prompts/micro/task_lists_pattern.md +0 -1
- package/prompts/micro/temp_folder.md +0 -1
- package/prompts/micro/think_before_acting.md +0 -5
- package/prompts/micro/tone_and_style.md +0 -1
- package/prompts/micro/tool_preferences.md +0 -1
- package/prompts/micro/trust_subagent_context.md +0 -1
- package/prompts/micro/when_to_use_sub_agent.md +0 -1
- package/requirements.txt +0 -9
- package/src/__init__.py +0 -11
- package/src/core/__init__.py +0 -1
- package/src/core/agentic.py +0 -1085
- package/src/core/chat_manager.py +0 -1577
- package/src/core/config_manager.py +0 -260
- package/src/core/cron.py +0 -578
- package/src/core/cron_allowlist.py +0 -118
- package/src/core/memory.py +0 -145
- package/src/core/metadata.py +0 -75
- package/src/core/retry.py +0 -71
- package/src/core/skills.py +0 -463
- package/src/core/sub_agent.py +0 -376
- package/src/core/tool_approval.py +0 -220
- package/src/core/tool_feedback.py +0 -789
- package/src/exceptions.py +0 -79
- package/src/llm/__init__.py +0 -1
- package/src/llm/client.py +0 -176
- package/src/llm/codex_provider.py +0 -350
- package/src/llm/config.py +0 -536
- package/src/llm/prompts.py +0 -494
- package/src/llm/providers.py +0 -438
- package/src/llm/streaming.py +0 -163
- package/src/llm/token_tracker.py +0 -399
- package/src/tools/__init__.py +0 -151
- package/src/tools/constants.py +0 -59
- package/src/tools/create_file.py +0 -136
- package/src/tools/directory.py +0 -389
- package/src/tools/edit.py +0 -549
- package/src/tools/file_reader.py +0 -322
- package/src/tools/helpers/__init__.py +0 -99
- package/src/tools/helpers/base.py +0 -599
- package/src/tools/helpers/converters.py +0 -44
- package/src/tools/helpers/file_helpers.py +0 -189
- package/src/tools/helpers/formatters.py +0 -411
- package/src/tools/helpers/loader.py +0 -145
- package/src/tools/helpers/parallel_executor.py +0 -231
- package/src/tools/helpers/path_resolver.py +0 -283
- package/src/tools/helpers/plugin_manifest.py +0 -185
- package/src/tools/obsidian.py +0 -96
- package/src/tools/review_sub_agent.py +0 -190
- package/src/tools/rg_search.py +0 -477
- package/src/tools/search_plugins.py +0 -177
- package/src/tools/select_option.py +0 -600
- package/src/tools/shell.py +0 -302
- package/src/tools/sub_agent.py +0 -139
- package/src/tools/task_list.py +0 -269
- package/src/tools/web_search.py +0 -61
- package/src/ui/__init__.py +0 -1
- package/src/ui/banner.py +0 -87
- package/src/ui/commands.py +0 -3131
- package/src/ui/displays.py +0 -239
- package/src/ui/loader.py +0 -284
- package/src/ui/main.py +0 -643
- package/src/ui/prompt_utils.py +0 -113
- package/src/ui/setting_selector.py +0 -590
- package/src/ui/setup_wizard.py +0 -294
- package/src/ui/sub_agent_panel.py +0 -234
- package/src/ui/tool_confirmation.py +0 -226
- package/src/utils/__init__.py +0 -1
- package/src/utils/citation_parser.py +0 -199
- package/src/utils/editor.py +0 -207
- package/src/utils/gitignore_filter.py +0 -149
- package/src/utils/logger.py +0 -254
- package/src/utils/paths.py +0 -30
- package/src/utils/result_parsers.py +0 -108
- package/src/utils/safe_commands.py +0 -243
- package/src/utils/settings.py +0 -195
- package/src/utils/user_message_logger.py +0 -120
- package/src/utils/validation.py +0 -201
- package/src/utils/web_search.py +0 -173
package/src/core/sub_agent.py
DELETED
|
@@ -1,376 +0,0 @@
|
|
|
1
|
-
"""Sub-agent for delegated tasks.
|
|
2
|
-
|
|
3
|
-
Uses existing AgenticOrchestrator with isolated message context
|
|
4
|
-
and read-only tools to execute generic delegated tasks.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
|
|
9
|
-
from core.chat_manager import ChatManager
|
|
10
|
-
from exceptions import LLMError
|
|
11
|
-
from llm.prompts import build_sub_agent_prompt
|
|
12
|
-
from utils.settings import sub_agent_settings
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class HardLimitExceeded(Exception):
|
|
16
|
-
"""Raised when the sub-agent hits its hard token limit."""
|
|
17
|
-
pass
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class BilledLimitExceeded(Exception):
|
|
21
|
-
"""Raised when the sub-agent hits its cumulative billed token limit."""
|
|
22
|
-
pass
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def _format_messages_dump(messages) -> str:
|
|
26
|
-
"""Format sub-agent message history as a markdown dump.
|
|
27
|
-
|
|
28
|
-
Args:
|
|
29
|
-
messages: List of message dicts from the sub-agent ChatManager.
|
|
30
|
-
|
|
31
|
-
Returns:
|
|
32
|
-
Markdown string with the full conversation context.
|
|
33
|
-
"""
|
|
34
|
-
lines = [
|
|
35
|
-
"## Sub-Agent Context Dump (Hard Limit Reached)",
|
|
36
|
-
"",
|
|
37
|
-
"The sub-agent exceeded its hard token limit. Below is the full, unabridged context of its investigation. No summary was produced.",
|
|
38
|
-
"",
|
|
39
|
-
"---",
|
|
40
|
-
"",
|
|
41
|
-
]
|
|
42
|
-
for i, msg in enumerate(messages):
|
|
43
|
-
role = msg.get("role", "unknown")
|
|
44
|
-
content = msg.get("content", "")
|
|
45
|
-
tool_calls = msg.get("tool_calls")
|
|
46
|
-
tool_call_id = msg.get("tool_call_id")
|
|
47
|
-
|
|
48
|
-
if tool_call_id:
|
|
49
|
-
lines.append(f"### Message {i} — tool result ({tool_call_id})")
|
|
50
|
-
elif tool_calls:
|
|
51
|
-
lines.append(f"### Message {i} — assistant tool calls")
|
|
52
|
-
for tc in tool_calls:
|
|
53
|
-
fn = tc.get("function", {})
|
|
54
|
-
lines.append(f"- `{fn.get('name', '?')}` — `{fn.get('arguments', '')}`")
|
|
55
|
-
else:
|
|
56
|
-
lines.append(f"### Message {i} — {role}")
|
|
57
|
-
|
|
58
|
-
if content:
|
|
59
|
-
# Truncate large content to avoid blowing out the main agent's context
|
|
60
|
-
max_chars = 4000
|
|
61
|
-
if len(content) > max_chars:
|
|
62
|
-
content = content[:max_chars] + f"\n\n... (truncated, {len(content) - max_chars:,} chars omitted)"
|
|
63
|
-
lines.append(content)
|
|
64
|
-
lines.append("")
|
|
65
|
-
return "\n".join(lines)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
def _configure_compaction():
|
|
69
|
-
"""Create a ChatManager with compaction settings from config.
|
|
70
|
-
|
|
71
|
-
Returns:
|
|
72
|
-
ChatManager: A new ChatManager instance with compaction configured
|
|
73
|
-
"""
|
|
74
|
-
if sub_agent_settings.enable_compaction:
|
|
75
|
-
return ChatManager(compact_trigger_tokens=sub_agent_settings.compact_trigger_tokens)
|
|
76
|
-
else:
|
|
77
|
-
return ChatManager(compact_trigger_tokens=None)
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
def _inject_system_prompt(chat_manager, sub_agent_type: str = "research"):
|
|
81
|
-
"""Build sub-agent prompt and inject it.
|
|
82
|
-
|
|
83
|
-
Token usage is reported live by the wrapper in run_sub_agent(),
|
|
84
|
-
so the system prompt is kept clean.
|
|
85
|
-
|
|
86
|
-
Args:
|
|
87
|
-
chat_manager: ChatManager instance to configure
|
|
88
|
-
sub_agent_type: Type of sub-agent ('research' or 'review').
|
|
89
|
-
"""
|
|
90
|
-
base_prompt = build_sub_agent_prompt(
|
|
91
|
-
sub_agent_type=sub_agent_type,
|
|
92
|
-
soft_limit_tokens=sub_agent_settings.soft_limit_tokens,
|
|
93
|
-
hard_limit_tokens=sub_agent_settings.hard_limit_tokens,
|
|
94
|
-
)
|
|
95
|
-
chat_manager.messages = [{"role": "system", "content": base_prompt}]
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
def _load_codebase_map(chat_manager):
|
|
99
|
-
"""Load agents.md codebase map into sub-agent context if available.
|
|
100
|
-
|
|
101
|
-
Args:
|
|
102
|
-
chat_manager: ChatManager instance to add context to
|
|
103
|
-
"""
|
|
104
|
-
agents_path = Path.cwd() / "agents.md"
|
|
105
|
-
if agents_path.exists():
|
|
106
|
-
map_content = agents_path.read_text(encoding="utf-8").strip()
|
|
107
|
-
user_msg = (
|
|
108
|
-
"Here is the codebase map for this project. "
|
|
109
|
-
"This provides an overview of the repository structure and file purposes. "
|
|
110
|
-
"Use this as a reference when exploring the codebase.\n\n"
|
|
111
|
-
f"## Codebase Map (auto-generated from agents.md)\n\n{map_content}"
|
|
112
|
-
)
|
|
113
|
-
chat_manager.messages.append({"role": "user", "content": user_msg})
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
def _configure_isolation(chat_manager):
|
|
117
|
-
"""Apply isolation settings for sub-agent context.
|
|
118
|
-
|
|
119
|
-
Disables conversation logging.
|
|
120
|
-
|
|
121
|
-
Args:
|
|
122
|
-
chat_manager: ChatManager instance to configure
|
|
123
|
-
"""
|
|
124
|
-
chat_manager.markdown_logger = None
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
def _create_chat_manager(sub_agent_type: str = "research"):
|
|
128
|
-
"""Create a fresh ChatManager instance for sub-agent use.
|
|
129
|
-
|
|
130
|
-
Orchestrates compaction, prompt injection, codebase map loading,
|
|
131
|
-
and isolation configuration.
|
|
132
|
-
|
|
133
|
-
Args:
|
|
134
|
-
sub_agent_type: Type of sub-agent ('research' or 'review').
|
|
135
|
-
|
|
136
|
-
Returns:
|
|
137
|
-
ChatManager: A new ChatManager instance with pre-configured system prompt
|
|
138
|
-
"""
|
|
139
|
-
chat_manager = _configure_compaction()
|
|
140
|
-
chat_manager._compaction_disabled = True
|
|
141
|
-
_inject_system_prompt(chat_manager, sub_agent_type=sub_agent_type)
|
|
142
|
-
_load_codebase_map(chat_manager)
|
|
143
|
-
_configure_isolation(chat_manager)
|
|
144
|
-
return chat_manager
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
def run_sub_agent(
|
|
148
|
-
task_query: str,
|
|
149
|
-
repo_root: Path,
|
|
150
|
-
rg_exe_path: str,
|
|
151
|
-
console=None,
|
|
152
|
-
panel_updater=None,
|
|
153
|
-
sub_agent_type: str = "research",
|
|
154
|
-
initial_context: str = None,
|
|
155
|
-
) -> dict:
|
|
156
|
-
"""Run sub-agent using existing AgenticOrchestrator for delegated tasks.
|
|
157
|
-
|
|
158
|
-
Args:
|
|
159
|
-
task_query: Generic task query to execute (e.g., "Read file config.json")
|
|
160
|
-
repo_root: Repository root path
|
|
161
|
-
rg_exe_path: Path to rg executable
|
|
162
|
-
console: Optional Rich console for output
|
|
163
|
-
panel_updater: Optional SubAgentPanel for live panel updates
|
|
164
|
-
sub_agent_type: Type of sub-agent ('research' or 'review').
|
|
165
|
-
initial_context: Optional string injected as context before the task query
|
|
166
|
-
(e.g., a git diff for review mode).
|
|
167
|
-
|
|
168
|
-
Returns:
|
|
169
|
-
Dict with:
|
|
170
|
-
- 'result': Formatted markdown string (goes into chat history)
|
|
171
|
-
- 'usage': Usage data for billing
|
|
172
|
-
- 'error': Error message if failed (None if success)
|
|
173
|
-
"""
|
|
174
|
-
# Validate panel_updater type if provided
|
|
175
|
-
if panel_updater is not None and not hasattr(panel_updater, 'append'):
|
|
176
|
-
panel_updater = None
|
|
177
|
-
|
|
178
|
-
# If no panel_updater provided, create a simple no-op one
|
|
179
|
-
if panel_updater is None:
|
|
180
|
-
from tools.sub_agent import SimplePanelUpdater
|
|
181
|
-
panel_updater = SimplePanelUpdater(console)
|
|
182
|
-
|
|
183
|
-
# Create fresh ChatManager for sub-agent
|
|
184
|
-
temp_chat_manager = _create_chat_manager(sub_agent_type=sub_agent_type)
|
|
185
|
-
|
|
186
|
-
# Inject initial context as a user/assistant exchange if provided
|
|
187
|
-
if initial_context:
|
|
188
|
-
temp_chat_manager.messages.append(
|
|
189
|
-
{"role": "user", "content": initial_context}
|
|
190
|
-
)
|
|
191
|
-
|
|
192
|
-
# Import here to avoid circular import with core.agentic
|
|
193
|
-
from core.agentic import AgenticOrchestrator
|
|
194
|
-
|
|
195
|
-
# Create orchestrator (reuses existing implementation)
|
|
196
|
-
orchestrator = AgenticOrchestrator(
|
|
197
|
-
chat_manager=temp_chat_manager,
|
|
198
|
-
repo_root=repo_root,
|
|
199
|
-
rg_exe_path=rg_exe_path,
|
|
200
|
-
console=console,
|
|
201
|
-
debug_mode=False,
|
|
202
|
-
suppress_result_display=True,
|
|
203
|
-
is_sub_agent=True,
|
|
204
|
-
panel_updater=panel_updater,
|
|
205
|
-
force_parallel_execution=True # Enable parallel execution for read-only tools
|
|
206
|
-
)
|
|
207
|
-
|
|
208
|
-
# Wrap orchestrator._get_llm_response to check hard token limit and
|
|
209
|
-
# wrap client.chat_completion once (outside the loop) to inject live
|
|
210
|
-
# token feedback as a system message — avoids per-call monkey-patching
|
|
211
|
-
# and eliminates any re-entrancy risk.
|
|
212
|
-
original_get_llm_response = orchestrator._get_llm_response
|
|
213
|
-
original_chat_completion = temp_chat_manager.client.chat_completion
|
|
214
|
-
|
|
215
|
-
_soft_limit_warned = False
|
|
216
|
-
_billed_warning_sent = False
|
|
217
|
-
|
|
218
|
-
def _chat_completion_with_token_hint(messages, **kwargs):
|
|
219
|
-
"""Prepend a system-level token budget hint and one-time warnings to every LLM call."""
|
|
220
|
-
nonlocal _soft_limit_warned, _billed_warning_sent
|
|
221
|
-
tt = temp_chat_manager.token_tracker
|
|
222
|
-
hint = f"[Token budget: {tt.current_context_tokens:,} curr / {tt.conv_total_tokens:,} total billed]"
|
|
223
|
-
warnings = []
|
|
224
|
-
|
|
225
|
-
if not _soft_limit_warned and tt.current_context_tokens >= sub_agent_settings.soft_limit_tokens:
|
|
226
|
-
_soft_limit_warned = True
|
|
227
|
-
warnings.append(
|
|
228
|
-
f"WARNING: You have exceeded the current-context soft token limit "
|
|
229
|
-
f"({tt.current_context_tokens:,} / {sub_agent_settings.soft_limit_tokens:,}). "
|
|
230
|
-
"STOP exploring and return your findings immediately. Do NOT call any more tools."
|
|
231
|
-
)
|
|
232
|
-
|
|
233
|
-
if not _billed_warning_sent and tt.conv_total_tokens >= sub_agent_settings.billed_warning_tokens:
|
|
234
|
-
_billed_warning_sent = True
|
|
235
|
-
warnings.append(
|
|
236
|
-
f"WARNING: You have exceeded the cumulative billed token warning limit "
|
|
237
|
-
f"({tt.conv_total_tokens:,} / {sub_agent_settings.billed_warning_tokens:,}). "
|
|
238
|
-
"This sub-agent may be running away. STOP exploring and return your findings immediately. "
|
|
239
|
-
"Do NOT call any more tools."
|
|
240
|
-
)
|
|
241
|
-
|
|
242
|
-
if warnings:
|
|
243
|
-
hint = "\n".join([*warnings, hint])
|
|
244
|
-
|
|
245
|
-
token_msg = {"role": "system", "content": hint}
|
|
246
|
-
return original_chat_completion([token_msg, *messages], **kwargs)
|
|
247
|
-
|
|
248
|
-
def _get_llm_response_with_hard_limit(allowed_tools=None, allow_active_plugins=False):
|
|
249
|
-
"""Wrapper to check context and billed token limits and update panel state."""
|
|
250
|
-
tt = temp_chat_manager.token_tracker
|
|
251
|
-
|
|
252
|
-
# Check hard token limit before making LLM call
|
|
253
|
-
# Use current_context_tokens (prompt size) not total_tokens (cumulative billing)
|
|
254
|
-
# to catch prompt-length-over-limit errors before they hit the API.
|
|
255
|
-
if tt.current_context_tokens >= sub_agent_settings.hard_limit_tokens:
|
|
256
|
-
raise HardLimitExceeded(
|
|
257
|
-
f"Sub-agent hard token limit exceeded: "
|
|
258
|
-
f"{tt.current_context_tokens:,} / {sub_agent_settings.hard_limit_tokens:,} tokens."
|
|
259
|
-
)
|
|
260
|
-
|
|
261
|
-
# Check cumulative billed tokens to stop runaway sub-agents even when
|
|
262
|
-
# current context remains below the prompt-size hard limit.
|
|
263
|
-
#
|
|
264
|
-
# Note: the billed warning is injected by _chat_completion_with_token_hint
|
|
265
|
-
# on the next chat_completion call. This hard stop runs before each LLM
|
|
266
|
-
# response, so once we hit the billed hard limit the warning may never be
|
|
267
|
-
# delivered if no further chat_completion call is made.
|
|
268
|
-
if tt.conv_total_tokens >= sub_agent_settings.billed_hard_limit_tokens:
|
|
269
|
-
raise BilledLimitExceeded(
|
|
270
|
-
f"Sub-agent billed token limit exceeded: "
|
|
271
|
-
f"{tt.conv_total_tokens:,} / {sub_agent_settings.billed_hard_limit_tokens:,} tokens."
|
|
272
|
-
)
|
|
273
|
-
|
|
274
|
-
# Update panel with live token counts
|
|
275
|
-
# Order: conversation length (current context) first, total tokens billed second
|
|
276
|
-
conv_length = tt.current_context_tokens
|
|
277
|
-
total_billed = tt.conv_total_tokens
|
|
278
|
-
if hasattr(panel_updater, 'token_info'):
|
|
279
|
-
panel_updater.token_info = f"{conv_length:,} curr | {total_billed:,} total"
|
|
280
|
-
panel_updater.append("") # Refresh panel title
|
|
281
|
-
|
|
282
|
-
return original_get_llm_response(
|
|
283
|
-
allowed_tools=allowed_tools,
|
|
284
|
-
allow_active_plugins=allow_active_plugins,
|
|
285
|
-
)
|
|
286
|
-
|
|
287
|
-
# Apply both patches once, before the orchestrator loop starts
|
|
288
|
-
orchestrator._get_llm_response = _get_llm_response_with_hard_limit
|
|
289
|
-
temp_chat_manager.client.chat_completion = _chat_completion_with_token_hint
|
|
290
|
-
|
|
291
|
-
hard_limit_exceeded = False
|
|
292
|
-
billed_limit_exceeded = False
|
|
293
|
-
|
|
294
|
-
try:
|
|
295
|
-
# Run sub-agent task
|
|
296
|
-
orchestrator.run(
|
|
297
|
-
task_query,
|
|
298
|
-
thinking_indicator=None,
|
|
299
|
-
allowed_tools=sub_agent_settings.allowed_tools,
|
|
300
|
-
allow_active_plugins=sub_agent_settings.allow_active_plugins,
|
|
301
|
-
)
|
|
302
|
-
except HardLimitExceeded:
|
|
303
|
-
hard_limit_exceeded = True
|
|
304
|
-
except BilledLimitExceeded:
|
|
305
|
-
billed_limit_exceeded = True
|
|
306
|
-
except LLMError as e:
|
|
307
|
-
return {
|
|
308
|
-
"result": "",
|
|
309
|
-
"usage": {
|
|
310
|
-
"prompt_tokens": 0,
|
|
311
|
-
"completion_tokens": 0,
|
|
312
|
-
"total_tokens": 0
|
|
313
|
-
},
|
|
314
|
-
"model": temp_chat_manager.client.model,
|
|
315
|
-
"error": str(e)
|
|
316
|
-
}
|
|
317
|
-
except Exception as e:
|
|
318
|
-
import traceback
|
|
319
|
-
error_details = f"{e}\n\nTraceback:\n{traceback.format_exc()}"
|
|
320
|
-
return {
|
|
321
|
-
"result": "",
|
|
322
|
-
"usage": {
|
|
323
|
-
"prompt_tokens": 0,
|
|
324
|
-
"completion_tokens": 0,
|
|
325
|
-
"total_tokens": 0
|
|
326
|
-
},
|
|
327
|
-
"model": "",
|
|
328
|
-
"error": error_details
|
|
329
|
-
}
|
|
330
|
-
finally:
|
|
331
|
-
# Restore originals
|
|
332
|
-
temp_chat_manager.client.chat_completion = original_chat_completion
|
|
333
|
-
|
|
334
|
-
# Get final token usage (no need for delta calculation on fresh instance)
|
|
335
|
-
delta_prompt = temp_chat_manager.token_tracker.total_prompt_tokens
|
|
336
|
-
delta_completion = temp_chat_manager.token_tracker.total_completion_tokens
|
|
337
|
-
delta_total = temp_chat_manager.token_tracker.total_tokens
|
|
338
|
-
tt = temp_chat_manager.token_tracker
|
|
339
|
-
delta_cost = tt.total_actual_cost + tt.total_estimated_cost
|
|
340
|
-
|
|
341
|
-
if hard_limit_exceeded and sub_agent_settings.dump_context_on_hard_limit:
|
|
342
|
-
result = _format_messages_dump(temp_chat_manager.messages)
|
|
343
|
-
else:
|
|
344
|
-
# Extract final response (last assistant message with content)
|
|
345
|
-
final_content = ""
|
|
346
|
-
for msg in reversed(temp_chat_manager.messages):
|
|
347
|
-
if msg.get("role") == "assistant" and msg.get("content"):
|
|
348
|
-
final_content = msg["content"].strip()
|
|
349
|
-
break
|
|
350
|
-
|
|
351
|
-
if billed_limit_exceeded:
|
|
352
|
-
prefix = (
|
|
353
|
-
"WARNING: Sub-agent billed token limit reached. "
|
|
354
|
-
"Returning current findings early to prevent runaway execution."
|
|
355
|
-
)
|
|
356
|
-
result = f"{prefix}\n\n{final_content}" if final_content else prefix
|
|
357
|
-
else:
|
|
358
|
-
result = final_content
|
|
359
|
-
|
|
360
|
-
usage = {
|
|
361
|
-
"prompt_tokens": delta_prompt,
|
|
362
|
-
"completion_tokens": delta_completion,
|
|
363
|
-
"total_tokens": delta_total,
|
|
364
|
-
"context_tokens": tt.current_context_tokens,
|
|
365
|
-
}
|
|
366
|
-
if delta_cost > 0:
|
|
367
|
-
usage["cost"] = delta_cost
|
|
368
|
-
|
|
369
|
-
return {
|
|
370
|
-
"result": result,
|
|
371
|
-
"usage": usage,
|
|
372
|
-
"model": temp_chat_manager.client.model,
|
|
373
|
-
"error": None,
|
|
374
|
-
"hard_limit_exceeded": hard_limit_exceeded,
|
|
375
|
-
"billed_limit_exceeded": billed_limit_exceeded,
|
|
376
|
-
}
|
|
@@ -1,220 +0,0 @@
|
|
|
1
|
-
"""Tool approval workflows for edit_file and execute_command."""
|
|
2
|
-
|
|
3
|
-
from rich.text import Text
|
|
4
|
-
|
|
5
|
-
from tools import confirm_tool
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def handle_edit_approval(preview, file_path, args_dict, console, thinking_indicator,
|
|
9
|
-
approve_mode, cycle_approve_mode, repo_root, gitignore_spec,
|
|
10
|
-
vault_root_str):
|
|
11
|
-
"""Handle edit_file approval workflow.
|
|
12
|
-
|
|
13
|
-
Args:
|
|
14
|
-
preview: Either a rich Text object or a plain string to display.
|
|
15
|
-
file_path: The file path being edited (for the confirm prompt).
|
|
16
|
-
args_dict: Tool arguments dict (path, search, replace, context_lines).
|
|
17
|
-
console: Rich console for display.
|
|
18
|
-
thinking_indicator: ThinkingIndicator instance (may be None).
|
|
19
|
-
approve_mode: Current approval mode string.
|
|
20
|
-
cycle_approve_mode: Callable to cycle approval mode.
|
|
21
|
-
repo_root: Repository root path string.
|
|
22
|
-
gitignore_spec: Gitignore spec object.
|
|
23
|
-
vault_root_str: Callable returning vault root path string.
|
|
24
|
-
|
|
25
|
-
Returns:
|
|
26
|
-
(result_str, should_exit) tuple where should_exit=True means cancel the agentic loop.
|
|
27
|
-
"""
|
|
28
|
-
# Display preview
|
|
29
|
-
console.print(preview)
|
|
30
|
-
console.print()
|
|
31
|
-
|
|
32
|
-
# Stop thinking indicator while waiting for user input
|
|
33
|
-
if thinking_indicator:
|
|
34
|
-
thinking_indicator.stop()
|
|
35
|
-
|
|
36
|
-
action, guidance = confirm_tool(
|
|
37
|
-
f"edit_file: {file_path}",
|
|
38
|
-
console,
|
|
39
|
-
reason=args_dict.get('reason', 'Apply file edit with above changes'),
|
|
40
|
-
requires_approval=True,
|
|
41
|
-
approve_mode=approve_mode,
|
|
42
|
-
is_edit_tool=True,
|
|
43
|
-
cycle_approve_mode=cycle_approve_mode
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
if action == "accept":
|
|
47
|
-
from tools.edit import _execute_edit_file
|
|
48
|
-
final_result = _execute_edit_file(
|
|
49
|
-
path=args_dict.get('path'),
|
|
50
|
-
search=args_dict.get('search'),
|
|
51
|
-
replace=args_dict.get('replace'),
|
|
52
|
-
repo_root=repo_root,
|
|
53
|
-
console=console,
|
|
54
|
-
gitignore_spec=gitignore_spec,
|
|
55
|
-
context_lines=args_dict.get('context_lines', 3),
|
|
56
|
-
vault_root=vault_root_str()
|
|
57
|
-
)
|
|
58
|
-
# Strip exit_code line from final result before displaying
|
|
59
|
-
if final_result and isinstance(final_result, str):
|
|
60
|
-
result_lines = [line for line in final_result.split('\n') if not line.startswith('exit_code=')]
|
|
61
|
-
final_result = '\n'.join(result_lines).strip()
|
|
62
|
-
result_str, should_exit = final_result, False
|
|
63
|
-
elif action == "advise":
|
|
64
|
-
console.print(f"[dim]Edit not applied. User advice: {guidance}[/dim]")
|
|
65
|
-
result_str = f"exit_code=1\nEdit not applied. User advice: {guidance}"
|
|
66
|
-
should_exit = False
|
|
67
|
-
else: # cancel
|
|
68
|
-
console.print("[dim]Operation canceled by user.[/dim]")
|
|
69
|
-
result_str = "exit_code=1\nOperation canceled by user. Do not retry this operation."
|
|
70
|
-
should_exit = True
|
|
71
|
-
|
|
72
|
-
# Restart thinking indicator after user input
|
|
73
|
-
if thinking_indicator:
|
|
74
|
-
thinking_indicator.start()
|
|
75
|
-
|
|
76
|
-
return result_str, should_exit
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def resolve_edit_preview(result):
|
|
80
|
-
"""Extract a displayable preview from an edit_file tool result.
|
|
81
|
-
|
|
82
|
-
Handles both Rich Text objects (new format) and legacy string format.
|
|
83
|
-
|
|
84
|
-
Args:
|
|
85
|
-
result: Either a rich Text object or a string.
|
|
86
|
-
|
|
87
|
-
Returns:
|
|
88
|
-
(preview, is_valid) tuple.
|
|
89
|
-
- preview: Text object, plain string, or None if error.
|
|
90
|
-
- is_valid: False if the result is an error (non-zero exit_code).
|
|
91
|
-
"""
|
|
92
|
-
if isinstance(result, Text):
|
|
93
|
-
return result, True
|
|
94
|
-
elif isinstance(result, str) and result.startswith("exit_code=0"):
|
|
95
|
-
lines = result.split('\n')
|
|
96
|
-
preview_lines = [line for line in lines if not line.startswith("exit_code=")]
|
|
97
|
-
preview = '\n'.join(preview_lines).strip()
|
|
98
|
-
return preview, True
|
|
99
|
-
else:
|
|
100
|
-
# Error occurred during preview - don't show to user
|
|
101
|
-
return None, False
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
def handle_command_approval(command, arguments, tool, context, console,
|
|
105
|
-
thinking_indicator, approve_mode, debug_mode,
|
|
106
|
-
cron_job_id=None, cron_allowlist=None,
|
|
107
|
-
cron_interactive=False):
|
|
108
|
-
"""Handle execute_command approval workflow.
|
|
109
|
-
|
|
110
|
-
Checks for silent blocks, auto-approval, and prompts user if needed.
|
|
111
|
-
When a cron_job_id and cron_allowlist are provided, commands on the
|
|
112
|
-
job's allow list are auto-approved; unlisted commands are blocked
|
|
113
|
-
(in scheduled mode) or prompted interactively (in test-run mode).
|
|
114
|
-
|
|
115
|
-
Args:
|
|
116
|
-
command: The shell command string.
|
|
117
|
-
arguments: Tool arguments dict (includes 'reason').
|
|
118
|
-
tool: The tool object to execute on approval.
|
|
119
|
-
context: Tool execution context dict.
|
|
120
|
-
console: Rich console for display.
|
|
121
|
-
thinking_indicator: ThinkingIndicator instance (may be None).
|
|
122
|
-
approve_mode: Current approval mode string.
|
|
123
|
-
debug_mode: Whether debug mode is active (for silent block logging).
|
|
124
|
-
cron_job_id: Optional cron job ID for allow list checking.
|
|
125
|
-
cron_allowlist: Optional CronAllowlist instance for cron command gating.
|
|
126
|
-
cron_interactive: If True, cron job is in interactive test-run mode.
|
|
127
|
-
|
|
128
|
-
Returns:
|
|
129
|
-
(result, should_exit, command_executed) tuple.
|
|
130
|
-
- result: Tool result string.
|
|
131
|
-
- should_exit: True if the user canceled (break the agentic loop).
|
|
132
|
-
- command_executed: True if the command was actually executed (display output).
|
|
133
|
-
"""
|
|
134
|
-
from utils.validation import is_auto_approved_command, check_for_silent_blocked_command
|
|
135
|
-
|
|
136
|
-
# Check if command should be silently blocked (redirect to native tool)
|
|
137
|
-
is_blocked, reprompt_msg = check_for_silent_blocked_command(command)
|
|
138
|
-
if is_blocked:
|
|
139
|
-
if debug_mode:
|
|
140
|
-
console.print(f"[dim]Silently blocked command: {command.split()[0]}[/dim]")
|
|
141
|
-
result = f"exit_code=1\n{reprompt_msg}"
|
|
142
|
-
return result, False, False
|
|
143
|
-
|
|
144
|
-
# Check if command should be auto-approved (global safe commands)
|
|
145
|
-
auto_approve = is_auto_approved_command(command)
|
|
146
|
-
|
|
147
|
-
# Check cron allow list
|
|
148
|
-
cron_auto_approved = False
|
|
149
|
-
if cron_job_id and cron_allowlist:
|
|
150
|
-
if cron_allowlist.is_allowed(cron_job_id, command):
|
|
151
|
-
cron_auto_approved = True
|
|
152
|
-
elif not auto_approve:
|
|
153
|
-
# Command not on allow list and not globally safe
|
|
154
|
-
# Determine if we're in interactive test-run or scheduled mode
|
|
155
|
-
if cron_interactive:
|
|
156
|
-
# Interactive test run (/cron run) — prompt the user
|
|
157
|
-
pass # Fall through to normal interactive approval below
|
|
158
|
-
else:
|
|
159
|
-
# Scheduled run — block the command, let agent adapt
|
|
160
|
-
allowed_cmds = cron_allowlist.get_commands(cron_job_id)
|
|
161
|
-
allowed_preview = ", ".join(f"'{c}'" for c in allowed_cmds[:5])
|
|
162
|
-
if len(allowed_cmds) > 5:
|
|
163
|
-
allowed_preview += f", ... ({len(allowed_cmds)} total)"
|
|
164
|
-
if not allowed_preview:
|
|
165
|
-
allowed_preview = "(none - run '/cron run <id>' to build the allow list)"
|
|
166
|
-
result = (
|
|
167
|
-
f"exit_code=1\n"
|
|
168
|
-
f"Command not in cron allow list for job '{cron_job_id}'.\n"
|
|
169
|
-
f"Command: {command}\n"
|
|
170
|
-
f"Allowed: {allowed_preview}\n"
|
|
171
|
-
f"Do not retry this command. Use only approved commands or "
|
|
172
|
-
f"ask the user to run '/cron run {cron_job_id}' to add it."
|
|
173
|
-
)
|
|
174
|
-
return result, False, False
|
|
175
|
-
|
|
176
|
-
if cron_auto_approved or auto_approve:
|
|
177
|
-
# Auto-approved command - execute without prompting
|
|
178
|
-
result = tool.execute(arguments, context)
|
|
179
|
-
command_executed = True
|
|
180
|
-
|
|
181
|
-
# In cron test-run mode, auto-save newly approved commands to allow list
|
|
182
|
-
# Skip globally-safe commands — they're auto-approved regardless of the allow list
|
|
183
|
-
if cron_job_id and cron_allowlist and cron_interactive and not auto_approve:
|
|
184
|
-
cron_allowlist.add_command(cron_job_id, command)
|
|
185
|
-
|
|
186
|
-
return result, False, command_executed
|
|
187
|
-
|
|
188
|
-
# Interactive approval (test-run mode or normal session)
|
|
189
|
-
# Stop thinking indicator while waiting for user input
|
|
190
|
-
if thinking_indicator:
|
|
191
|
-
thinking_indicator.stop()
|
|
192
|
-
|
|
193
|
-
action, guidance = confirm_tool(
|
|
194
|
-
f"execute_command: {command[:80]}{'...' if len(command) > 80 else ''}",
|
|
195
|
-
console,
|
|
196
|
-
reason=arguments.get('reason', 'Execute shell command'),
|
|
197
|
-
requires_approval=True,
|
|
198
|
-
approve_mode=approve_mode
|
|
199
|
-
)
|
|
200
|
-
|
|
201
|
-
if action == "accept":
|
|
202
|
-
result = tool.execute(arguments, context)
|
|
203
|
-
command_executed = True
|
|
204
|
-
# Auto-save approved command to cron allow list during test run
|
|
205
|
-
if cron_job_id and cron_allowlist:
|
|
206
|
-
cron_allowlist.add_command(cron_job_id, command)
|
|
207
|
-
elif action == "advise":
|
|
208
|
-
result = f"Command not executed. User advice: {guidance}"
|
|
209
|
-
command_executed = False
|
|
210
|
-
elif action == "cancel":
|
|
211
|
-
result = "Command canceled by user. Do not retry this operation."
|
|
212
|
-
if thinking_indicator:
|
|
213
|
-
thinking_indicator.start()
|
|
214
|
-
return result, True, False
|
|
215
|
-
|
|
216
|
-
# Restart thinking indicator after user input
|
|
217
|
-
if thinking_indicator:
|
|
218
|
-
thinking_indicator.start()
|
|
219
|
-
|
|
220
|
-
return result, False, command_executed
|