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/src/core/sub_agent.py
CHANGED
|
@@ -4,11 +4,12 @@ Uses existing AgenticOrchestrator with isolated message context
|
|
|
4
4
|
and read-only tools to execute generic delegated tasks.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
|
|
9
|
-
from core.chat_manager import ChatManager
|
|
10
|
-
from
|
|
11
|
-
from
|
|
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
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
class HardLimitExceeded(Exception):
|
|
@@ -16,6 +17,11 @@ class HardLimitExceeded(Exception):
|
|
|
16
17
|
pass
|
|
17
18
|
|
|
18
19
|
|
|
20
|
+
class BilledLimitExceeded(Exception):
|
|
21
|
+
"""Raised when the sub-agent hits its cumulative billed token limit."""
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
19
25
|
def _format_messages_dump(messages) -> str:
|
|
20
26
|
"""Format sub-agent message history as a markdown dump.
|
|
21
27
|
|
|
@@ -89,7 +95,7 @@ def _inject_system_prompt(chat_manager, sub_agent_type: str = "research"):
|
|
|
89
95
|
chat_manager.messages = [{"role": "system", "content": base_prompt}]
|
|
90
96
|
|
|
91
97
|
|
|
92
|
-
def _load_codebase_map(chat_manager):
|
|
98
|
+
def _load_codebase_map(chat_manager):
|
|
93
99
|
"""Load agents.md codebase map into sub-agent context if available.
|
|
94
100
|
|
|
95
101
|
Args:
|
|
@@ -98,19 +104,13 @@ def _load_codebase_map(chat_manager):
|
|
|
98
104
|
agents_path = Path.cwd() / "agents.md"
|
|
99
105
|
if agents_path.exists():
|
|
100
106
|
map_content = agents_path.read_text(encoding="utf-8").strip()
|
|
101
|
-
user_msg = (
|
|
102
|
-
"Here is the codebase map for this project. "
|
|
103
|
-
"This provides an overview of the repository structure and file purposes. "
|
|
104
|
-
"Use this as a reference when exploring the codebase.\n\n"
|
|
105
|
-
f"## Codebase Map (auto-generated from agents.md)\n\n{map_content}"
|
|
106
|
-
)
|
|
107
|
-
|
|
108
|
-
"I've received the codebase map. I'll use this as a reference when "
|
|
109
|
-
"exploring the repository, but I'll always verify current state by "
|
|
110
|
-
"reading files and searching the codebase before making changes."
|
|
111
|
-
)
|
|
112
|
-
chat_manager.messages.append({"role": "user", "content": user_msg})
|
|
113
|
-
chat_manager.messages.append({"role": "assistant", "content": assistant_msg})
|
|
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
114
|
|
|
115
115
|
|
|
116
116
|
def _configure_isolation(chat_manager):
|
|
@@ -183,14 +183,11 @@ def run_sub_agent(
|
|
|
183
183
|
# Create fresh ChatManager for sub-agent
|
|
184
184
|
temp_chat_manager = _create_chat_manager(sub_agent_type=sub_agent_type)
|
|
185
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
|
-
temp_chat_manager.messages.append(
|
|
192
|
-
{"role": "assistant", "content": "I've received the context. I'll analyze it and use the available tools to gather additional information as needed."}
|
|
193
|
-
)
|
|
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
|
+
)
|
|
194
191
|
|
|
195
192
|
# Import here to avoid circular import with core.agentic
|
|
196
193
|
from core.agentic import AgenticOrchestrator
|
|
@@ -216,27 +213,40 @@ def run_sub_agent(
|
|
|
216
213
|
original_chat_completion = temp_chat_manager.client.chat_completion
|
|
217
214
|
|
|
218
215
|
_soft_limit_warned = False
|
|
216
|
+
_billed_warning_sent = False
|
|
219
217
|
|
|
220
218
|
def _chat_completion_with_token_hint(messages, **kwargs):
|
|
221
|
-
"""Prepend a system-level token budget hint
|
|
222
|
-
nonlocal _soft_limit_warned
|
|
219
|
+
"""Prepend a system-level token budget hint and one-time warnings to every LLM call."""
|
|
220
|
+
nonlocal _soft_limit_warned, _billed_warning_sent
|
|
223
221
|
tt = temp_chat_manager.token_tracker
|
|
224
|
-
hint = f"[Token budget: {tt.current_context_tokens:,} curr / {tt.conv_total_tokens:,} total]"
|
|
222
|
+
hint = f"[Token budget: {tt.current_context_tokens:,} curr / {tt.conv_total_tokens:,} total billed]"
|
|
223
|
+
warnings = []
|
|
225
224
|
|
|
226
225
|
if not _soft_limit_warned and tt.current_context_tokens >= sub_agent_settings.soft_limit_tokens:
|
|
227
226
|
_soft_limit_warned = True
|
|
228
|
-
|
|
229
|
-
f"WARNING: You have exceeded the soft token limit "
|
|
227
|
+
warnings.append(
|
|
228
|
+
f"WARNING: You have exceeded the current-context soft token limit "
|
|
230
229
|
f"({tt.current_context_tokens:,} / {sub_agent_settings.soft_limit_tokens:,}). "
|
|
231
|
-
"STOP exploring and return your findings immediately. Do NOT call any more tools.
|
|
232
|
-
+ hint
|
|
230
|
+
"STOP exploring and return your findings immediately. Do NOT call any more tools."
|
|
233
231
|
)
|
|
234
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
|
+
|
|
235
245
|
token_msg = {"role": "system", "content": hint}
|
|
236
246
|
return original_chat_completion([token_msg, *messages], **kwargs)
|
|
237
247
|
|
|
238
|
-
def _get_llm_response_with_hard_limit(allowed_tools=None):
|
|
239
|
-
"""Wrapper to check
|
|
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."""
|
|
240
250
|
tt = temp_chat_manager.token_tracker
|
|
241
251
|
|
|
242
252
|
# Check hard token limit before making LLM call
|
|
@@ -248,6 +258,19 @@ def run_sub_agent(
|
|
|
248
258
|
f"{tt.current_context_tokens:,} / {sub_agent_settings.hard_limit_tokens:,} tokens."
|
|
249
259
|
)
|
|
250
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
|
+
|
|
251
274
|
# Update panel with live token counts
|
|
252
275
|
# Order: conversation length (current context) first, total tokens billed second
|
|
253
276
|
conv_length = tt.current_context_tokens
|
|
@@ -256,27 +279,45 @@ def run_sub_agent(
|
|
|
256
279
|
panel_updater.token_info = f"{conv_length:,} curr | {total_billed:,} total"
|
|
257
280
|
panel_updater.append("") # Refresh panel title
|
|
258
281
|
|
|
259
|
-
return original_get_llm_response(
|
|
282
|
+
return original_get_llm_response(
|
|
283
|
+
allowed_tools=allowed_tools,
|
|
284
|
+
allow_active_plugins=allow_active_plugins,
|
|
285
|
+
)
|
|
260
286
|
|
|
261
287
|
# Apply both patches once, before the orchestrator loop starts
|
|
262
288
|
orchestrator._get_llm_response = _get_llm_response_with_hard_limit
|
|
263
289
|
temp_chat_manager.client.chat_completion = _chat_completion_with_token_hint
|
|
264
290
|
|
|
265
291
|
hard_limit_exceeded = False
|
|
292
|
+
billed_limit_exceeded = False
|
|
266
293
|
|
|
267
294
|
try:
|
|
268
295
|
# Run sub-agent task
|
|
269
296
|
orchestrator.run(
|
|
270
297
|
task_query,
|
|
271
298
|
thinking_indicator=None,
|
|
272
|
-
allowed_tools=sub_agent_settings.allowed_tools
|
|
299
|
+
allowed_tools=sub_agent_settings.allowed_tools,
|
|
300
|
+
allow_active_plugins=sub_agent_settings.allow_active_plugins,
|
|
273
301
|
)
|
|
274
302
|
except HardLimitExceeded:
|
|
275
303
|
hard_limit_exceeded = True
|
|
276
|
-
except
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
return {
|
|
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 {
|
|
280
321
|
"result": "",
|
|
281
322
|
"usage": {
|
|
282
323
|
"prompt_tokens": 0,
|
|
@@ -306,7 +347,15 @@ def run_sub_agent(
|
|
|
306
347
|
if msg.get("role") == "assistant" and msg.get("content"):
|
|
307
348
|
final_content = msg["content"].strip()
|
|
308
349
|
break
|
|
309
|
-
|
|
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
|
|
310
359
|
|
|
311
360
|
usage = {
|
|
312
361
|
"prompt_tokens": delta_prompt,
|
|
@@ -323,4 +372,5 @@ def run_sub_agent(
|
|
|
323
372
|
"model": temp_chat_manager.client.model,
|
|
324
373
|
"error": None,
|
|
325
374
|
"hard_limit_exceeded": hard_limit_exceeded,
|
|
375
|
+
"billed_limit_exceeded": billed_limit_exceeded,
|
|
326
376
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Tool result display functions for the agentic loop."""
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
|
+
import textwrap
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
from typing import Optional
|
|
6
7
|
|
|
@@ -8,6 +9,7 @@ from rich.syntax import Syntax
|
|
|
8
9
|
|
|
9
10
|
from utils.settings import MAX_COMMAND_OUTPUT_LINES, MonokaiDarkBGStyle
|
|
10
11
|
from utils.result_parsers import extract_exit_code, extract_all_metadata, extract_multiple_metadata
|
|
12
|
+
from tools.search_plugins import HEADER_MATCHES, HEADER_ALL
|
|
11
13
|
from tools.task_list import _format_task_list, _strip_rich_markup
|
|
12
14
|
|
|
13
15
|
|
|
@@ -303,110 +305,119 @@ def handle_list_directory_feedback(tool_result, console, panel_updater):
|
|
|
303
305
|
def handle_search_plugins_feedback(tool_result, console, panel_updater):
|
|
304
306
|
"""Handle feedback for search_plugins tool.
|
|
305
307
|
|
|
306
|
-
Display
|
|
307
|
-
similar to list_directory feedback style.
|
|
308
|
+
Display compact capability results in the same style as other tool output.
|
|
308
309
|
"""
|
|
309
310
|
lines = tool_result.split('\n')
|
|
311
|
+
prefix = "╰─ " if not panel_updater else ""
|
|
310
312
|
|
|
311
|
-
# Extract query from "Found N plugin(s) matching 'query':" line
|
|
312
313
|
query = ""
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
query = query_match.group(1)
|
|
314
|
+
capabilities = []
|
|
315
|
+
in_capability_section = False
|
|
316
316
|
|
|
317
|
-
# Parse plugin entries: - **name** [category] (status): description
|
|
318
|
-
# Tags: tag1, tag2
|
|
319
|
-
plugins = []
|
|
320
317
|
i = 0
|
|
321
318
|
while i < len(lines):
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
319
|
+
raw_line = lines[i]
|
|
320
|
+
line = raw_line.strip()
|
|
321
|
+
|
|
322
|
+
if line.startswith(HEADER_MATCHES):
|
|
323
|
+
query = line.replace(HEADER_MATCHES, "", 1)
|
|
324
|
+
in_capability_section = True
|
|
325
|
+
i += 1
|
|
326
|
+
continue
|
|
327
|
+
|
|
328
|
+
if line == HEADER_ALL:
|
|
329
|
+
query = line
|
|
330
|
+
in_capability_section = True
|
|
331
|
+
i += 1
|
|
332
|
+
continue
|
|
333
|
+
|
|
334
|
+
if not in_capability_section:
|
|
335
|
+
i += 1
|
|
336
|
+
continue
|
|
337
|
+
|
|
338
|
+
if line.startswith("Results: ") or line.startswith("Total: ") or not line:
|
|
339
|
+
i += 1
|
|
340
|
+
continue
|
|
341
|
+
|
|
342
|
+
if line.startswith("Activated plugins:") or line.startswith("Loaded skills:") or line.startswith("Load issues:"):
|
|
343
|
+
break
|
|
344
|
+
|
|
345
|
+
item_match = re.match(r'^- (.+)$', line)
|
|
346
|
+
if item_match:
|
|
347
|
+
item = {
|
|
348
|
+
"name": item_match.group(1),
|
|
349
|
+
"type": "",
|
|
350
|
+
"status": "",
|
|
351
|
+
"summary": "",
|
|
352
|
+
"tags": "",
|
|
353
|
+
}
|
|
354
|
+
i += 1
|
|
355
|
+
while i < len(lines):
|
|
356
|
+
raw_detail_line = lines[i]
|
|
357
|
+
stripped = raw_detail_line.strip()
|
|
358
|
+
if not stripped:
|
|
359
|
+
i += 1
|
|
360
|
+
continue
|
|
361
|
+
if re.match(r'^- (.+)$', stripped):
|
|
362
|
+
break
|
|
363
|
+
if stripped.startswith("Activated plugins:") or stripped.startswith("Loaded skills:") or stripped.startswith("Load issues:"):
|
|
364
|
+
break
|
|
365
|
+
if stripped.startswith("type: "):
|
|
366
|
+
item["type"] = stripped.replace("type: ", "", 1)
|
|
367
|
+
elif stripped.startswith("status: "):
|
|
368
|
+
item["status"] = stripped.replace("status: ", "", 1)
|
|
369
|
+
elif stripped.startswith("summary: "):
|
|
370
|
+
item["summary"] = stripped.replace("summary: ", "", 1)
|
|
371
|
+
elif stripped.startswith("tags: "):
|
|
372
|
+
item["tags"] = stripped.replace("tags: ", "", 1)
|
|
333
373
|
i += 1
|
|
374
|
+
capabilities.append(item)
|
|
375
|
+
continue
|
|
334
376
|
|
|
335
|
-
plugins.append({
|
|
336
|
-
"name": name,
|
|
337
|
-
"category": category,
|
|
338
|
-
"status": status,
|
|
339
|
-
"description": description,
|
|
340
|
-
"tags": tags,
|
|
341
|
-
})
|
|
342
377
|
i += 1
|
|
343
378
|
|
|
344
|
-
if not
|
|
345
|
-
# No plugins found — just show the message
|
|
346
|
-
prefix = "╰─ " if not panel_updater else ""
|
|
347
|
-
# Extract the informational message (skip exit_code line)
|
|
379
|
+
if not capabilities:
|
|
348
380
|
msg_lines = [l for l in lines if l.strip() and not l.startswith("exit_code=")]
|
|
349
|
-
output = prefix + "\n".join(msg_lines) if msg_lines else f"{prefix}No
|
|
381
|
+
output = prefix + "\n".join(msg_lines) if msg_lines else f"{prefix}No capability matches"
|
|
350
382
|
_print_or_append(output, console, panel_updater)
|
|
351
383
|
if not panel_updater:
|
|
352
384
|
console.print()
|
|
353
385
|
return
|
|
354
386
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
remaining = max(0, len(plugins) - max_display)
|
|
387
|
+
def append_wrapped_detail(line_parts, text, indent, width=72):
|
|
388
|
+
wrapped_lines = textwrap.wrap(text, width=width) or [text]
|
|
389
|
+
line_parts.extend(f"{indent}[dim]{wrapped}[/dim]" for wrapped in wrapped_lines)
|
|
359
390
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
391
|
+
max_display = 10
|
|
392
|
+
display_capabilities = capabilities[:max_display]
|
|
393
|
+
remaining = max(0, len(capabilities) - max_display)
|
|
363
394
|
|
|
395
|
+
header = f"{query} ({len(capabilities)} match{'es' if len(capabilities) != 1 else ''})" if query else "capabilities"
|
|
364
396
|
tree_lines = []
|
|
365
|
-
for i, plugin in enumerate(display_plugins):
|
|
366
|
-
is_last = (i == len(display_plugins) - 1) and (remaining == 0)
|
|
367
|
-
connector = "└─" if is_last else "├─"
|
|
368
397
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
398
|
+
for idx, capability in enumerate(display_capabilities):
|
|
399
|
+
is_last = (idx == len(display_capabilities) - 1) and (remaining == 0)
|
|
400
|
+
connector = "└─" if is_last else "├─"
|
|
372
401
|
|
|
373
|
-
if
|
|
374
|
-
|
|
402
|
+
if capability["type"] == "plugin":
|
|
403
|
+
meta = "[dim](plugin)[/dim]"
|
|
404
|
+
if capability["status"]:
|
|
405
|
+
meta += f" [dim][{capability['status']}][/dim]"
|
|
375
406
|
else:
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
line = f" {connector} **{plugin['name']}**{cat_part} ({status_display}): {plugin['description']}"
|
|
379
|
-
tree_lines.append(line)
|
|
407
|
+
meta = "[dim](skill)[/dim]"
|
|
380
408
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
409
|
+
detail_indent = " │ " if not is_last else " "
|
|
410
|
+
line_parts = [f" {connector} {capability['name']} {meta}"]
|
|
411
|
+
if capability["summary"]:
|
|
412
|
+
append_wrapped_detail(line_parts, capability["summary"], detail_indent)
|
|
413
|
+
if capability["tags"]:
|
|
414
|
+
append_wrapped_detail(line_parts, f"tags: {capability['tags']}", detail_indent)
|
|
415
|
+
tree_lines.append("\n".join(line_parts))
|
|
385
416
|
|
|
386
|
-
# Add overflow indicator
|
|
387
417
|
if remaining > 0:
|
|
388
418
|
tree_lines.append(f" └─ ... and {remaining} more")
|
|
389
419
|
|
|
390
|
-
|
|
391
|
-
if query:
|
|
392
|
-
header = f"plugins matching '{query}' ({len(plugins)} found)"
|
|
393
|
-
else:
|
|
394
|
-
header = f"plugins ({len(plugins)} found)"
|
|
395
|
-
|
|
396
|
-
prefix = "╰─ " if not panel_updater else ""
|
|
397
|
-
output = f"{prefix}{header}\n"
|
|
398
|
-
output += "\n".join(tree_lines)
|
|
399
|
-
|
|
400
|
-
# Activation summary
|
|
401
|
-
if activated_count > 0 or already_active_count > 0:
|
|
402
|
-
summary_parts = []
|
|
403
|
-
if activated_count > 0:
|
|
404
|
-
summary_parts.append(f"{activated_count} activated")
|
|
405
|
-
if already_active_count > 0:
|
|
406
|
-
summary_parts.append(f"{already_active_count} already active")
|
|
407
|
-
summary = ", ".join(summary_parts)
|
|
408
|
-
output += f"\n{prefix}[dim]{summary}. Schemas available next turn. Auto-evict after 10 turns of non-use.[/dim]"
|
|
409
|
-
|
|
420
|
+
output = f"{prefix}{header}\n" + "\n".join(tree_lines)
|
|
410
421
|
_print_or_append(output, console, panel_updater)
|
|
411
422
|
|
|
412
423
|
if not panel_updater:
|
package/src/llm/client.py
CHANGED
|
@@ -127,8 +127,13 @@ class LLMClient:
|
|
|
127
127
|
self.handler.parse_stream(response)
|
|
128
128
|
)
|
|
129
129
|
else:
|
|
130
|
-
|
|
131
|
-
|
|
130
|
+
try:
|
|
131
|
+
response_json = response.json()
|
|
132
|
+
return self.handler.parse_response(response_json)
|
|
133
|
+
except ValueError:
|
|
134
|
+
if getattr(self.handler, "supports_sse_response_fallback", False):
|
|
135
|
+
return self.handler.parse_sse_response(response.text)
|
|
136
|
+
raise
|
|
132
137
|
|
|
133
138
|
except requests.exceptions.RequestException as e:
|
|
134
139
|
raise LLMConnectionError(
|