bone-agent 1.3.2 → 1.4.0
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/README.md +19 -2
- package/config.yaml.example +13 -2
- package/package.json +3 -2
- package/prompts/main/ask_questions.md +31 -0
- package/prompts/main/batch_independent_calls.md +5 -0
- package/prompts/main/casual_interactions.md +11 -0
- package/prompts/main/code_references.md +8 -0
- package/prompts/main/communication_style.md +12 -0
- package/prompts/main/context_reliability.md +12 -0
- package/prompts/main/conversational_tool_calling.md +15 -0
- package/prompts/main/dream.md +50 -0
- package/prompts/main/editing_pattern.md +13 -0
- package/prompts/main/error_handling.md +6 -0
- package/prompts/main/exploration_pattern.md +21 -0
- package/prompts/main/intro.md +1 -0
- package/prompts/main/obsidian.md +16 -0
- package/prompts/main/obsidian_project.md +79 -0
- package/prompts/main/professional_objectivity.md +3 -0
- package/prompts/main/skills.md +3 -0
- package/prompts/main/targeted_searching.md +10 -0
- package/prompts/main/task_lists_pattern.md +8 -0
- package/prompts/main/temp_folder.md +9 -0
- package/prompts/main/think_before_acting.md +10 -0
- package/prompts/main/tone_and_style.md +4 -0
- package/prompts/main/tool_preferences.md +24 -0
- package/prompts/main/trust_subagent_context.md +21 -0
- package/prompts/main/when_to_use_sub_agent.md +7 -0
- package/prompts/micro/ask_questions.md +1 -0
- package/prompts/micro/batch_independent_calls.md +1 -0
- package/prompts/micro/casual_interactions.md +1 -0
- package/prompts/micro/code_references.md +1 -0
- package/prompts/micro/communication_style.md +1 -0
- package/prompts/micro/context_reliability.md +1 -0
- package/prompts/micro/conversational_tool_calling.md +1 -0
- package/prompts/micro/editing_pattern.md +1 -0
- package/prompts/micro/error_handling.md +1 -0
- package/prompts/micro/exploration_pattern.md +1 -0
- package/prompts/micro/intro.md +1 -0
- package/prompts/micro/obsidian.md +4 -0
- package/prompts/micro/obsidian_project.md +5 -0
- package/prompts/micro/professional_objectivity.md +1 -0
- package/prompts/micro/skills.md +1 -0
- package/prompts/micro/targeted_searching.md +1 -0
- package/prompts/micro/task_lists_pattern.md +1 -0
- package/prompts/micro/temp_folder.md +1 -0
- package/prompts/micro/think_before_acting.md +5 -0
- package/prompts/micro/tone_and_style.md +1 -0
- package/prompts/micro/tool_preferences.md +1 -0
- package/prompts/micro/trust_subagent_context.md +1 -0
- package/prompts/micro/when_to_use_sub_agent.md +1 -0
- package/src/core/agentic.py +134 -106
- package/src/core/chat_manager.py +60 -12
- package/src/core/config_manager.py +14 -1
- package/src/core/cron.py +57 -6
- package/src/core/memory.py +3 -90
- package/src/core/metadata.py +75 -0
- package/src/core/skills.py +463 -0
- package/src/core/sub_agent.py +93 -43
- package/src/core/tool_feedback.py +87 -76
- package/src/llm/client.py +7 -2
- package/src/llm/codex_provider.py +350 -0
- package/src/llm/config.py +74 -4
- package/src/llm/prompts.py +261 -502
- package/src/llm/providers.py +28 -7
- package/src/llm/token_tracker.py +32 -1
- package/src/tools/__init__.py +24 -85
- package/src/tools/create_file.py +1 -1
- package/src/tools/directory.py +1 -1
- package/src/tools/edit.py +13 -7
- package/src/tools/file_reader.py +1 -1
- package/src/tools/helpers/__init__.py +1 -7
- package/src/tools/helpers/base.py +65 -16
- package/src/tools/helpers/loader.py +2 -88
- package/src/tools/helpers/path_resolver.py +70 -13
- package/src/tools/helpers/plugin_manifest.py +99 -70
- package/src/tools/review_sub_agent.py +2 -1
- package/src/tools/rg_search.py +119 -35
- package/src/tools/search_plugins.py +140 -72
- package/src/tools/shell.py +3 -3
- package/src/ui/commands.py +470 -33
- package/src/ui/displays.py +27 -1
- package/src/ui/main.py +1 -4
- package/src/ui/tool_confirmation.py +16 -5
- package/src/utils/editor.py +88 -39
- package/src/utils/settings.py +25 -4
- package/src/utils/user_message_logger.py +120 -0
- package/src/utils/validation.py +10 -0
package/src/core/agentic.py
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import logging
|
|
5
|
-
import time
|
|
6
5
|
from pathlib import Path
|
|
7
6
|
from typing import Optional
|
|
8
7
|
|
|
@@ -47,6 +46,7 @@ from core.tool_feedback import (
|
|
|
47
46
|
display_tool_feedback,
|
|
48
47
|
)
|
|
49
48
|
from ui.sub_agent_panel import SubAgentPanel
|
|
49
|
+
from tools.helpers.path_resolver import extract_boundary_path, is_boundary_error, set_full_filesystem_access
|
|
50
50
|
|
|
51
51
|
|
|
52
52
|
def _handle_empty_response(empty_response_count, console):
|
|
@@ -88,7 +88,6 @@ def _handle_tool_limit_reached(chat_manager, console):
|
|
|
88
88
|
response,
|
|
89
89
|
model_name=provider_cfg.get("model", ""),
|
|
90
90
|
)
|
|
91
|
-
|
|
92
91
|
try:
|
|
93
92
|
final_message = response["choices"][0]["message"]
|
|
94
93
|
except (KeyError, IndexError):
|
|
@@ -106,8 +105,6 @@ def _handle_tool_limit_reached(chat_manager, console):
|
|
|
106
105
|
console.print("[red]Error: model returned empty response after tool limit reached.[/red]")
|
|
107
106
|
return False
|
|
108
107
|
|
|
109
|
-
|
|
110
|
-
|
|
111
108
|
class AgenticOrchestrator:
|
|
112
109
|
"""Orchestrates the agentic tool-calling loop.
|
|
113
110
|
|
|
@@ -172,78 +169,42 @@ class AgenticOrchestrator:
|
|
|
172
169
|
# Check if we're in a parallel context with suppressed console
|
|
173
170
|
return self._parallel_context.get('console', self.console)
|
|
174
171
|
|
|
175
|
-
def
|
|
176
|
-
"""
|
|
172
|
+
def _get_effective_tools(self, allowed_tools=None, allow_active_plugins=False):
|
|
173
|
+
"""Return tool schemas allowed for the current run."""
|
|
174
|
+
from tools.helpers.base import ToolRegistry
|
|
177
175
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
- Any file under {repo_root}/.bone/ — project memory directory
|
|
176
|
+
tools = TOOLS()
|
|
177
|
+
if allowed_tools is None:
|
|
178
|
+
return tools
|
|
182
179
|
|
|
183
|
-
|
|
184
|
-
|
|
180
|
+
effective_names = set(allowed_tools)
|
|
181
|
+
if allow_active_plugins:
|
|
182
|
+
effective_names.update(ToolRegistry.active_plugin_names())
|
|
185
183
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
"""
|
|
189
|
-
p = Path(path).resolve()
|
|
190
|
-
repo_root = Path(self.repo_root).resolve()
|
|
191
|
-
# Known memory paths
|
|
192
|
-
if p == Path.home() / ".bone" / "user_memory.md":
|
|
193
|
-
return True
|
|
194
|
-
# Any file under {repo_root}/.bone/ (future memory files)
|
|
195
|
-
bone_dir = repo_root / ".bone"
|
|
196
|
-
if p.is_relative_to(bone_dir):
|
|
197
|
-
return True
|
|
198
|
-
return False
|
|
184
|
+
if not effective_names:
|
|
185
|
+
return []
|
|
199
186
|
|
|
200
|
-
|
|
201
|
-
"""Apply a memory file edit synchronously with one retry on failure.
|
|
187
|
+
return [tool for tool in tools if tool["function"]["name"] in effective_names]
|
|
202
188
|
|
|
203
|
-
|
|
204
|
-
arguments: Tool arguments dict (path, search, replace, etc.)
|
|
205
|
-
"""
|
|
206
|
-
from tools.edit import _execute_edit_file
|
|
207
|
-
|
|
208
|
-
kwargs = dict(
|
|
209
|
-
path=arguments.get("path"),
|
|
210
|
-
search=arguments.get("search"),
|
|
211
|
-
replace=arguments.get("replace"),
|
|
212
|
-
repo_root=self.repo_root,
|
|
213
|
-
console=None, # silent — no output in chat
|
|
214
|
-
gitignore_spec=self.gitignore_spec,
|
|
215
|
-
context_lines=arguments.get("context_lines", 3),
|
|
216
|
-
vault_root=vault_root_str(),
|
|
217
|
-
)
|
|
218
|
-
try:
|
|
219
|
-
_execute_edit_file(**kwargs)
|
|
220
|
-
return True
|
|
221
|
-
except Exception as e:
|
|
222
|
-
logger.warning("Memory edit failed (retrying in 0.5s): %s", e)
|
|
223
|
-
time.sleep(0.5)
|
|
224
|
-
try:
|
|
225
|
-
_execute_edit_file(**kwargs)
|
|
226
|
-
logger.info("Memory edit retry succeeded after initial failure.")
|
|
227
|
-
return True
|
|
228
|
-
except Exception as e2:
|
|
229
|
-
logger.error("Memory edit failed after retry: %s", e2)
|
|
230
|
-
return False
|
|
231
|
-
|
|
232
|
-
def run(self, user_input, thinking_indicator=None, allowed_tools=None):
|
|
189
|
+
def run(self, user_input, thinking_indicator=None, allowed_tools=None, allow_active_plugins=False):
|
|
233
190
|
"""Main orchestration loop.
|
|
234
191
|
|
|
235
192
|
Args:
|
|
236
193
|
user_input: User's input message
|
|
237
194
|
thinking_indicator: Optional ThinkingIndicator instance
|
|
238
195
|
allowed_tools: Optional list of allowed tool names (for research)
|
|
196
|
+
allow_active_plugins: Whether to include active plugin tools in restricted runs
|
|
239
197
|
"""
|
|
198
|
+
self._current_allowed_tools = allowed_tools
|
|
199
|
+
self._current_allow_active_plugins = allow_active_plugins
|
|
200
|
+
|
|
240
201
|
# Append user message
|
|
241
202
|
self.chat_manager.messages.append({"role": "user", "content": user_input})
|
|
242
203
|
|
|
243
204
|
# Log user message
|
|
244
205
|
self.chat_manager.log_message({"role": "user", "content": user_input})
|
|
245
206
|
|
|
246
|
-
from tools.base import ToolRegistry
|
|
207
|
+
from tools.helpers.base import ToolRegistry
|
|
247
208
|
|
|
248
209
|
while True:
|
|
249
210
|
# Decrement plugin TTLs after previous iteration's tool execution.
|
|
@@ -253,7 +214,10 @@ class AgenticOrchestrator:
|
|
|
253
214
|
self.console.print(f"[dim]Plugins evicted (TTL expired): {evicted}[/dim]")
|
|
254
215
|
|
|
255
216
|
# Get response from LLM
|
|
256
|
-
response = self._get_llm_response(
|
|
217
|
+
response = self._get_llm_response(
|
|
218
|
+
allowed_tools=allowed_tools,
|
|
219
|
+
allow_active_plugins=allow_active_plugins,
|
|
220
|
+
)
|
|
257
221
|
if response is None:
|
|
258
222
|
return
|
|
259
223
|
|
|
@@ -267,11 +231,16 @@ class AgenticOrchestrator:
|
|
|
267
231
|
if self._handle_final_response(response, thinking_indicator):
|
|
268
232
|
return
|
|
269
233
|
else:
|
|
270
|
-
should_exit = self._handle_tool_calls(
|
|
234
|
+
should_exit = self._handle_tool_calls(
|
|
235
|
+
response,
|
|
236
|
+
thinking_indicator,
|
|
237
|
+
allowed_tools,
|
|
238
|
+
allow_active_plugins=allow_active_plugins,
|
|
239
|
+
)
|
|
271
240
|
if should_exit:
|
|
272
241
|
return
|
|
273
242
|
|
|
274
|
-
def _get_llm_response(self, allowed_tools=None):
|
|
243
|
+
def _get_llm_response(self, allowed_tools=None, allow_active_plugins=False):
|
|
275
244
|
"""Get next LLM response with tool definitions.
|
|
276
245
|
|
|
277
246
|
Includes automatic retry with live countdown for timeout/connection errors.
|
|
@@ -279,6 +248,7 @@ class AgenticOrchestrator:
|
|
|
279
248
|
|
|
280
249
|
Args:
|
|
281
250
|
allowed_tools: Optional list of allowed tool names (overrides mode-based filtering)
|
|
251
|
+
allow_active_plugins: Whether to include active plugin tools in restricted runs
|
|
282
252
|
|
|
283
253
|
Returns:
|
|
284
254
|
Response dict from LLM, or None if error occurred
|
|
@@ -287,19 +257,17 @@ class AgenticOrchestrator:
|
|
|
287
257
|
self.chat_manager.ensure_context_fits(console=self.console)
|
|
288
258
|
|
|
289
259
|
# Use allowed_tools if provided, otherwise use mode-based filtering
|
|
290
|
-
if allowed_tools is not None:
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
else:
|
|
302
|
-
tools = TOOLS()
|
|
260
|
+
if allowed_tools is not None and not allowed_tools and not allow_active_plugins:
|
|
261
|
+
self.console.print("[red]Error: allowed_tools is empty[/red]")
|
|
262
|
+
return None
|
|
263
|
+
|
|
264
|
+
tools = self._get_effective_tools(
|
|
265
|
+
allowed_tools=allowed_tools,
|
|
266
|
+
allow_active_plugins=allow_active_plugins,
|
|
267
|
+
)
|
|
268
|
+
if allowed_tools is not None and self.debug_mode:
|
|
269
|
+
tool_names = [t["function"]["name"] for t in tools]
|
|
270
|
+
self.console.print(f"[dim]Available tools: {tool_names}[/dim]")
|
|
303
271
|
|
|
304
272
|
# Retry loop for timeout/connection errors
|
|
305
273
|
last_error = None
|
|
@@ -320,7 +288,21 @@ class AgenticOrchestrator:
|
|
|
320
288
|
continue
|
|
321
289
|
else:
|
|
322
290
|
# Non-retryable error or final attempt exhausted
|
|
291
|
+
detail_lines = []
|
|
292
|
+
for key, value in getattr(e, "details", {}).items():
|
|
293
|
+
value_str = str(value)
|
|
294
|
+
if "\n" in value_str or key == "original_error":
|
|
295
|
+
detail_lines.append(f"{key}: {value_str}")
|
|
296
|
+
detailed_error = str(e)
|
|
297
|
+
if detail_lines:
|
|
298
|
+
detailed_error += "\n\n" + "\n\n".join(detail_lines)
|
|
299
|
+
|
|
300
|
+
if self.is_sub_agent:
|
|
301
|
+
raise LLMError(detailed_error, details=getattr(e, "details", {}))
|
|
302
|
+
|
|
323
303
|
self.console.print(f"[red]LLM Error: {e}[/red]")
|
|
304
|
+
if detail_lines:
|
|
305
|
+
self.console.print(f"[dim]{detail_lines[0]}[/dim]", markup=False)
|
|
324
306
|
return None
|
|
325
307
|
|
|
326
308
|
# Successful response — parse and return
|
|
@@ -382,8 +364,11 @@ class AgenticOrchestrator:
|
|
|
382
364
|
# NEW: Compact tool results after final answer (per-message compaction)
|
|
383
365
|
self.chat_manager.compact_tool_results(skip_token_update=True)
|
|
384
366
|
|
|
385
|
-
# Update context tokens with current
|
|
386
|
-
tools_for_mode =
|
|
367
|
+
# Update context tokens with current run's effective tools
|
|
368
|
+
tools_for_mode = self._get_effective_tools(
|
|
369
|
+
allowed_tools=getattr(self, "_current_allowed_tools", None),
|
|
370
|
+
allow_active_plugins=getattr(self, "_current_allow_active_plugins", False),
|
|
371
|
+
)
|
|
387
372
|
self.chat_manager._update_context_tokens(tools_for_mode)
|
|
388
373
|
|
|
389
374
|
self.console.print()
|
|
@@ -395,13 +380,14 @@ class AgenticOrchestrator:
|
|
|
395
380
|
)
|
|
396
381
|
return not should_continue
|
|
397
382
|
|
|
398
|
-
def _handle_tool_calls(self, response, thinking_indicator, allowed_tools=None):
|
|
383
|
+
def _handle_tool_calls(self, response, thinking_indicator, allowed_tools=None, allow_active_plugins=False):
|
|
399
384
|
"""Process tool calls and display accompanying content.
|
|
400
385
|
|
|
401
386
|
Args:
|
|
402
387
|
response: Full message dict from LLM (includes content and tool_calls)
|
|
403
388
|
thinking_indicator: Optional ThinkingIndicator instance
|
|
404
389
|
allowed_tools: Optional list of allowed tool names
|
|
390
|
+
allow_active_plugins: Whether to allow active plugin tools in restricted runs
|
|
405
391
|
|
|
406
392
|
Returns:
|
|
407
393
|
True if should exit the orchestration loop
|
|
@@ -415,6 +401,8 @@ class AgenticOrchestrator:
|
|
|
415
401
|
# This must happen BEFORE filtering so the LLM sees its original intent
|
|
416
402
|
content = (response.get("content") or "").strip()
|
|
417
403
|
assistant_msg = {"role": "assistant", "tool_calls": tool_calls}
|
|
404
|
+
if response.get("_responses_output"):
|
|
405
|
+
assistant_msg["_responses_output"] = response["_responses_output"]
|
|
418
406
|
if content:
|
|
419
407
|
assistant_msg["content"] = content
|
|
420
408
|
self.chat_manager.messages.append(assistant_msg)
|
|
@@ -425,7 +413,7 @@ class AgenticOrchestrator:
|
|
|
425
413
|
# This silently removes unknown tools or tools not in the allowed whitelist
|
|
426
414
|
# to prevent error messages from reaching the user while allowing the agent
|
|
427
415
|
# to continue with alternative tools.
|
|
428
|
-
from tools.base import ToolRegistry
|
|
416
|
+
from tools.helpers.base import ToolRegistry
|
|
429
417
|
|
|
430
418
|
filtered_calls = []
|
|
431
419
|
filtered_tool_ids = [] # Track filtered tool IDs to provide feedback
|
|
@@ -442,10 +430,14 @@ class AgenticOrchestrator:
|
|
|
442
430
|
filtered_tool_ids.append(tool_call.get("id"))
|
|
443
431
|
continue
|
|
444
432
|
|
|
445
|
-
# Check if tool is in
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
433
|
+
# Check if tool is in the effective allowlist for this run.
|
|
434
|
+
effective_allowed_tools = None
|
|
435
|
+
if allowed_tools is not None:
|
|
436
|
+
effective_allowed_tools = set(allowed_tools)
|
|
437
|
+
if allow_active_plugins:
|
|
438
|
+
effective_allowed_tools.update(ToolRegistry.active_plugin_names())
|
|
439
|
+
|
|
440
|
+
if effective_allowed_tools is not None and function_name not in effective_allowed_tools:
|
|
449
441
|
# Silent fail - skip this tool
|
|
450
442
|
if self.debug_mode:
|
|
451
443
|
self.console.print(f"[dim]Silently filtered non-allowed tool: {function_name}[/dim]")
|
|
@@ -600,8 +592,11 @@ class AgenticOrchestrator:
|
|
|
600
592
|
# Compact completed tool blocks once after all tools complete
|
|
601
593
|
self.chat_manager.compact_tool_results(skip_token_update=True)
|
|
602
594
|
|
|
603
|
-
# Update context tokens with current
|
|
604
|
-
tools_for_mode =
|
|
595
|
+
# Update context tokens with current run's effective tools
|
|
596
|
+
tools_for_mode = self._get_effective_tools(
|
|
597
|
+
allowed_tools=getattr(self, "_current_allowed_tools", None),
|
|
598
|
+
allow_active_plugins=getattr(self, "_current_allow_active_plugins", False),
|
|
599
|
+
)
|
|
605
600
|
self.chat_manager._update_context_tokens(tools_for_mode)
|
|
606
601
|
|
|
607
602
|
# Pre-send guard: ensure context fits before next LLM call
|
|
@@ -621,7 +616,7 @@ class AgenticOrchestrator:
|
|
|
621
616
|
"""
|
|
622
617
|
if not tool_calls:
|
|
623
618
|
return False
|
|
624
|
-
from tools.parallel_executor import ParallelToolExecutor, ToolCall
|
|
619
|
+
from tools.helpers.parallel_executor import ParallelToolExecutor, ToolCall
|
|
625
620
|
|
|
626
621
|
# Suppress console output in handlers during parallel execution
|
|
627
622
|
# We'll display results ourselves in order below
|
|
@@ -841,8 +836,11 @@ class AgenticOrchestrator:
|
|
|
841
836
|
# after all parallel results are appended (safe — only compacts completed blocks)
|
|
842
837
|
self.chat_manager.compact_tool_results(skip_token_update=True)
|
|
843
838
|
|
|
844
|
-
# Update context tokens with current
|
|
845
|
-
tools_for_mode =
|
|
839
|
+
# Update context tokens with current run's effective tools
|
|
840
|
+
tools_for_mode = self._get_effective_tools(
|
|
841
|
+
allowed_tools=getattr(self, "_current_allowed_tools", None),
|
|
842
|
+
allow_active_plugins=getattr(self, "_current_allow_active_plugins", False),
|
|
843
|
+
)
|
|
846
844
|
self.chat_manager._update_context_tokens(tools_for_mode)
|
|
847
845
|
|
|
848
846
|
# Pre-send guard: ensure context fits before next LLM call
|
|
@@ -853,6 +851,38 @@ class AgenticOrchestrator:
|
|
|
853
851
|
# Restore console output
|
|
854
852
|
self._parallel_context['console'] = self.console
|
|
855
853
|
|
|
854
|
+
def _boundary_prompt(self, path_str):
|
|
855
|
+
"""Prompt the user to grant filesystem access for a path outside boundaries.
|
|
856
|
+
|
|
857
|
+
Called after a tool returns a boundary error. If the user grants access,
|
|
858
|
+
the caller retries the tool with the boundary lifted.
|
|
859
|
+
|
|
860
|
+
Args:
|
|
861
|
+
path_str: The path that triggered the boundary violation.
|
|
862
|
+
|
|
863
|
+
Returns:
|
|
864
|
+
True if user granted access, False if denied.
|
|
865
|
+
"""
|
|
866
|
+
if self.is_sub_agent:
|
|
867
|
+
return False
|
|
868
|
+
|
|
869
|
+
console = self._get_console()
|
|
870
|
+
if console is None:
|
|
871
|
+
return False
|
|
872
|
+
|
|
873
|
+
from ui.tool_confirmation import ToolConfirmationPanel
|
|
874
|
+
panel = ToolConfirmationPanel(
|
|
875
|
+
'Grant filesystem access',
|
|
876
|
+
reason=f'Agent requested access outside project boundary: {path_str}',
|
|
877
|
+
is_edit_tool=False
|
|
878
|
+
)
|
|
879
|
+
action, _ = panel.run()
|
|
880
|
+
|
|
881
|
+
if action == "accept":
|
|
882
|
+
console.print("[yellow]Full filesystem access granted[/yellow]\n")
|
|
883
|
+
return True
|
|
884
|
+
return False
|
|
885
|
+
|
|
856
886
|
def _process_single_tool_call(self, tool_call, thinking_indicator):
|
|
857
887
|
"""Process a single tool call.
|
|
858
888
|
|
|
@@ -884,7 +914,7 @@ class AgenticOrchestrator:
|
|
|
884
914
|
panel_to_use = SubAgentPanel(query, self.console)
|
|
885
915
|
|
|
886
916
|
# Execute via tool registry
|
|
887
|
-
from tools.base import ToolRegistry, build_context
|
|
917
|
+
from tools.helpers.base import ToolRegistry, build_context
|
|
888
918
|
|
|
889
919
|
tool = ToolRegistry.get(function_name)
|
|
890
920
|
if tool:
|
|
@@ -908,26 +938,12 @@ class AgenticOrchestrator:
|
|
|
908
938
|
|
|
909
939
|
# Check if tool requires approval
|
|
910
940
|
if tool.requires_approval:
|
|
911
|
-
# For edit_file:
|
|
941
|
+
# For edit_file: validate path then request approval
|
|
912
942
|
if function_name == "edit_file":
|
|
913
943
|
edit_path = arguments.get("path", "")
|
|
914
944
|
if not edit_path:
|
|
915
945
|
return False, "Error: path is required for edit_file."
|
|
916
946
|
|
|
917
|
-
# Memory file: auto-approve, fire-and-forget
|
|
918
|
-
if self._is_memory_file(edit_path):
|
|
919
|
-
# Generate preview to validate the edit (reuses existing logic)
|
|
920
|
-
result = tool.execute(arguments, context)
|
|
921
|
-
preview, is_valid = resolve_edit_preview(result)
|
|
922
|
-
if is_valid:
|
|
923
|
-
ok = self._execute_memory_edit(arguments)
|
|
924
|
-
if self.debug_mode:
|
|
925
|
-
console = self._get_console()
|
|
926
|
-
if console:
|
|
927
|
-
console.print(f"[dim]Memory edit auto-approved: {edit_path}[/dim]")
|
|
928
|
-
return False, "Memory saved." if ok else f"Memory edit failed: {edit_path}"
|
|
929
|
-
return False, str(result)
|
|
930
|
-
|
|
931
947
|
# Normal edit: generate preview and request approval
|
|
932
948
|
result = tool.execute(arguments, context)
|
|
933
949
|
|
|
@@ -988,6 +1004,20 @@ class AgenticOrchestrator:
|
|
|
988
1004
|
if policy == TERMINAL_YIELD and thinking_indicator:
|
|
989
1005
|
thinking_indicator.resume()
|
|
990
1006
|
|
|
1007
|
+
# Boundary escalation: if the tool result is a path boundary
|
|
1008
|
+
# violation, prompt the user to grant session-wide access.
|
|
1009
|
+
result_str = str(result)
|
|
1010
|
+
if is_boundary_error(result_str):
|
|
1011
|
+
path_arg = arguments.get("path", arguments.get("path_str", ""))
|
|
1012
|
+
if not path_arg:
|
|
1013
|
+
path_arg = extract_boundary_path(result_str)
|
|
1014
|
+
granted = self._boundary_prompt(path_arg)
|
|
1015
|
+
if granted:
|
|
1016
|
+
set_full_filesystem_access(True)
|
|
1017
|
+
# Retry with the boundary now lifted
|
|
1018
|
+
result = tool.execute(arguments, context)
|
|
1019
|
+
result_str = str(result)
|
|
1020
|
+
|
|
991
1021
|
# Display result for registry tools
|
|
992
1022
|
# Skip display for tools that take over the terminal (they handle their own display)
|
|
993
1023
|
if policy != TERMINAL_YIELD:
|
|
@@ -1020,7 +1050,7 @@ class AgenticOrchestrator:
|
|
|
1020
1050
|
# Then display feedback
|
|
1021
1051
|
display_tool_feedback(label, result, console, indent=self.is_sub_agent, panel_updater=self.panel_updater)
|
|
1022
1052
|
|
|
1023
|
-
return False,
|
|
1053
|
+
return False, result_str
|
|
1024
1054
|
except Exception as e:
|
|
1025
1055
|
# If thinking_indicator was paused (TERMINAL_YIELD) and tool
|
|
1026
1056
|
# raised, resume it so the spinner reappears for the next iteration
|
|
@@ -1053,5 +1083,3 @@ def agentic_answer(chat_manager, user_input, console, repo_root, rg_exe_path, de
|
|
|
1053
1083
|
debug_mode=debug_mode,
|
|
1054
1084
|
)
|
|
1055
1085
|
orchestrator.run(user_input, thinking_indicator)
|
|
1056
|
-
|
|
1057
|
-
|
package/src/core/chat_manager.py
CHANGED
|
@@ -9,12 +9,14 @@ import requests
|
|
|
9
9
|
from typing import Optional, IO
|
|
10
10
|
|
|
11
11
|
from llm.client import LLMClient
|
|
12
|
-
from llm.config import get_providers, get_provider_config, reload_config
|
|
12
|
+
from llm.config import get_providers, get_provider_config, get_provider_display_name, reload_config
|
|
13
13
|
from llm.prompts import build_system_prompt
|
|
14
|
+
from core.skills import render_active_skills_section
|
|
14
15
|
from pathlib import Path
|
|
15
16
|
from llm.token_tracker import TokenTracker
|
|
16
17
|
from utils.settings import server_settings, context_settings
|
|
17
18
|
from utils.logger import MarkdownConversationLogger
|
|
19
|
+
from utils.user_message_logger import UserMessageLogger
|
|
18
20
|
from utils.result_parsers import extract_exit_code, extract_metadata_from_result
|
|
19
21
|
|
|
20
22
|
# Token counting constants
|
|
@@ -44,6 +46,10 @@ class ChatManager:
|
|
|
44
46
|
self.task_list = []
|
|
45
47
|
self.task_list_title = None
|
|
46
48
|
|
|
49
|
+
# In-session active skill tracking. These skills are rendered into the
|
|
50
|
+
# system prompt for the current chat.
|
|
51
|
+
self.loaded_skills = set()
|
|
52
|
+
|
|
47
53
|
# .gitignore filtering state
|
|
48
54
|
self._gitignore_spec = None
|
|
49
55
|
self._gitignore_mtime = None
|
|
@@ -62,6 +68,9 @@ class ChatManager:
|
|
|
62
68
|
conversations_dir=context_settings.conversations_dir
|
|
63
69
|
)
|
|
64
70
|
|
|
71
|
+
# User message logging (always on, for dream memory system)
|
|
72
|
+
self.user_message_logger = UserMessageLogger()
|
|
73
|
+
|
|
65
74
|
# Compaction lock: prevents compaction during active tool execution
|
|
66
75
|
# Set by agentic.py before executing tools, cleared after all results appended
|
|
67
76
|
self._compaction_locked = False
|
|
@@ -90,6 +99,9 @@ class ChatManager:
|
|
|
90
99
|
if self.markdown_logger:
|
|
91
100
|
self.markdown_logger.start_session()
|
|
92
101
|
|
|
102
|
+
# Active skills are scoped to the current message history/session.
|
|
103
|
+
self.loaded_skills = set()
|
|
104
|
+
|
|
93
105
|
# Start with system prompt only
|
|
94
106
|
self.messages = [{"role": "system", "content": self._build_system_prompt()}]
|
|
95
107
|
|
|
@@ -119,20 +131,38 @@ class ChatManager:
|
|
|
119
131
|
self._update_context_tokens()
|
|
120
132
|
self.context_token_estimate = self.token_tracker.current_context_tokens
|
|
121
133
|
|
|
122
|
-
def _build_system_prompt(self) -> str:
|
|
123
|
-
"""Build system prompt.
|
|
124
|
-
|
|
134
|
+
def _build_system_prompt(self, variant: str | None = None) -> str:
|
|
135
|
+
"""Build system prompt.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
variant: Prompt variant name (e.g. 'main', 'micro').
|
|
139
|
+
If None, reads from prompt_settings.
|
|
140
|
+
"""
|
|
141
|
+
if variant is None:
|
|
142
|
+
from utils.settings import prompt_settings
|
|
143
|
+
variant = prompt_settings.variant
|
|
144
|
+
active_skills_section = render_active_skills_section(self.loaded_skills)
|
|
145
|
+
return build_system_prompt(variant, active_skills_section=active_skills_section)
|
|
146
|
+
|
|
147
|
+
def update_system_prompt(self, variant: str | None = None):
|
|
148
|
+
"""Rebuild system prompt in-place (e.g. after hotswap or session reset).
|
|
125
149
|
|
|
126
|
-
|
|
127
|
-
|
|
150
|
+
Args:
|
|
151
|
+
variant: Prompt variant to use. If None, keeps current variant.
|
|
152
|
+
Updates token_tracker.current_variant.
|
|
153
|
+
"""
|
|
128
154
|
if not self.messages:
|
|
129
155
|
raise RuntimeError("Cannot update system prompt: messages array is empty")
|
|
130
156
|
|
|
131
157
|
if self.messages[0]["role"] != "system":
|
|
132
158
|
raise RuntimeError(f"Cannot update system prompt: messages[0] has role '{self.messages[0]['role']}', expected 'system'")
|
|
133
159
|
|
|
134
|
-
|
|
135
|
-
|
|
160
|
+
if variant is None:
|
|
161
|
+
from utils.settings import prompt_settings
|
|
162
|
+
variant = prompt_settings.variant
|
|
163
|
+
|
|
164
|
+
self.messages[0]["content"] = self._build_system_prompt(variant)
|
|
165
|
+
self.token_tracker.current_variant = variant
|
|
136
166
|
self._update_context_tokens()
|
|
137
167
|
|
|
138
168
|
def _load_agents_md(self) -> tuple[str, str]:
|
|
@@ -1305,7 +1335,8 @@ Provide a concise summary (2-4 paragraphs) that captures all essential context f
|
|
|
1305
1335
|
"""
|
|
1306
1336
|
providers = get_providers()
|
|
1307
1337
|
if provider_name not in providers:
|
|
1308
|
-
|
|
1338
|
+
available = ', '.join(get_provider_display_name(provider) for provider in providers)
|
|
1339
|
+
return f"Invalid provider. Use /provider to list. Available: {available}"
|
|
1309
1340
|
|
|
1310
1341
|
previous_provider = self.client.provider
|
|
1311
1342
|
|
|
@@ -1321,10 +1352,13 @@ Provide a concise summary (2-4 paragraphs) that captures all essential context f
|
|
|
1321
1352
|
# Failed to start server - revert
|
|
1322
1353
|
self.client.switch_provider(previous_provider)
|
|
1323
1354
|
self._init_messages(reset_costs=True)
|
|
1324
|
-
|
|
1355
|
+
previous_label = get_provider_display_name(previous_provider)
|
|
1356
|
+
return f"Failed to start local server. Reverted to {previous_label} provider."
|
|
1325
1357
|
self.server_process = server
|
|
1326
|
-
|
|
1327
|
-
|
|
1358
|
+
provider_label = get_provider_display_name(provider_name)
|
|
1359
|
+
return f"Switched to {provider_label} provider (server ready)."
|
|
1360
|
+
provider_label = get_provider_display_name(provider_name)
|
|
1361
|
+
return f"Switched to {provider_label} provider."
|
|
1328
1362
|
return "Provider switch failed."
|
|
1329
1363
|
|
|
1330
1364
|
def reload_config(self):
|
|
@@ -1376,6 +1410,10 @@ Provide a concise summary (2-4 paragraphs) that captures all essential context f
|
|
|
1376
1410
|
server_path,
|
|
1377
1411
|
"-m", model_path,
|
|
1378
1412
|
"-ngl", str(server_settings.ngl_layers),
|
|
1413
|
+
"--threads", str(server_settings.threads),
|
|
1414
|
+
"--batch-size", str(server_settings.batch_size),
|
|
1415
|
+
"--ubatch-size", str(server_settings.ubatch_size),
|
|
1416
|
+
"--flash-attn" if server_settings.flash_attn else "--no-flash-attn",
|
|
1379
1417
|
"--split-mode", "none",
|
|
1380
1418
|
"--ctx-size", str(server_settings.ctx_size),
|
|
1381
1419
|
"--n-predict", str(server_settings.n_predict),
|
|
@@ -1383,6 +1421,7 @@ Provide a concise summary (2-4 paragraphs) that captures all essential context f
|
|
|
1383
1421
|
"--host", host,
|
|
1384
1422
|
"--port", str(port),
|
|
1385
1423
|
"--jinja",
|
|
1424
|
+
"--reasoning", "off",
|
|
1386
1425
|
]
|
|
1387
1426
|
|
|
1388
1427
|
# Restrict to RTX 5070 Ti only (GPU 0)
|
|
@@ -1459,6 +1498,15 @@ Provide a concise summary (2-4 paragraphs) that captures all essential context f
|
|
|
1459
1498
|
if self.markdown_logger:
|
|
1460
1499
|
self.markdown_logger.log_message(message)
|
|
1461
1500
|
|
|
1501
|
+
# Log user messages to JSONL for dream memory processing (only if memory enabled)
|
|
1502
|
+
if message.get("role") == "user" and message.get("content"):
|
|
1503
|
+
from llm.config import MEMORY_SETTINGS
|
|
1504
|
+
if MEMORY_SETTINGS.get("enabled", True):
|
|
1505
|
+
self.user_message_logger.log_user_message(
|
|
1506
|
+
message["content"],
|
|
1507
|
+
project_dir=Path.cwd().resolve(),
|
|
1508
|
+
)
|
|
1509
|
+
|
|
1462
1510
|
def sync_log(self):
|
|
1463
1511
|
"""Rewrite the entire conversation log to match current message state.
|
|
1464
1512
|
|
|
@@ -36,12 +36,16 @@ class ConfigManager:
|
|
|
36
36
|
with open(self.config_path, 'r', encoding='utf-8-sig') as f:
|
|
37
37
|
self._cached_data = yaml.safe_load(f) or {}
|
|
38
38
|
|
|
39
|
-
# Migrate
|
|
39
|
+
# Migrate legacy provider IDs to current names.
|
|
40
40
|
old_provider = self._cached_data.get('LAST_PROVIDER')
|
|
41
41
|
if old_provider in ('vmcode_proxy', 'vmcode_free', 'vmcode'):
|
|
42
42
|
logger.info("Migrating provider name '%s' -> 'bone'", old_provider)
|
|
43
43
|
self._cached_data['LAST_PROVIDER'] = 'bone'
|
|
44
44
|
self.save(self._cached_data, create_backup=True)
|
|
45
|
+
elif old_provider == 'codex_plan':
|
|
46
|
+
logger.info("Migrating provider name '%s' -> 'codex'", old_provider)
|
|
47
|
+
self._cached_data['LAST_PROVIDER'] = 'codex'
|
|
48
|
+
self.save(self._cached_data, create_backup=True)
|
|
45
49
|
|
|
46
50
|
return self._cached_data
|
|
47
51
|
except yaml.YAMLError as e:
|
|
@@ -114,11 +118,14 @@ class ConfigManager:
|
|
|
114
118
|
if model is None:
|
|
115
119
|
provider_model_map = {
|
|
116
120
|
'bone': 'BONE_PROXY_MODEL',
|
|
121
|
+
'codex': 'CODEX_PLAN_MODEL',
|
|
117
122
|
'openrouter': 'OPENROUTER_MODEL',
|
|
118
123
|
'glm': 'GLM_MODEL',
|
|
124
|
+
'glm_plan': 'GLM_PLAN_MODEL',
|
|
119
125
|
'openai': 'OPENAI_MODEL',
|
|
120
126
|
'gemini': 'GEMINI_MODEL',
|
|
121
127
|
'minimax': 'MINIMAX_MODEL',
|
|
128
|
+
'minimax_plan': 'MINIMAX_PLAN_MODEL',
|
|
122
129
|
'anthropic': 'ANTHROPIC_MODEL',
|
|
123
130
|
'kimi': 'KIMI_MODEL'
|
|
124
131
|
}
|
|
@@ -142,11 +149,14 @@ class ConfigManager:
|
|
|
142
149
|
provider_keys = {
|
|
143
150
|
'local': 'LOCAL_MODEL_PATH',
|
|
144
151
|
'bone': 'BONE_PROXY_MODEL',
|
|
152
|
+
'codex': 'CODEX_PLAN_MODEL',
|
|
145
153
|
'openrouter': 'OPENROUTER_MODEL',
|
|
146
154
|
'glm': 'GLM_MODEL',
|
|
155
|
+
'glm_plan': 'GLM_PLAN_MODEL',
|
|
147
156
|
'openai': 'OPENAI_MODEL',
|
|
148
157
|
'gemini': 'GEMINI_MODEL',
|
|
149
158
|
'minimax': 'MINIMAX_MODEL',
|
|
159
|
+
'minimax_plan': 'MINIMAX_PLAN_MODEL',
|
|
150
160
|
'anthropic': 'ANTHROPIC_MODEL',
|
|
151
161
|
'kimi': 'KIMI_MODEL'
|
|
152
162
|
}
|
|
@@ -171,10 +181,13 @@ class ConfigManager:
|
|
|
171
181
|
provider_keys = {
|
|
172
182
|
'openrouter': 'OPENROUTER_API_KEY',
|
|
173
183
|
'bone': 'BONE_PROXY_API_KEY',
|
|
184
|
+
'codex': 'CODEX_PLAN_API_KEY',
|
|
174
185
|
'glm': 'GLM_API_KEY',
|
|
186
|
+
'glm_plan': 'GLM_PLAN_API_KEY',
|
|
175
187
|
'openai': 'OPENAI_API_KEY',
|
|
176
188
|
'gemini': 'GEMINI_API_KEY',
|
|
177
189
|
'minimax': 'MINIMAX_API_KEY',
|
|
190
|
+
'minimax_plan': 'MINIMAX_PLAN_API_KEY',
|
|
178
191
|
'anthropic': 'ANTHROPIC_API_KEY',
|
|
179
192
|
'kimi': 'KIMI_API_KEY'
|
|
180
193
|
}
|