Skip to content

Commit c9966ba

Browse files
committed
Task 5: Create sync tool wrapper functions
Agent-Id: agent-83b013f9-a1e5-47aa-a26c-8741f631560f Linked-Note-Id: 8acd5eaf-3498-4009-8095-8355f7338af9
1 parent 980b497 commit c9966ba

1 file changed

Lines changed: 132 additions & 5 deletions

File tree

npx/python/cli/virtual_runner.py

Lines changed: 132 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Virtual review runner - runs RLM reviews on GitHub content without local repo."""
22

33
import asyncio
4+
import concurrent.futures
45
import logging
56
import re
67
from typing import Callable
@@ -24,6 +25,25 @@
2425
from .repo_tools import RepoTools
2526

2627

28+
def _sync_call(coro):
29+
"""Bridge async coroutines to sync using ThreadPoolExecutor.
30+
31+
DSPy RLM tools must be sync, but RepoTools/LocalRepoTools methods are async.
32+
This helper runs async code in a thread pool.
33+
"""
34+
loop = None
35+
try:
36+
loop = asyncio.get_running_loop()
37+
except RuntimeError:
38+
# No running loop, create one
39+
return asyncio.run(coro)
40+
41+
# We're in an async context, use thread pool to run the coroutine
42+
with concurrent.futures.ThreadPoolExecutor() as executor:
43+
future = executor.submit(asyncio.run, coro)
44+
return future.result()
45+
46+
2747
# Tool usage instructions for the model - simplified and clear
2848
AGENTIC_TOOLS_PROMPT = """
2949
## AVAILABLE COMMANDS (USE ONLY THESE!)
@@ -126,24 +146,131 @@ def __init__(
126146

127147
def _load_local_checklist(self, path: str) -> str:
128148
"""Load a bundled checklist file from the CLI package.
129-
149+
130150
Args:
131151
path: Path like 'checklists/solid-checklist.md'
132-
152+
133153
Returns:
134154
Content of the checklist file, or error message if not found
135155
"""
136156
from pathlib import Path
137-
157+
138158
# Get the directory where this module is located
139159
cli_dir = Path(__file__).parent
140160
checklist_path = cli_dir / path
141-
161+
142162
if checklist_path.exists():
143163
return checklist_path.read_text()
144164
else:
145165
return f"[Error] Checklist not found: {path}"
146-
166+
167+
def _sync_call(self, coro):
168+
"""Bridge async coroutines to sync using ThreadPoolExecutor.
169+
170+
DSPy RLM tools must be sync, but RepoTools/LocalRepoTools methods are async.
171+
This helper runs the coroutine in a thread pool and returns the result.
172+
173+
Args:
174+
coro: An async coroutine to execute
175+
176+
Returns:
177+
The result of the coroutine
178+
"""
179+
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
180+
future = executor.submit(asyncio.run, coro)
181+
return future.result()
182+
183+
def _create_tool_functions(self):
184+
"""Create sync tool wrapper functions for DSPy RLM.
185+
186+
Returns a list of three sync tool functions as closures that capture
187+
self by reference (so self._repo_tools can change between review calls).
188+
189+
Returns:
190+
List of [fetch_file, list_dir, search_code] functions
191+
"""
192+
runner = self
193+
194+
def fetch_file(path: str) -> str:
195+
"""Fetch a file from the repository by path.
196+
197+
Handles both regular repository files and bundled checklist files.
198+
Returns file content as a string, or an error message if the file
199+
cannot be read.
200+
201+
Args:
202+
path: File path (e.g., 'src/main.py' or 'checklists/solid-checklist.md')
203+
204+
Returns:
205+
File content as string, or error message
206+
"""
207+
if path.startswith("checklists/"):
208+
return runner._load_local_checklist(path)
209+
return runner._sync_call(runner._repo_tools.fetch_file(path))
210+
211+
def list_dir(path: str) -> str:
212+
"""List directory contents at the given path.
213+
214+
Returns a formatted text listing of files and directories,
215+
showing path, type (file/dir), and size in bytes.
216+
217+
Args:
218+
path: Directory path (e.g., 'src' or '')
219+
220+
Returns:
221+
Formatted text listing of directory contents
222+
"""
223+
entries = runner._sync_call(runner._repo_tools.list_directory(path))
224+
if not entries:
225+
return "[No entries]"
226+
227+
# Format as readable text
228+
lines = []
229+
for entry in entries:
230+
if "error" in entry:
231+
lines.append(f"[Error] {entry['error']}")
232+
else:
233+
entry_path = entry.get("path", "?")
234+
entry_type = entry.get("type", "?")
235+
entry_size = entry.get("size", 0)
236+
if entry_type == "dir":
237+
lines.append(f"[DIR] {entry_path}")
238+
else:
239+
lines.append(f"[FILE] {entry_path} ({entry_size} bytes)")
240+
return "\n".join(lines)
241+
242+
def search_code(query: str) -> str:
243+
"""Search for code patterns in the repository.
244+
245+
Searches for code content, filenames, or paths. Returns a formatted
246+
text listing of matching files with code fragments.
247+
248+
Args:
249+
query: Search query (e.g., 'enable_tool_optimization' or 'rlm.py')
250+
251+
Returns:
252+
Formatted text listing of search results with file paths and fragments
253+
"""
254+
results = runner._sync_call(runner._repo_tools.search_code(query))
255+
if not results:
256+
return "[No matches found]"
257+
258+
# Format as readable text
259+
lines = []
260+
for result in results:
261+
path = result.get("path", "?")
262+
fragment = result.get("fragment", "")
263+
if fragment:
264+
# Truncate fragment if too long
265+
if len(fragment) > 100:
266+
fragment = fragment[:100] + "..."
267+
lines.append(f"{path}: {fragment}")
268+
else:
269+
lines.append(f"{path}")
270+
return "\n".join(lines)
271+
272+
return [fetch_file, list_dir, search_code]
273+
147274
def _ensure_configured(self):
148275
"""Configure DSPy and RLM on first use."""
149276
if self._configured:

0 commit comments

Comments
 (0)