Skip to content

Commit ab52a01

Browse files
authored
Merge pull request #31 from ayusrjn/dev
feat:Introduced new tools for getting the directory structure and git…
2 parents 0dce025 + 3899432 commit ab52a01

2 files changed

Lines changed: 226 additions & 22 deletions

File tree

lambda_agent/subagent.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,18 +47,34 @@
4747
# ---------------------------------------------------------------------------
4848

4949
# Default tools — the main agent can override per-task
50-
_DEFAULT_TOOL_NAMES = ["read_file", "search_repo", "run_command", "write_file"]
50+
_DEFAULT_TOOL_NAMES = [
51+
"read_file",
52+
"search_repo",
53+
"run_command",
54+
"write_file",
55+
"list_directory",
56+
"get_git_status",
57+
]
5158

5259

5360
def _get_tool_set() -> dict:
5461
"""Lazily import tool functions from tools.py to avoid circular imports."""
55-
from .tools import read_file, search_repo, run_command, write_file
62+
from .tools import (
63+
read_file,
64+
search_repo,
65+
run_command,
66+
write_file,
67+
list_directory,
68+
get_git_status,
69+
)
5670

5771
return {
5872
"read_file": read_file,
5973
"search_repo": search_repo,
6074
"run_command": run_command,
6175
"write_file": write_file,
76+
"list_directory": list_directory,
77+
"get_git_status": get_git_status,
6278
}
6379

6480

lambda_agent/tools.py

Lines changed: 208 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -64,34 +64,218 @@ def run_command(command: str) -> str:
6464
return f"Error executing command: {str(e)}"
6565

6666

67-
def get_workspace_summary() -> str:
68-
"""Gathers git context, branch, status, recent commits, and project documentation (like README.md or rule files) to help the agent understand the whole project."""
69-
summary_parts = []
67+
# ---------------------------------------------------------------------------
68+
# Dedicated directory listing tool
69+
# ---------------------------------------------------------------------------
70+
71+
# Directories to always skip when walking the tree
72+
_IGNORE_DIRS = {
73+
".git",
74+
"__pycache__",
75+
"node_modules",
76+
".venv",
77+
"venv",
78+
"env",
79+
".ruff_cache",
80+
"dist",
81+
"build",
82+
".next",
83+
".cache",
84+
".tox",
85+
".mypy_cache",
86+
".pytest_cache",
87+
"*.egg-info",
88+
}
89+
90+
91+
def _should_skip_dir(name: str) -> bool:
92+
"""Return True if a directory name matches any ignore pattern."""
93+
if name in _IGNORE_DIRS:
94+
return True
95+
# Handle glob-style suffix patterns like *.egg-info
96+
for pat in _IGNORE_DIRS:
97+
if pat.startswith("*") and name.endswith(pat[1:]):
98+
return True
99+
return False
100+
101+
102+
def list_directory(path: str = ".", max_depth: int = 3, git_aware: bool = True) -> str:
103+
"""Lists directory contents as a tree structure with smart filtering.
104+
105+
Returns a compact, indented tree of files and directories. By default it
106+
respects .gitignore (via `git ls-files`) so the model never wastes tokens
107+
on build artefacts or vendored deps.
108+
109+
Args:
110+
path: Root directory to list (defaults to current directory '.').
111+
max_depth: How many levels deep to recurse (defaults to 3).
112+
git_aware: If True, use git ls-files to respect .gitignore (defaults to True).
113+
"""
114+
abs_path = os.path.abspath(path)
115+
if not os.path.isdir(abs_path):
116+
return f"Error: '{path}' is not a directory."
117+
118+
# ---- Fast path: use git ls-files when inside a repo ----
119+
if git_aware:
120+
try:
121+
tracked = subprocess.check_output(
122+
["git", "ls-files", "--cached", "--others", "--exclude-standard", path],
123+
text=True,
124+
stderr=subprocess.DEVNULL,
125+
).strip()
126+
if tracked:
127+
lines = tracked.splitlines()
128+
# Enforce max_depth for git-aware output as well.
129+
normalized_root = os.path.normpath(path)
130+
filtered: list[str] = []
131+
for p in lines:
132+
rel = (
133+
os.path.relpath(p, normalized_root)
134+
if normalized_root not in (".", "")
135+
else p
136+
)
137+
depth = rel.count(os.sep) + 1
138+
if depth <= max_depth:
139+
filtered.append(p)
140+
lines = filtered
141+
if len(lines) > 300:
142+
return (
143+
"\n".join(lines[:300])
144+
f"\n\n... and {len(lines) - 300} more files."
145+
)
146+
return "\n".join(lines)
147+
except Exception:
148+
pass # Not a git repo or git not available — fall through to manual walk
149+
150+
# ---- Fallback: manual os.scandir walk ----
151+
output_lines: list[str] = []
152+
153+
def _walk(dir_path: str, prefix: str, depth: int):
154+
if depth > max_depth:
155+
return
156+
try:
157+
entries = sorted(
158+
os.scandir(dir_path), key=lambda e: (not e.is_dir(), e.name.lower())
159+
)
160+
except PermissionError:
161+
return
162+
163+
visible_dirs = [
164+
e
165+
for e in entries
166+
if e.is_dir()
167+
and not _should_skip_dir(e.name)
168+
and not e.name.startswith(".")
169+
]
170+
files = [e for e in entries if e.is_file()]
171+
combined = visible_dirs + files
172+
173+
for i, entry in enumerate(combined):
174+
is_last = i == len(combined) - 1
175+
connector = "└── " if is_last else "├── "
176+
suffix = "/" if entry.is_dir() else ""
177+
output_lines.append(f"{prefix}{connector}{entry.name}{suffix}")
178+
if entry.is_dir():
179+
extension = " " if is_last else "│ "
180+
_walk(entry.path, prefix + extension, depth + 1)
181+
182+
_walk(abs_path, "", 1)
183+
result = "\n".join(output_lines)
184+
if len(result) > 4000:
185+
result = result[:4000] + "\n...[TRUNCATED]"
186+
return result or "Empty directory."
187+
188+
189+
# ---------------------------------------------------------------------------
190+
# Dedicated git status tool
191+
# ---------------------------------------------------------------------------
192+
193+
194+
def get_git_status(include_diff: bool = False) -> str:
195+
"""Returns a comprehensive git status summary in a single call.
196+
197+
Bundles branch name, porcelain status, and recent commits so the agent
198+
does not need multiple run_command calls.
199+
200+
Args:
201+
include_diff: If True, also include a condensed diff stat of staged and unstaged changes (defaults to False).
202+
"""
203+
parts: list[str] = []
70204

71-
# 1. Gather Git Context
205+
# Branch
72206
try:
73207
branch = subprocess.check_output(
74-
["git", "branch", "--show-current"], text=True, stderr=subprocess.STDOUT
208+
["git", "branch", "--show-current"], text=True, stderr=subprocess.DEVNULL
75209
).strip()
76-
status = subprocess.check_output(
77-
["git", "status", "-s"], text=True, stderr=subprocess.STDOUT
78-
)
79-
log = subprocess.check_output(
80-
["git", "log", "-n", "5", "--oneline"], text=True, stderr=subprocess.STDOUT
81-
)
82-
83-
summary_parts.append(
84-
f"### Git Context\n**Branch**: {branch}\n**Status**:\n{status if status else 'Clean'}\n**Recent Commits**:\n{log}"
85-
)
210+
parts.append(f"Branch: {branch}")
86211
except Exception:
87-
summary_parts.append("### Git Context\nNot a git repository or git error.")
212+
return "Not a git repository."
88213

89-
# 2. Gather Directory Structure (limited to root)
214+
# Status (porcelain for compact, stable output)
90215
try:
91-
files = os.listdir(".")
92-
summary_parts.append(f"### Root Directory Files\n{', '.join(files)}")
216+
status = subprocess.check_output(
217+
["git", "status", "--porcelain=v2", "--branch"],
218+
text=True,
219+
stderr=subprocess.DEVNULL,
220+
).strip()
221+
parts.append(f"Status:\n{status}" if status else "Status: Clean working tree")
93222
except Exception as e:
94-
summary_parts.append(f"### Directory Listing Error\n{e}")
223+
parts.append(f"Status error: {e}")
224+
225+
# Recent commits
226+
try:
227+
log = subprocess.check_output(
228+
["git", "log", "-n", "5", "--oneline", "--no-decorate"],
229+
text=True,
230+
stderr=subprocess.DEVNULL,
231+
).strip()
232+
if log:
233+
parts.append(f"Recent commits:\n{log}")
234+
except Exception:
235+
pass
236+
237+
# Optional diff stat
238+
if include_diff:
239+
try:
240+
diff = subprocess.check_output(
241+
["git", "diff", "--stat", "--no-color"],
242+
text=True,
243+
stderr=subprocess.DEVNULL,
244+
).strip()
245+
if diff:
246+
parts.append(f"Unstaged changes:\n{diff}")
247+
except Exception:
248+
pass
249+
try:
250+
staged = subprocess.check_output(
251+
["git", "diff", "--cached", "--stat", "--no-color"],
252+
text=True,
253+
stderr=subprocess.DEVNULL,
254+
).strip()
255+
if staged:
256+
parts.append(f"Staged changes:\n{staged}")
257+
except Exception:
258+
pass
259+
260+
return "\n\n".join(parts)
261+
262+
263+
# ---------------------------------------------------------------------------
264+
# Workspace summary (used at session start)
265+
# ---------------------------------------------------------------------------
266+
267+
268+
def get_workspace_summary() -> str:
269+
"""Gathers git context, project structure, and documentation to help the agent understand the whole project."""
270+
summary_parts = []
271+
272+
# 1. Git context — reuse the dedicated tool
273+
git_info = get_git_status()
274+
summary_parts.append(f"### Git Context\n{git_info}")
275+
276+
# 2. Project structure — reuse the dedicated tool (depth 2 to keep it compact)
277+
tree = list_directory(".", max_depth=2)
278+
summary_parts.append(f"### Project Structure\n{tree}")
95279

96280
# 3. Read important docs
97281
docs_to_check = [
@@ -214,6 +398,8 @@ def finish_task(message: str) -> str:
214398
"read_file": read_file,
215399
"write_file": write_file,
216400
"run_command": run_command,
401+
"list_directory": list_directory,
402+
"get_git_status": get_git_status,
217403
"search_repo": search_repo,
218404
"ask_user": ask_user,
219405
"finish_task": finish_task,
@@ -227,6 +413,8 @@ def finish_task(message: str) -> str:
227413
read_file,
228414
write_file,
229415
run_command,
416+
list_directory,
417+
get_git_status,
230418
search_repo,
231419
ask_user,
232420
finish_task,

0 commit comments

Comments
 (0)