deepagents 0.3.8__py3-none-any.whl → 0.3.10__py3-none-any.whl

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.
@@ -219,17 +219,26 @@ def truncate_if_too_long(result: list[str] | str) -> list[str] | str:
219
219
  return result
220
220
 
221
221
 
222
- def _validate_path(path: str | None) -> str:
223
- """Validate and normalize a path.
222
+ def _normalize_path(path: str | None) -> str:
223
+ """Normalize a path to canonical form.
224
+
225
+ Converts path to absolute form starting with /, removes trailing slashes
226
+ (except for root), and validates that the path is not empty.
224
227
 
225
228
  Args:
226
- path: Path to validate
229
+ path: Path to normalize (None defaults to "/")
227
230
 
228
231
  Returns:
229
- Normalized path starting with /
232
+ Normalized path starting with / (without trailing slash unless it's root)
230
233
 
231
234
  Raises:
232
- ValueError: If path is invalid
235
+ ValueError: If path is invalid (empty string after strip)
236
+
237
+ Example:
238
+ _normalize_path(None) -> "/"
239
+ _normalize_path("/dir/") -> "/dir"
240
+ _normalize_path("dir") -> "/dir"
241
+ _normalize_path("/") -> "/"
233
242
  """
234
243
  path = path or "/"
235
244
  if not path or path.strip() == "":
@@ -237,12 +246,43 @@ def _validate_path(path: str | None) -> str:
237
246
 
238
247
  normalized = path if path.startswith("/") else "/" + path
239
248
 
240
- if not normalized.endswith("/"):
241
- normalized += "/"
249
+ # Only root should have trailing slash
250
+ if normalized != "/" and normalized.endswith("/"):
251
+ normalized = normalized.rstrip("/")
242
252
 
243
253
  return normalized
244
254
 
245
255
 
256
+ def _filter_files_by_path(files: dict[str, Any], normalized_path: str) -> dict[str, Any]:
257
+ """Filter files dict by normalized path, handling exact file matches and directory prefixes.
258
+
259
+ Expects a normalized path from _normalize_path (no trailing slash except root).
260
+
261
+ Args:
262
+ files: Dictionary mapping file paths to file data
263
+ normalized_path: Normalized path from _normalize_path (e.g., "/", "/dir", "/dir/file")
264
+
265
+ Returns:
266
+ Filtered dictionary of files matching the path
267
+
268
+ Example:
269
+ files = {"/dir/file": {...}, "/dir/other": {...}}
270
+ _filter_files_by_path(files, "/dir/file") # Returns {"/dir/file": {...}}
271
+ _filter_files_by_path(files, "/dir") # Returns both files
272
+ """
273
+ # Check if path matches an exact file
274
+ if normalized_path in files:
275
+ return {normalized_path: files[normalized_path]}
276
+
277
+ # Otherwise treat as directory prefix
278
+ if normalized_path == "/":
279
+ # Root directory - match all files starting with /
280
+ return {fp: fd for fp, fd in files.items() if fp.startswith("/")}
281
+ # Non-root directory - add trailing slash for prefix matching
282
+ dir_prefix = normalized_path + "/"
283
+ return {fp: fd for fp, fd in files.items() if fp.startswith(dir_prefix)}
284
+
285
+
246
286
  def _glob_search_files(
247
287
  files: dict[str, Any],
248
288
  pattern: str,
@@ -267,11 +307,11 @@ def _glob_search_files(
267
307
  ```
268
308
  """
269
309
  try:
270
- normalized_path = _validate_path(path)
310
+ normalized_path = _normalize_path(path)
271
311
  except ValueError:
272
312
  return "No files found"
273
313
 
274
- filtered = {fp: fd for fp, fd in files.items() if fp.startswith(normalized_path)}
314
+ filtered = _filter_files_by_path(files, normalized_path)
275
315
 
276
316
  # Respect standard glob semantics:
277
317
  # - Patterns without path separators (e.g., "*.py") match only in the current
@@ -281,9 +321,17 @@ def _glob_search_files(
281
321
 
282
322
  matches = []
283
323
  for file_path, file_data in filtered.items():
284
- relative = file_path[len(normalized_path) :].lstrip("/")
285
- if not relative:
324
+ # Compute relative path for glob matching
325
+ # If normalized_path is "/dir", we want "/dir/file.txt" -> "file.txt"
326
+ # If normalized_path is "/dir/file.txt" (exact file), we want "file.txt"
327
+ if normalized_path == "/":
328
+ relative = file_path[1:] # Remove leading slash
329
+ elif file_path == normalized_path:
330
+ # Exact file match - use just the filename
286
331
  relative = file_path.split("/")[-1]
332
+ else:
333
+ # Directory prefix - strip the directory path
334
+ relative = file_path[len(normalized_path) + 1 :] # +1 for the slash
287
335
 
288
336
  if wcglob.globmatch(relative, effective_pattern, flags=wcglob.BRACE | wcglob.GLOBSTAR):
289
337
  matches.append((file_path, file_data["modified_at"]))
@@ -357,11 +405,11 @@ def _grep_search_files(
357
405
  return f"Invalid regex pattern: {e}"
358
406
 
359
407
  try:
360
- normalized_path = _validate_path(path)
408
+ normalized_path = _normalize_path(path)
361
409
  except ValueError:
362
410
  return "No matches found"
363
411
 
364
- filtered = {fp: fd for fp, fd in files.items() if fp.startswith(normalized_path)}
412
+ filtered = _filter_files_by_path(files, normalized_path)
365
413
 
366
414
  if glob:
367
415
  filtered = {fp: fd for fp, fd in filtered.items() if wcglob.globmatch(Path(fp).name, glob, flags=wcglob.BRACE)}
@@ -390,21 +438,18 @@ def grep_matches_from_files(
390
438
  ) -> list[GrepMatch] | str:
391
439
  """Return structured grep matches from an in-memory files mapping.
392
440
 
393
- Returns a list of GrepMatch on success, or a string for invalid inputs
394
- (e.g., invalid regex). We deliberately do not raise here to keep backends
395
- non-throwing in tool contexts and preserve user-facing error messages.
396
- """
397
- try:
398
- regex = re.compile(pattern)
399
- except re.error as e:
400
- return f"Invalid regex pattern: {e}"
441
+ Performs literal text search (not regex).
401
442
 
443
+ Returns a list of GrepMatch on success, or a string for invalid inputs.
444
+ We deliberately do not raise here to keep backends non-throwing in tool
445
+ contexts and preserve user-facing error messages.
446
+ """
402
447
  try:
403
- normalized_path = _validate_path(path)
448
+ normalized_path = _normalize_path(path)
404
449
  except ValueError:
405
450
  return []
406
451
 
407
- filtered = {fp: fd for fp, fd in files.items() if fp.startswith(normalized_path)}
452
+ filtered = _filter_files_by_path(files, normalized_path)
408
453
 
409
454
  if glob:
410
455
  filtered = {fp: fd for fp, fd in filtered.items() if wcglob.globmatch(Path(fp).name, glob, flags=wcglob.BRACE)}
@@ -412,7 +457,7 @@ def grep_matches_from_files(
412
457
  matches: list[GrepMatch] = []
413
458
  for file_path, file_data in filtered.items():
414
459
  for line_num, line in enumerate(file_data["content"], 1):
415
- if regex.search(line):
460
+ if pattern in line: # Simple substring search for literal matching
416
461
  matches.append({"path": file_path, "line": int(line_num), "text": line})
417
462
  return matches
418
463