Skip to content

Commit d1844aa

Browse files
committed
feat:Introduced new tools for getting the directory structure and git status, earlier agent had to loop three times to get it
1 parent 6f265eb commit d1844aa

2 files changed

Lines changed: 213 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: 195 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -64,34 +64,205 @@ 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+
if len(lines) > 300:
129+
return (
130+
"\n".join(lines[:300])
131+
+ f"\n\n... and {len(lines) - 300} more files."
132+
)
133+
return "\n".join(lines)
134+
except Exception:
135+
pass # Not a git repo or git not available — fall through to manual walk
136+
137+
# ---- Fallback: manual os.scandir walk ----
138+
output_lines: list[str] = []
139+
140+
def _walk(dir_path: str, prefix: str, depth: int):
141+
if depth > max_depth:
142+
return
143+
try:
144+
entries = sorted(
145+
os.scandir(dir_path), key=lambda e: (not e.is_dir(), e.name.lower())
146+
)
147+
except PermissionError:
148+
return
149+
150+
visible_dirs = [
151+
e
152+
for e in entries
153+
if e.is_dir()
154+
and not _should_skip_dir(e.name)
155+
and not e.name.startswith(".")
156+
]
157+
files = [e for e in entries if e.is_file()]
158+
combined = visible_dirs + files
159+
160+
for i, entry in enumerate(combined):
161+
is_last = i == len(combined) - 1
162+
connector = "└── " if is_last else "├── "
163+
suffix = "/" if entry.is_dir() else ""
164+
output_lines.append(f"{prefix}{connector}{entry.name}{suffix}")
165+
if entry.is_dir():
166+
extension = " " if is_last else "│ "
167+
_walk(entry.path, prefix + extension, depth + 1)
168+
169+
_walk(abs_path, "", 1)
170+
result = "\n".join(output_lines)
171+
if len(result) > 4000:
172+
result = result[:4000] + "\n...[TRUNCATED]"
173+
return result or "Empty directory."
174+
175+
176+
# ---------------------------------------------------------------------------
177+
# Dedicated git status tool
178+
# ---------------------------------------------------------------------------
179+
180+
181+
def get_git_status(include_diff: bool = False) -> str:
182+
"""Returns a comprehensive git status summary in a single call.
183+
184+
Bundles branch name, porcelain status, and recent commits so the agent
185+
does not need multiple run_command calls.
186+
187+
Args:
188+
include_diff: If True, also include a condensed diff stat of staged and unstaged changes (defaults to False).
189+
"""
190+
parts: list[str] = []
70191

71-
# 1. Gather Git Context
192+
# Branch
72193
try:
73194
branch = subprocess.check_output(
74-
["git", "branch", "--show-current"], text=True, stderr=subprocess.STDOUT
195+
["git", "branch", "--show-current"], text=True, stderr=subprocess.DEVNULL
75196
).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-
)
197+
parts.append(f"Branch: {branch}")
86198
except Exception:
87-
summary_parts.append("### Git Context\nNot a git repository or git error.")
199+
return "Not a git repository."
88200

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

96267
# 3. Read important docs
97268
docs_to_check = [
@@ -214,6 +385,8 @@ def finish_task(message: str) -> str:
214385
"read_file": read_file,
215386
"write_file": write_file,
216387
"run_command": run_command,
388+
"list_directory": list_directory,
389+
"get_git_status": get_git_status,
217390
"search_repo": search_repo,
218391
"ask_user": ask_user,
219392
"finish_task": finish_task,
@@ -227,6 +400,8 @@ def finish_task(message: str) -> str:
227400
read_file,
228401
write_file,
229402
run_command,
403+
list_directory,
404+
get_git_status,
230405
search_repo,
231406
ask_user,
232407
finish_task,

0 commit comments

Comments
 (0)