bone-agent 1.3.3 → 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 +17 -0
- package/config.yaml.example +5 -2
- package/package.json +1 -1
- package/prompts/main/communication_style.md +1 -1
- package/prompts/main/dream.md +23 -9
- package/prompts/main/skills.md +3 -0
- package/prompts/micro/communication_style.md +1 -1
- package/prompts/micro/skills.md +1 -0
- package/src/core/agentic.py +138 -38
- package/src/core/chat_manager.py +19 -6
- package/src/core/config_manager.py +8 -1
- package/src/core/cron.py +0 -4
- 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 +46 -2
- package/src/llm/prompts.py +12 -7
- package/src/llm/providers.py +3 -1
- package/src/llm/token_tracker.py +15 -0
- 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 +5 -1
- 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 +54 -3
- package/src/tools/helpers/plugin_manifest.py +99 -70
- package/src/tools/review_sub_agent.py +2 -1
- package/src/tools/rg_search.py +24 -7
- package/src/tools/search_plugins.py +140 -72
- package/src/tools/shell.py +3 -3
- package/src/ui/commands.py +355 -33
- package/src/ui/displays.py +26 -1
- package/src/ui/main.py +0 -4
- package/src/ui/tool_confirmation.py +16 -5
- package/src/utils/editor.py +88 -39
- package/src/utils/settings.py +6 -2
- package/src/utils/validation.py +10 -0
package/README.md
CHANGED
|
@@ -130,8 +130,25 @@ bone
|
|
|
130
130
|
- `/account` - View your bone-agent account and plan details
|
|
131
131
|
- `/plan` - View available plans and pricing
|
|
132
132
|
- `/upgrade` - Upgrade your subscription
|
|
133
|
+
- `/skills list [query]` - List saved skills, optionally filtered by name or content
|
|
134
|
+
- `/skills show <name>` - Display a saved skill
|
|
135
|
+
- `/skills add <name>` - Create a reusable prompt skill in your editor
|
|
136
|
+
- `/skills edit <name>` - Open a saved skill in your editor
|
|
137
|
+
- `/skills modify <name> [prompt]` - Update an existing saved skill inline or in your editor
|
|
138
|
+
- `/skills load <name>` - Load a saved skill into the current chat
|
|
139
|
+
- `/skills use <name>` - Alias for `/skills load`
|
|
140
|
+
- `/skills remove <name>` - Delete a saved skill
|
|
141
|
+
- `/skills dir` - Print the skills directory path
|
|
133
142
|
- `/help` - Display all available commands
|
|
134
143
|
|
|
144
|
+
Example:
|
|
145
|
+
|
|
146
|
+
```text
|
|
147
|
+
/skills add frontend_design
|
|
148
|
+
/skills modify frontend_design Use restrained, production-quality UI patterns.
|
|
149
|
+
/skills use frontend_design
|
|
150
|
+
```
|
|
151
|
+
|
|
135
152
|
/help Menu:
|
|
136
153
|
<img width="1843" height="1349" alt="image" src="https://github.com/user-attachments/assets/631ab805-f012-4bb6-a031-c82a339e94c5" />
|
|
137
154
|
|
package/config.yaml.example
CHANGED
|
@@ -104,8 +104,11 @@ CONTEXT_SETTINGS:
|
|
|
104
104
|
# -- Sub-Agent Settings -------------------------------------------------------
|
|
105
105
|
|
|
106
106
|
SUB_AGENT_SETTINGS:
|
|
107
|
-
soft_limit_tokens:
|
|
108
|
-
hard_limit_tokens:
|
|
107
|
+
soft_limit_tokens: 100000
|
|
108
|
+
hard_limit_tokens: 150000
|
|
109
|
+
billed_warning_tokens: 200000
|
|
110
|
+
billed_hard_limit_tokens: 500000
|
|
111
|
+
compact_trigger_tokens: 50000
|
|
109
112
|
enable_compaction: false
|
|
110
113
|
dump_context_on_hard_limit: true
|
|
111
114
|
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
**Important:** Default to concise explanations
|
|
4
4
|
|
|
5
5
|
- Show only changed code snippets when making edits via tools, never in explanations
|
|
6
|
-
- Use bullet points
|
|
6
|
+
- Use bullet points where necessary, not as default
|
|
7
7
|
- Target: 3-5 sentences max for explanations, 10-15 lines max for plans
|
|
8
8
|
- Explain the "why" and "what", skip the "how" unless requested
|
|
9
9
|
|
package/prompts/main/dream.md
CHANGED
|
@@ -10,13 +10,26 @@ You are the dream agent — a background process that consolidates user messages
|
|
|
10
10
|
b. If the project directory is found, read its project memory at `{project_dir}/.bone/agents.md`
|
|
11
11
|
c. If the project directory cannot be resolved, treat those messages as user-level only
|
|
12
12
|
3. Read the current user memory at `~/.bone/user_memory.md`
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
13
|
+
|
|
14
|
+
## What to remember
|
|
15
|
+
|
|
16
|
+
Memory exists to change how the agent behaves in future conversations. Before writing anything, ask: "Would knowing this actually change my behavior next time we talk?"
|
|
17
|
+
|
|
18
|
+
### High-value — write these
|
|
19
|
+
- Explicit "remember this" or "don't forget" requests
|
|
20
|
+
- Strong, repeated preferences the user has expressed multiple times or with emphasis
|
|
21
|
+
- Corrections the user gave after the agent did something wrong ("I don't like X, do Y instead")
|
|
22
|
+
- Hard constraints ("never do X", "always do Y")
|
|
23
|
+
|
|
24
|
+
### Low-value — do NOT write these
|
|
25
|
+
- One-off casual remarks that weren't emphasized or repeated
|
|
26
|
+
- Descriptions of the user's workflow that are just normal tool usage
|
|
27
|
+
- Feature implementation history ("added X to the config command")
|
|
28
|
+
- Things the agent can infer from context or that apply to most users
|
|
29
|
+
- Multiple entries saying the same thing in different words
|
|
30
|
+
|
|
31
|
+
### The bar
|
|
32
|
+
A single mention is usually not enough. Look for emphasis, repetition, or explicit instruction. When in doubt, don't write. Empty memory is better than noisy memory.
|
|
20
33
|
|
|
21
34
|
## Routing
|
|
22
35
|
|
|
@@ -27,10 +40,11 @@ You are the dream agent — a background process that consolidates user messages
|
|
|
27
40
|
## Rules
|
|
28
41
|
|
|
29
42
|
- Only write facts, preferences, and patterns — never private data, code snippets, or transient context
|
|
30
|
-
- Deduplicate aggressively — if a preference already exists in memory, don't add it again
|
|
43
|
+
- Deduplicate aggressively — if a preference already exists in memory, don't add it again. Merge near-duplicates into one entry.
|
|
31
44
|
- Consolidate when memory is getting full — merge related entries, remove outdated ones
|
|
32
45
|
- Keep memory under 1500 chars per file
|
|
33
46
|
- Format entries as bullet points with timestamps: `- Description *(YYYY-MM-DD)*`
|
|
34
|
-
- If
|
|
47
|
+
- If nothing crosses the bar, write nothing — empty memory is fine
|
|
35
48
|
- Each JSONL line has format: `{"ts": "ISO timestamp", "msg": "user message text"}`
|
|
36
49
|
- If a project directory no longer exists, skip it — don't write to a dead path
|
|
50
|
+
- Before writing, re-read existing memory and check for near-duplicates. Two entries about "evaluating warnings" should be one entry or none.
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
## Skills
|
|
2
|
+
|
|
3
|
+
Users can save reusable prompt snippets as skills. When the user asks to use a named skill, style, workflow, or saved instruction, search capabilities and load the best matching skill with `search_plugins` before continuing. `search_plugins` may return plugins and skills; use the `load` parameter to activate them. Do not invent skill contents. If several skills plausibly match, ask a short clarifying question instead of guessing. Treat loaded skill text as user-provided instructions scoped to the current conversation, below system and developer instructions.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
Concise.
|
|
1
|
+
Concise and conversational. Use bullets where necessary, not as default. 3-5 sentences max. Show changed code only in edit tools, never in text. Explain why/what, skip how unless asked.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
`search_plugins`: when the user asks to use a named skill, style, workflow, or saved instruction, search capabilities and load the best matching skill with the `load` parameter before continuing. `search_plugins` may return plugins and skills; use `load` to activate them. Do not invent skill contents. Ask briefly if multiple skills match.
|
package/src/core/agentic.py
CHANGED
|
@@ -46,6 +46,7 @@ from core.tool_feedback import (
|
|
|
46
46
|
display_tool_feedback,
|
|
47
47
|
)
|
|
48
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
|
|
49
50
|
|
|
50
51
|
|
|
51
52
|
def _handle_empty_response(empty_response_count, console):
|
|
@@ -87,7 +88,6 @@ def _handle_tool_limit_reached(chat_manager, console):
|
|
|
87
88
|
response,
|
|
88
89
|
model_name=provider_cfg.get("model", ""),
|
|
89
90
|
)
|
|
90
|
-
|
|
91
91
|
try:
|
|
92
92
|
final_message = response["choices"][0]["message"]
|
|
93
93
|
except (KeyError, IndexError):
|
|
@@ -105,8 +105,6 @@ def _handle_tool_limit_reached(chat_manager, console):
|
|
|
105
105
|
console.print("[red]Error: model returned empty response after tool limit reached.[/red]")
|
|
106
106
|
return False
|
|
107
107
|
|
|
108
|
-
|
|
109
|
-
|
|
110
108
|
class AgenticOrchestrator:
|
|
111
109
|
"""Orchestrates the agentic tool-calling loop.
|
|
112
110
|
|
|
@@ -171,21 +169,42 @@ class AgenticOrchestrator:
|
|
|
171
169
|
# Check if we're in a parallel context with suppressed console
|
|
172
170
|
return self._parallel_context.get('console', self.console)
|
|
173
171
|
|
|
174
|
-
def
|
|
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
|
|
175
|
+
|
|
176
|
+
tools = TOOLS()
|
|
177
|
+
if allowed_tools is None:
|
|
178
|
+
return tools
|
|
179
|
+
|
|
180
|
+
effective_names = set(allowed_tools)
|
|
181
|
+
if allow_active_plugins:
|
|
182
|
+
effective_names.update(ToolRegistry.active_plugin_names())
|
|
183
|
+
|
|
184
|
+
if not effective_names:
|
|
185
|
+
return []
|
|
186
|
+
|
|
187
|
+
return [tool for tool in tools if tool["function"]["name"] in effective_names]
|
|
188
|
+
|
|
189
|
+
def run(self, user_input, thinking_indicator=None, allowed_tools=None, allow_active_plugins=False):
|
|
175
190
|
"""Main orchestration loop.
|
|
176
191
|
|
|
177
192
|
Args:
|
|
178
193
|
user_input: User's input message
|
|
179
194
|
thinking_indicator: Optional ThinkingIndicator instance
|
|
180
195
|
allowed_tools: Optional list of allowed tool names (for research)
|
|
196
|
+
allow_active_plugins: Whether to include active plugin tools in restricted runs
|
|
181
197
|
"""
|
|
198
|
+
self._current_allowed_tools = allowed_tools
|
|
199
|
+
self._current_allow_active_plugins = allow_active_plugins
|
|
200
|
+
|
|
182
201
|
# Append user message
|
|
183
202
|
self.chat_manager.messages.append({"role": "user", "content": user_input})
|
|
184
203
|
|
|
185
204
|
# Log user message
|
|
186
205
|
self.chat_manager.log_message({"role": "user", "content": user_input})
|
|
187
206
|
|
|
188
|
-
from tools.base import ToolRegistry
|
|
207
|
+
from tools.helpers.base import ToolRegistry
|
|
189
208
|
|
|
190
209
|
while True:
|
|
191
210
|
# Decrement plugin TTLs after previous iteration's tool execution.
|
|
@@ -195,7 +214,10 @@ class AgenticOrchestrator:
|
|
|
195
214
|
self.console.print(f"[dim]Plugins evicted (TTL expired): {evicted}[/dim]")
|
|
196
215
|
|
|
197
216
|
# Get response from LLM
|
|
198
|
-
response = self._get_llm_response(
|
|
217
|
+
response = self._get_llm_response(
|
|
218
|
+
allowed_tools=allowed_tools,
|
|
219
|
+
allow_active_plugins=allow_active_plugins,
|
|
220
|
+
)
|
|
199
221
|
if response is None:
|
|
200
222
|
return
|
|
201
223
|
|
|
@@ -209,11 +231,16 @@ class AgenticOrchestrator:
|
|
|
209
231
|
if self._handle_final_response(response, thinking_indicator):
|
|
210
232
|
return
|
|
211
233
|
else:
|
|
212
|
-
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
|
+
)
|
|
213
240
|
if should_exit:
|
|
214
241
|
return
|
|
215
242
|
|
|
216
|
-
def _get_llm_response(self, allowed_tools=None):
|
|
243
|
+
def _get_llm_response(self, allowed_tools=None, allow_active_plugins=False):
|
|
217
244
|
"""Get next LLM response with tool definitions.
|
|
218
245
|
|
|
219
246
|
Includes automatic retry with live countdown for timeout/connection errors.
|
|
@@ -221,6 +248,7 @@ class AgenticOrchestrator:
|
|
|
221
248
|
|
|
222
249
|
Args:
|
|
223
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
|
|
224
252
|
|
|
225
253
|
Returns:
|
|
226
254
|
Response dict from LLM, or None if error occurred
|
|
@@ -229,19 +257,17 @@ class AgenticOrchestrator:
|
|
|
229
257
|
self.chat_manager.ensure_context_fits(console=self.console)
|
|
230
258
|
|
|
231
259
|
# Use allowed_tools if provided, otherwise use mode-based filtering
|
|
232
|
-
if allowed_tools is not None:
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
else:
|
|
244
|
-
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]")
|
|
245
271
|
|
|
246
272
|
# Retry loop for timeout/connection errors
|
|
247
273
|
last_error = None
|
|
@@ -262,7 +288,21 @@ class AgenticOrchestrator:
|
|
|
262
288
|
continue
|
|
263
289
|
else:
|
|
264
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
|
+
|
|
265
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)
|
|
266
306
|
return None
|
|
267
307
|
|
|
268
308
|
# Successful response — parse and return
|
|
@@ -324,8 +364,11 @@ class AgenticOrchestrator:
|
|
|
324
364
|
# NEW: Compact tool results after final answer (per-message compaction)
|
|
325
365
|
self.chat_manager.compact_tool_results(skip_token_update=True)
|
|
326
366
|
|
|
327
|
-
# Update context tokens with current
|
|
328
|
-
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
|
+
)
|
|
329
372
|
self.chat_manager._update_context_tokens(tools_for_mode)
|
|
330
373
|
|
|
331
374
|
self.console.print()
|
|
@@ -337,13 +380,14 @@ class AgenticOrchestrator:
|
|
|
337
380
|
)
|
|
338
381
|
return not should_continue
|
|
339
382
|
|
|
340
|
-
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):
|
|
341
384
|
"""Process tool calls and display accompanying content.
|
|
342
385
|
|
|
343
386
|
Args:
|
|
344
387
|
response: Full message dict from LLM (includes content and tool_calls)
|
|
345
388
|
thinking_indicator: Optional ThinkingIndicator instance
|
|
346
389
|
allowed_tools: Optional list of allowed tool names
|
|
390
|
+
allow_active_plugins: Whether to allow active plugin tools in restricted runs
|
|
347
391
|
|
|
348
392
|
Returns:
|
|
349
393
|
True if should exit the orchestration loop
|
|
@@ -357,6 +401,8 @@ class AgenticOrchestrator:
|
|
|
357
401
|
# This must happen BEFORE filtering so the LLM sees its original intent
|
|
358
402
|
content = (response.get("content") or "").strip()
|
|
359
403
|
assistant_msg = {"role": "assistant", "tool_calls": tool_calls}
|
|
404
|
+
if response.get("_responses_output"):
|
|
405
|
+
assistant_msg["_responses_output"] = response["_responses_output"]
|
|
360
406
|
if content:
|
|
361
407
|
assistant_msg["content"] = content
|
|
362
408
|
self.chat_manager.messages.append(assistant_msg)
|
|
@@ -367,7 +413,7 @@ class AgenticOrchestrator:
|
|
|
367
413
|
# This silently removes unknown tools or tools not in the allowed whitelist
|
|
368
414
|
# to prevent error messages from reaching the user while allowing the agent
|
|
369
415
|
# to continue with alternative tools.
|
|
370
|
-
from tools.base import ToolRegistry
|
|
416
|
+
from tools.helpers.base import ToolRegistry
|
|
371
417
|
|
|
372
418
|
filtered_calls = []
|
|
373
419
|
filtered_tool_ids = [] # Track filtered tool IDs to provide feedback
|
|
@@ -384,10 +430,14 @@ class AgenticOrchestrator:
|
|
|
384
430
|
filtered_tool_ids.append(tool_call.get("id"))
|
|
385
431
|
continue
|
|
386
432
|
|
|
387
|
-
# Check if tool is in
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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:
|
|
391
441
|
# Silent fail - skip this tool
|
|
392
442
|
if self.debug_mode:
|
|
393
443
|
self.console.print(f"[dim]Silently filtered non-allowed tool: {function_name}[/dim]")
|
|
@@ -542,8 +592,11 @@ class AgenticOrchestrator:
|
|
|
542
592
|
# Compact completed tool blocks once after all tools complete
|
|
543
593
|
self.chat_manager.compact_tool_results(skip_token_update=True)
|
|
544
594
|
|
|
545
|
-
# Update context tokens with current
|
|
546
|
-
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
|
+
)
|
|
547
600
|
self.chat_manager._update_context_tokens(tools_for_mode)
|
|
548
601
|
|
|
549
602
|
# Pre-send guard: ensure context fits before next LLM call
|
|
@@ -563,7 +616,7 @@ class AgenticOrchestrator:
|
|
|
563
616
|
"""
|
|
564
617
|
if not tool_calls:
|
|
565
618
|
return False
|
|
566
|
-
from tools.parallel_executor import ParallelToolExecutor, ToolCall
|
|
619
|
+
from tools.helpers.parallel_executor import ParallelToolExecutor, ToolCall
|
|
567
620
|
|
|
568
621
|
# Suppress console output in handlers during parallel execution
|
|
569
622
|
# We'll display results ourselves in order below
|
|
@@ -783,8 +836,11 @@ class AgenticOrchestrator:
|
|
|
783
836
|
# after all parallel results are appended (safe — only compacts completed blocks)
|
|
784
837
|
self.chat_manager.compact_tool_results(skip_token_update=True)
|
|
785
838
|
|
|
786
|
-
# Update context tokens with current
|
|
787
|
-
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
|
+
)
|
|
788
844
|
self.chat_manager._update_context_tokens(tools_for_mode)
|
|
789
845
|
|
|
790
846
|
# Pre-send guard: ensure context fits before next LLM call
|
|
@@ -795,6 +851,38 @@ class AgenticOrchestrator:
|
|
|
795
851
|
# Restore console output
|
|
796
852
|
self._parallel_context['console'] = self.console
|
|
797
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
|
+
|
|
798
886
|
def _process_single_tool_call(self, tool_call, thinking_indicator):
|
|
799
887
|
"""Process a single tool call.
|
|
800
888
|
|
|
@@ -826,7 +914,7 @@ class AgenticOrchestrator:
|
|
|
826
914
|
panel_to_use = SubAgentPanel(query, self.console)
|
|
827
915
|
|
|
828
916
|
# Execute via tool registry
|
|
829
|
-
from tools.base import ToolRegistry, build_context
|
|
917
|
+
from tools.helpers.base import ToolRegistry, build_context
|
|
830
918
|
|
|
831
919
|
tool = ToolRegistry.get(function_name)
|
|
832
920
|
if tool:
|
|
@@ -916,6 +1004,20 @@ class AgenticOrchestrator:
|
|
|
916
1004
|
if policy == TERMINAL_YIELD and thinking_indicator:
|
|
917
1005
|
thinking_indicator.resume()
|
|
918
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
|
+
|
|
919
1021
|
# Display result for registry tools
|
|
920
1022
|
# Skip display for tools that take over the terminal (they handle their own display)
|
|
921
1023
|
if policy != TERMINAL_YIELD:
|
|
@@ -948,7 +1050,7 @@ class AgenticOrchestrator:
|
|
|
948
1050
|
# Then display feedback
|
|
949
1051
|
display_tool_feedback(label, result, console, indent=self.is_sub_agent, panel_updater=self.panel_updater)
|
|
950
1052
|
|
|
951
|
-
return False,
|
|
1053
|
+
return False, result_str
|
|
952
1054
|
except Exception as e:
|
|
953
1055
|
# If thinking_indicator was paused (TERMINAL_YIELD) and tool
|
|
954
1056
|
# raised, resume it so the spinner reappears for the next iteration
|
|
@@ -981,5 +1083,3 @@ def agentic_answer(chat_manager, user_input, console, repo_root, rg_exe_path, de
|
|
|
981
1083
|
debug_mode=debug_mode,
|
|
982
1084
|
)
|
|
983
1085
|
orchestrator.run(user_input, thinking_indicator)
|
|
984
|
-
|
|
985
|
-
|
package/src/core/chat_manager.py
CHANGED
|
@@ -9,8 +9,9 @@ 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
|
|
@@ -45,6 +46,10 @@ class ChatManager:
|
|
|
45
46
|
self.task_list = []
|
|
46
47
|
self.task_list_title = None
|
|
47
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
|
+
|
|
48
53
|
# .gitignore filtering state
|
|
49
54
|
self._gitignore_spec = None
|
|
50
55
|
self._gitignore_mtime = None
|
|
@@ -94,6 +99,9 @@ class ChatManager:
|
|
|
94
99
|
if self.markdown_logger:
|
|
95
100
|
self.markdown_logger.start_session()
|
|
96
101
|
|
|
102
|
+
# Active skills are scoped to the current message history/session.
|
|
103
|
+
self.loaded_skills = set()
|
|
104
|
+
|
|
97
105
|
# Start with system prompt only
|
|
98
106
|
self.messages = [{"role": "system", "content": self._build_system_prompt()}]
|
|
99
107
|
|
|
@@ -133,7 +141,8 @@ class ChatManager:
|
|
|
133
141
|
if variant is None:
|
|
134
142
|
from utils.settings import prompt_settings
|
|
135
143
|
variant = prompt_settings.variant
|
|
136
|
-
|
|
144
|
+
active_skills_section = render_active_skills_section(self.loaded_skills)
|
|
145
|
+
return build_system_prompt(variant, active_skills_section=active_skills_section)
|
|
137
146
|
|
|
138
147
|
def update_system_prompt(self, variant: str | None = None):
|
|
139
148
|
"""Rebuild system prompt in-place (e.g. after hotswap or session reset).
|
|
@@ -1326,7 +1335,8 @@ Provide a concise summary (2-4 paragraphs) that captures all essential context f
|
|
|
1326
1335
|
"""
|
|
1327
1336
|
providers = get_providers()
|
|
1328
1337
|
if provider_name not in providers:
|
|
1329
|
-
|
|
1338
|
+
available = ', '.join(get_provider_display_name(provider) for provider in providers)
|
|
1339
|
+
return f"Invalid provider. Use /provider to list. Available: {available}"
|
|
1330
1340
|
|
|
1331
1341
|
previous_provider = self.client.provider
|
|
1332
1342
|
|
|
@@ -1342,10 +1352,13 @@ Provide a concise summary (2-4 paragraphs) that captures all essential context f
|
|
|
1342
1352
|
# Failed to start server - revert
|
|
1343
1353
|
self.client.switch_provider(previous_provider)
|
|
1344
1354
|
self._init_messages(reset_costs=True)
|
|
1345
|
-
|
|
1355
|
+
previous_label = get_provider_display_name(previous_provider)
|
|
1356
|
+
return f"Failed to start local server. Reverted to {previous_label} provider."
|
|
1346
1357
|
self.server_process = server
|
|
1347
|
-
|
|
1348
|
-
|
|
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."
|
|
1349
1362
|
return "Provider switch failed."
|
|
1350
1363
|
|
|
1351
1364
|
def reload_config(self):
|
|
@@ -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,6 +118,7 @@ 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',
|
|
119
124
|
'glm_plan': 'GLM_PLAN_MODEL',
|
|
@@ -144,6 +149,7 @@ class ConfigManager:
|
|
|
144
149
|
provider_keys = {
|
|
145
150
|
'local': 'LOCAL_MODEL_PATH',
|
|
146
151
|
'bone': 'BONE_PROXY_MODEL',
|
|
152
|
+
'codex': 'CODEX_PLAN_MODEL',
|
|
147
153
|
'openrouter': 'OPENROUTER_MODEL',
|
|
148
154
|
'glm': 'GLM_MODEL',
|
|
149
155
|
'glm_plan': 'GLM_PLAN_MODEL',
|
|
@@ -175,6 +181,7 @@ class ConfigManager:
|
|
|
175
181
|
provider_keys = {
|
|
176
182
|
'openrouter': 'OPENROUTER_API_KEY',
|
|
177
183
|
'bone': 'BONE_PROXY_API_KEY',
|
|
184
|
+
'codex': 'CODEX_PLAN_API_KEY',
|
|
178
185
|
'glm': 'GLM_API_KEY',
|
|
179
186
|
'glm_plan': 'GLM_PLAN_API_KEY',
|
|
180
187
|
'openai': 'OPENAI_API_KEY',
|
package/src/core/cron.py
CHANGED
|
@@ -344,15 +344,11 @@ def run_single_job(job: CronJob, console=None, interactive=False) -> None:
|
|
|
344
344
|
from core.chat_manager import ChatManager
|
|
345
345
|
from core.agentic import AgenticOrchestrator
|
|
346
346
|
from utils.paths import RG_EXE_PATH
|
|
347
|
-
from tools.loader import load_all_tools
|
|
348
347
|
from llm.config import TOOLS_ENABLED
|
|
349
348
|
|
|
350
349
|
if not TOOLS_ENABLED:
|
|
351
350
|
raise RuntimeError("Cron requires tools to be enabled")
|
|
352
351
|
|
|
353
|
-
# Ensure tools are loaded
|
|
354
|
-
load_all_tools()
|
|
355
|
-
|
|
356
352
|
# Fresh ChatManager for this job
|
|
357
353
|
chat_manager = ChatManager()
|
|
358
354
|
|