bone-agent 1.3.3 → 2.0.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/bin/bone.js +39 -0
- package/package.json +25 -39
- package/LICENSE +0 -21
- package/README.md +0 -184
- package/bin/npm-wrapper.js +0 -235
- package/bin/rg +0 -0
- package/bin/rg.exe +0 -0
- package/config.yaml.example +0 -141
- 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 -36
- 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/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/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 -985
- package/src/core/chat_manager.py +0 -1564
- package/src/core/config_manager.py +0 -253
- package/src/core/cron.py +0 -582
- package/src/core/cron_allowlist.py +0 -118
- package/src/core/memory.py +0 -145
- package/src/core/retry.py +0 -71
- package/src/core/sub_agent.py +0 -326
- package/src/core/tool_approval.py +0 -220
- package/src/core/tool_feedback.py +0 -778
- package/src/exceptions.py +0 -79
- package/src/llm/__init__.py +0 -1
- package/src/llm/client.py +0 -171
- package/src/llm/config.py +0 -492
- package/src/llm/prompts.py +0 -489
- package/src/llm/providers.py +0 -436
- package/src/llm/streaming.py +0 -163
- package/src/llm/token_tracker.py +0 -384
- package/src/tools/__init__.py +0 -212
- 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 -545
- package/src/tools/file_reader.py +0 -322
- package/src/tools/helpers/__init__.py +0 -105
- package/src/tools/helpers/base.py +0 -550
- 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 -231
- package/src/tools/helpers/parallel_executor.py +0 -231
- package/src/tools/helpers/path_resolver.py +0 -232
- package/src/tools/helpers/plugin_manifest.py +0 -156
- package/src/tools/obsidian.py +0 -96
- package/src/tools/review_sub_agent.py +0 -189
- package/src/tools/rg_search.py +0 -460
- package/src/tools/search_plugins.py +0 -109
- 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 -2809
- package/src/ui/displays.py +0 -214
- package/src/ui/loader.py +0 -284
- package/src/ui/main.py +0 -647
- 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 -215
- package/src/utils/__init__.py +0 -1
- package/src/utils/citation_parser.py +0 -199
- package/src/utils/editor.py +0 -158
- 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 -191
- package/src/utils/user_message_logger.py +0 -120
- package/src/utils/validation.py +0 -191
- package/src/utils/web_search.py +0 -173
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
"""Plugin manifest for on-demand tool discovery.
|
|
2
|
-
|
|
3
|
-
Plugin-tier tools are registered here instead of ToolRegistry at import time.
|
|
4
|
-
This keeps plugin schemas out of the LLM context window until explicitly
|
|
5
|
-
activated via the search_plugins core tool.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
import logging
|
|
9
|
-
from typing import Dict, List, Optional
|
|
10
|
-
|
|
11
|
-
from .base import ToolDefinition
|
|
12
|
-
|
|
13
|
-
logger = logging.getLogger(__name__)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class PluginManifest:
|
|
17
|
-
"""Index of plugin-tier tools available for on-demand activation.
|
|
18
|
-
|
|
19
|
-
Tools are registered here when modules with @tool(tier="plugin") are
|
|
20
|
-
imported. The search_plugins core tool queries this index to find and
|
|
21
|
-
activate matching plugins.
|
|
22
|
-
"""
|
|
23
|
-
|
|
24
|
-
def __init__(self):
|
|
25
|
-
self._plugins: Dict[str, ToolDefinition] = {}
|
|
26
|
-
|
|
27
|
-
def register(self, tool_def: ToolDefinition) -> None:
|
|
28
|
-
"""Register a plugin tool definition.
|
|
29
|
-
|
|
30
|
-
Args:
|
|
31
|
-
tool_def: ToolDefinition with tier="plugin"
|
|
32
|
-
"""
|
|
33
|
-
if tool_def.name in self._plugins:
|
|
34
|
-
logger.warning(
|
|
35
|
-
f"Plugin '{tool_def.name}' is being overwritten. "
|
|
36
|
-
f"Previous: {self._plugins[tool_def.name].handler}, "
|
|
37
|
-
f"New: {tool_def.handler}"
|
|
38
|
-
)
|
|
39
|
-
self._plugins[tool_def.name] = tool_def
|
|
40
|
-
logger.debug(f"Plugin registered in manifest: {tool_def.name}")
|
|
41
|
-
|
|
42
|
-
def get(self, name: str) -> Optional[ToolDefinition]:
|
|
43
|
-
"""Get a plugin tool definition by name.
|
|
44
|
-
|
|
45
|
-
Args:
|
|
46
|
-
name: Plugin tool name
|
|
47
|
-
|
|
48
|
-
Returns:
|
|
49
|
-
ToolDefinition or None if not found
|
|
50
|
-
"""
|
|
51
|
-
return self._plugins.get(name)
|
|
52
|
-
|
|
53
|
-
def get_all(self) -> List[ToolDefinition]:
|
|
54
|
-
"""Get all registered plugin definitions.
|
|
55
|
-
|
|
56
|
-
Returns:
|
|
57
|
-
List of all ToolDefinitions in the manifest
|
|
58
|
-
"""
|
|
59
|
-
return list(self._plugins.values())
|
|
60
|
-
|
|
61
|
-
def search(self, query: str, category: str = None, max_results: int = 5) -> List[ToolDefinition]:
|
|
62
|
-
"""Search the manifest for plugins matching a query.
|
|
63
|
-
|
|
64
|
-
Args:
|
|
65
|
-
query: Search query (matched against name, description, tags, category)
|
|
66
|
-
category: Optional category filter
|
|
67
|
-
max_results: Maximum number of results to return
|
|
68
|
-
|
|
69
|
-
Returns:
|
|
70
|
-
List of matching ToolDefinitions, sorted by relevance score
|
|
71
|
-
"""
|
|
72
|
-
query_lower = query.lower()
|
|
73
|
-
query_terms = query_lower.split()
|
|
74
|
-
|
|
75
|
-
scored = []
|
|
76
|
-
for tool_def in self._plugins.values():
|
|
77
|
-
# Apply category filter if specified
|
|
78
|
-
if category and tool_def.category != category:
|
|
79
|
-
continue
|
|
80
|
-
|
|
81
|
-
# Calculate relevance score
|
|
82
|
-
score = 0.0
|
|
83
|
-
|
|
84
|
-
# Exact name match (highest priority)
|
|
85
|
-
if query_lower == tool_def.name.lower():
|
|
86
|
-
score += 100.0
|
|
87
|
-
|
|
88
|
-
# Name contains query
|
|
89
|
-
if query_lower in tool_def.name.lower():
|
|
90
|
-
score += 50.0
|
|
91
|
-
|
|
92
|
-
# Name contains individual terms
|
|
93
|
-
for term in query_terms:
|
|
94
|
-
if term in tool_def.name.lower():
|
|
95
|
-
score += 20.0
|
|
96
|
-
|
|
97
|
-
# Description match
|
|
98
|
-
if query_lower in tool_def.description.lower():
|
|
99
|
-
score += 30.0
|
|
100
|
-
for term in query_terms:
|
|
101
|
-
if term in tool_def.description.lower():
|
|
102
|
-
score += 10.0
|
|
103
|
-
|
|
104
|
-
# Tag match
|
|
105
|
-
for tag in tool_def.tags:
|
|
106
|
-
if query_lower in tag.lower():
|
|
107
|
-
score += 15.0
|
|
108
|
-
for term in query_terms:
|
|
109
|
-
if term in tag.lower():
|
|
110
|
-
score += 5.0
|
|
111
|
-
|
|
112
|
-
# Category match
|
|
113
|
-
if category and tool_def.category == category:
|
|
114
|
-
score += 10.0
|
|
115
|
-
elif not category and query_lower in tool_def.category.lower():
|
|
116
|
-
score += 15.0
|
|
117
|
-
|
|
118
|
-
if score > 0:
|
|
119
|
-
scored.append((score, tool_def))
|
|
120
|
-
|
|
121
|
-
# Sort by score descending
|
|
122
|
-
scored.sort(key=lambda x: x[0], reverse=True)
|
|
123
|
-
|
|
124
|
-
return [tool_def for _, tool_def in scored[:max_results]]
|
|
125
|
-
|
|
126
|
-
def get_categories(self) -> List[str]:
|
|
127
|
-
"""Get all unique categories in the manifest.
|
|
128
|
-
|
|
129
|
-
Returns:
|
|
130
|
-
Sorted list of category strings
|
|
131
|
-
"""
|
|
132
|
-
categories = {td.category for td in self._plugins.values() if td.category}
|
|
133
|
-
return sorted(categories)
|
|
134
|
-
|
|
135
|
-
def plugin_count(self) -> int:
|
|
136
|
-
"""Get the number of registered plugins.
|
|
137
|
-
|
|
138
|
-
Returns:
|
|
139
|
-
Number of plugins in the manifest
|
|
140
|
-
"""
|
|
141
|
-
return len(self._plugins)
|
|
142
|
-
|
|
143
|
-
def has_plugin(self, name: str) -> bool:
|
|
144
|
-
"""Check if a plugin exists in the manifest.
|
|
145
|
-
|
|
146
|
-
Args:
|
|
147
|
-
name: Plugin tool name
|
|
148
|
-
|
|
149
|
-
Returns:
|
|
150
|
-
True if plugin is in the manifest
|
|
151
|
-
"""
|
|
152
|
-
return name in self._plugins
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
# Singleton instance
|
|
156
|
-
plugin_manifest = PluginManifest()
|
package/src/tools/obsidian.py
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
"""Obsidian vault integration utilities.
|
|
2
|
-
|
|
3
|
-
Provides vault session management for project note routing.
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
import logging
|
|
7
|
-
import os
|
|
8
|
-
from pathlib import Path
|
|
9
|
-
from dataclasses import dataclass
|
|
10
|
-
from typing import Optional
|
|
11
|
-
|
|
12
|
-
logger = logging.getLogger(__name__)
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
@dataclass(frozen=True)
|
|
16
|
-
class VaultSession:
|
|
17
|
-
"""Immutable snapshot of vault pairing state.
|
|
18
|
-
|
|
19
|
-
Built once when the vault is activated (register()) and shared everywhere.
|
|
20
|
-
Eliminates scattered derivation of vault_root, project_folder, and path prefixes.
|
|
21
|
-
|
|
22
|
-
Attributes:
|
|
23
|
-
vault_root: Absolute validated vault root Path.
|
|
24
|
-
project_folder: Absolute project folder (vault_root / project_base / repo_name).
|
|
25
|
-
project_folder_relative: Vault-relative project folder string (e.g. "Dev/myrepo").
|
|
26
|
-
"""
|
|
27
|
-
vault_root: Path
|
|
28
|
-
project_folder: Path
|
|
29
|
-
project_folder_relative: str
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
# Module-level session — set by register(), cleared by unregister()
|
|
33
|
-
_session: Optional[VaultSession] = None
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def get_vault_session() -> Optional[VaultSession]:
|
|
37
|
-
"""Return the active VaultSession, or None if vault is not paired."""
|
|
38
|
-
return _session
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def build_vault_session(repo_root: Path = None) -> Optional[VaultSession]:
|
|
42
|
-
"""Build a VaultSession from current settings.
|
|
43
|
-
|
|
44
|
-
Args:
|
|
45
|
-
repo_root: Repository root used to derive project folder name.
|
|
46
|
-
Falls back to os.getcwd() if not provided.
|
|
47
|
-
|
|
48
|
-
Returns:
|
|
49
|
-
VaultSession if vault is configured and valid, None otherwise.
|
|
50
|
-
"""
|
|
51
|
-
from utils.settings import obsidian_settings
|
|
52
|
-
|
|
53
|
-
if not obsidian_settings.is_active():
|
|
54
|
-
return None
|
|
55
|
-
|
|
56
|
-
root = obsidian_settings.vault_path # Already validated by is_active()
|
|
57
|
-
# Re-resolve to absolute (is_active checks existence but returns raw string)
|
|
58
|
-
root = Path(root).resolve()
|
|
59
|
-
|
|
60
|
-
project_base = obsidian_settings.project_base or "Dev"
|
|
61
|
-
repo_name = repo_root.name if repo_root else os.path.basename(os.getcwd())
|
|
62
|
-
if not repo_name:
|
|
63
|
-
return None
|
|
64
|
-
|
|
65
|
-
project_folder = root / project_base / repo_name
|
|
66
|
-
pf_relative = str(project_base) + "/" + repo_name
|
|
67
|
-
|
|
68
|
-
return VaultSession(
|
|
69
|
-
vault_root=root,
|
|
70
|
-
project_folder=project_folder,
|
|
71
|
-
project_folder_relative=pf_relative,
|
|
72
|
-
)
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def init_session(repo_root: Path = None) -> Optional[VaultSession]:
|
|
76
|
-
"""Build and cache the VaultSession.
|
|
77
|
-
|
|
78
|
-
Should be called once with the actual repo_root when available.
|
|
79
|
-
Falls back to os.getcwd() if repo_root is None (acceptable for prompt
|
|
80
|
-
building before the orchestrator starts).
|
|
81
|
-
|
|
82
|
-
Returns:
|
|
83
|
-
The active VaultSession, or None if vault is not configured.
|
|
84
|
-
"""
|
|
85
|
-
global _session
|
|
86
|
-
session = build_vault_session(repo_root=repo_root)
|
|
87
|
-
if session:
|
|
88
|
-
# Only cache if we have a real repo_root — otherwise the orchestrator
|
|
89
|
-
# will call us again with the correct path and that should win.
|
|
90
|
-
if repo_root is not None or _session is None:
|
|
91
|
-
_session = session
|
|
92
|
-
logger.info(f"Vault session initialized: {session.project_folder_relative}")
|
|
93
|
-
return _session
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
"""Review sub-agent tool for analyzing git diffs."""
|
|
2
|
-
|
|
3
|
-
import re
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
|
|
6
|
-
from core.sub_agent import run_sub_agent
|
|
7
|
-
from tools.sub_agent import SimplePanelUpdater
|
|
8
|
-
from utils.citation_parser import inject_file_contents
|
|
9
|
-
|
|
10
|
-
# Approximate chars per token (conservative heuristic, avoids tokenizer dependency)
|
|
11
|
-
_CHARS_PER_TOKEN = 4
|
|
12
|
-
|
|
13
|
-
# Max tokens for the diff context (leaves room for agent conversation within budget)
|
|
14
|
-
_MAX_DIFF_TOKENS = 50_000
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def _estimate_tokens(text: str) -> int:
|
|
18
|
-
"""Rough token estimate based on character count."""
|
|
19
|
-
return len(text) // _CHARS_PER_TOKEN
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def _truncate_diff(diff: str, max_chars: int) -> tuple:
|
|
23
|
-
"""Truncate a diff to fit within a character budget, keeping whole files.
|
|
24
|
-
|
|
25
|
-
Returns:
|
|
26
|
-
Tuple of (truncated_diff, list_of_skipped_file_names)
|
|
27
|
-
"""
|
|
28
|
-
# Split on diff headers: each file starts with "diff --git"
|
|
29
|
-
file_blocks = re.split(r'(?=^diff --git )', diff, flags=re.MULTILINE)
|
|
30
|
-
# First element may be empty if diff starts with "diff --git"
|
|
31
|
-
if file_blocks and file_blocks[0].strip() == '':
|
|
32
|
-
file_blocks = file_blocks[1:]
|
|
33
|
-
|
|
34
|
-
result_blocks = []
|
|
35
|
-
skipped_files = []
|
|
36
|
-
current_chars = 0
|
|
37
|
-
|
|
38
|
-
for block in file_blocks:
|
|
39
|
-
block_chars = len(block)
|
|
40
|
-
if current_chars + block_chars > max_chars:
|
|
41
|
-
# Extract file name from the block header for the skip list
|
|
42
|
-
match = re.search(r'^diff --git a/(.*?) b/', block, re.MULTILINE)
|
|
43
|
-
if match:
|
|
44
|
-
skipped_files.append(match.group(1))
|
|
45
|
-
else:
|
|
46
|
-
result_blocks.append(block)
|
|
47
|
-
current_chars += block_chars
|
|
48
|
-
|
|
49
|
-
truncated = '\n'.join(result_blocks)
|
|
50
|
-
return truncated, skipped_files
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def _count_binary_files(diff: str) -> int:
|
|
54
|
-
"""Count binary file entries in a diff."""
|
|
55
|
-
return len(re.findall(r'^Binary files ', diff, re.MULTILINE))
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def _build_review_context(diff: str) -> str:
|
|
59
|
-
"""Build the initial context message for the review sub-agent.
|
|
60
|
-
|
|
61
|
-
Handles truncation and binary file counting.
|
|
62
|
-
|
|
63
|
-
Returns:
|
|
64
|
-
Tuple of (context_string, warning_string_or_None)
|
|
65
|
-
"""
|
|
66
|
-
warning = None
|
|
67
|
-
max_chars = _MAX_DIFF_TOKENS * _CHARS_PER_TOKEN
|
|
68
|
-
|
|
69
|
-
# Check for binary files
|
|
70
|
-
binary_count = _count_binary_files(diff)
|
|
71
|
-
binary_note = ""
|
|
72
|
-
if binary_count > 0:
|
|
73
|
-
binary_note = f"\n\nNote: {binary_count} binary file(s) were skipped in this diff."
|
|
74
|
-
|
|
75
|
-
# Check if truncation is needed
|
|
76
|
-
estimated_tokens = _estimate_tokens(diff)
|
|
77
|
-
if estimated_tokens > _MAX_DIFF_TOKENS:
|
|
78
|
-
truncated_diff, skipped_files = _truncate_diff(diff, max_chars)
|
|
79
|
-
skipped_note = ""
|
|
80
|
-
if skipped_files:
|
|
81
|
-
skipped_note = (
|
|
82
|
-
f"\n\nWARNING: Diff was too large and has been truncated. "
|
|
83
|
-
f"The following {len(skipped_files)} file(s) were omitted from review:\n"
|
|
84
|
-
+ "\n".join(f" - {f}" for f in skipped_files)
|
|
85
|
-
)
|
|
86
|
-
warning = f"Diff truncated: {len(skipped_files)} file(s) omitted."
|
|
87
|
-
context = f"Review this git diff:\n\n{truncated_diff}{binary_note}{skipped_note}"
|
|
88
|
-
else:
|
|
89
|
-
context = f"Review this git diff:\n\n{diff}{binary_note}"
|
|
90
|
-
|
|
91
|
-
return context, warning
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
def review_changes(
|
|
95
|
-
diff_output: str,
|
|
96
|
-
repo_root: Path,
|
|
97
|
-
rg_exe_path: str,
|
|
98
|
-
console,
|
|
99
|
-
chat_manager,
|
|
100
|
-
gitignore_spec=None,
|
|
101
|
-
panel_updater=None,
|
|
102
|
-
user_intent: str = None,
|
|
103
|
-
) -> dict:
|
|
104
|
-
"""Run review sub-agent on a git diff.
|
|
105
|
-
|
|
106
|
-
Args:
|
|
107
|
-
diff_output: Git diff string to review
|
|
108
|
-
repo_root: Repository root directory
|
|
109
|
-
rg_exe_path: Path to rg executable
|
|
110
|
-
console: Rich console for output
|
|
111
|
-
chat_manager: ChatManager instance
|
|
112
|
-
gitignore_spec: PathSpec for .gitignore filtering
|
|
113
|
-
panel_updater: Optional SubAgentPanel for live updates
|
|
114
|
-
user_intent: Optional description of what the user was trying to do
|
|
115
|
-
|
|
116
|
-
Returns:
|
|
117
|
-
Dict with keys:
|
|
118
|
-
display: Clean review text (no injected file contents)
|
|
119
|
-
history: Review text with file contents injected from citations
|
|
120
|
-
"""
|
|
121
|
-
if not diff_output or not isinstance(diff_output, str) or not diff_output.strip():
|
|
122
|
-
result = "No changes to review."
|
|
123
|
-
return {"display": result, "history": result}
|
|
124
|
-
|
|
125
|
-
# Build context (handles truncation and binary file notes)
|
|
126
|
-
review_context, truncation_warning = _build_review_context(diff_output)
|
|
127
|
-
|
|
128
|
-
if truncation_warning:
|
|
129
|
-
console.print(f"[yellow]{truncation_warning}[/yellow]")
|
|
130
|
-
|
|
131
|
-
# Set up panel updater
|
|
132
|
-
if panel_updater is None:
|
|
133
|
-
panel_updater = SimplePanelUpdater(console)
|
|
134
|
-
|
|
135
|
-
# Task query for the review agent
|
|
136
|
-
if user_intent:
|
|
137
|
-
task_query = (
|
|
138
|
-
"Analyze the diff provided above. "
|
|
139
|
-
"For each changed file, use read_file to get surrounding context. "
|
|
140
|
-
"Then provide a structured code review with findings, issues, and suggestions.\n\n"
|
|
141
|
-
f"The user's intent for these changes was: {user_intent}\n"
|
|
142
|
-
"Use this context to better understand the changes and focus your review accordingly."
|
|
143
|
-
)
|
|
144
|
-
else:
|
|
145
|
-
task_query = (
|
|
146
|
-
"Analyze the diff provided above. "
|
|
147
|
-
"For each changed file, use read_file to get surrounding context. "
|
|
148
|
-
"Then provide a structured code review with findings, issues, and suggestions."
|
|
149
|
-
)
|
|
150
|
-
|
|
151
|
-
with panel_updater as panel:
|
|
152
|
-
sub_agent_data = run_sub_agent(
|
|
153
|
-
task_query=task_query,
|
|
154
|
-
repo_root=repo_root,
|
|
155
|
-
rg_exe_path=rg_exe_path,
|
|
156
|
-
console=console,
|
|
157
|
-
panel_updater=panel,
|
|
158
|
-
sub_agent_type="review",
|
|
159
|
-
initial_context=review_context,
|
|
160
|
-
)
|
|
161
|
-
|
|
162
|
-
# Check for errors
|
|
163
|
-
if sub_agent_data.get('error'):
|
|
164
|
-
panel.set_error(sub_agent_data['error'])
|
|
165
|
-
result = f"exit_code=1\n{sub_agent_data['error']}"
|
|
166
|
-
return {"display": result, "history": result}
|
|
167
|
-
|
|
168
|
-
# Track usage
|
|
169
|
-
usage = sub_agent_data.get('usage', {})
|
|
170
|
-
if usage:
|
|
171
|
-
chat_manager.token_tracker.add_usage(usage, model_name=sub_agent_data.get("model", ""))
|
|
172
|
-
panel.set_complete({
|
|
173
|
-
'prompt_tokens': usage.get('prompt_tokens', 0),
|
|
174
|
-
'completion_tokens': usage.get('completion_tokens', 0),
|
|
175
|
-
'total_tokens': usage.get('total_tokens', 0)
|
|
176
|
-
})
|
|
177
|
-
|
|
178
|
-
raw_result = sub_agent_data.get('result', '')
|
|
179
|
-
|
|
180
|
-
# If hard limit was exceeded, skip injection and return raw dump for both
|
|
181
|
-
if sub_agent_data.get('hard_limit_exceeded'):
|
|
182
|
-
return {"display": raw_result, "history": raw_result}
|
|
183
|
-
|
|
184
|
-
# Always inject file contents from citations into the history version
|
|
185
|
-
injected_result = inject_file_contents(
|
|
186
|
-
raw_result, repo_root, gitignore_spec, console
|
|
187
|
-
)
|
|
188
|
-
|
|
189
|
-
return {"display": raw_result, "history": injected_result}
|