Agent File System Search: Grep and Glob Tools

2 min

1. Glob Tool Implementation

The glob tool has a fallback strategy for efficiency and resource optimization:

Glob Tool Fallback Strategy
Glob Tool Fallback Strategy

Two implementation approaches with different strengths:

  • glob package: Returns complete file information (metadata like size, modification time) — no extra operations needed. Native to Node.js.
  • ripgrep: Returns only file paths without metadata — requires additional stat calls. But ripgrep searches faster (Rust implementation, loads a binary at runtime).

Total execution time formula: Total = Search time + N × Per-file processing time

  1. glob package: per-file processing is negligible, so search time ≈ total time
  2. ripgrep: faster search time but adds per-file stat call overhead

Recommendations:

  • For development convenience: Use glob directly — faster development, no external dependencies
  • For search efficiency: Consider ripgrep — if you’re implementing grep too, ripgrep is the natural choice for both
  • For stability: Use fallback strategy — try ripgrep first, fall back to glob if ripgrep isn’t available or download fails

2. Grep Tool Implementation

Grep Four Implementation Priorities
Grep Four Implementation Priorities

Four implementations in priority order with fallback strategy:

  1. ripgrep: Rust binary — extremely fast search
  2. git grep: Reads from .git/index cached file list, skips expensive directory traversal
  3. System grep: Traditional C implementation, single-threaded recursive search — available on most Unix systems but not Windows
  4. JS grep: Pure JS implementation as last resort — uses glob for file listing, reads each file, regex matches line-by-line. Slowest.

3. Ripgrep Auto-Download Mechanism

Ripgrep Auto-download Mechanism
Ripgrep Auto-download Mechanism

Ripgrep commands need the full binary path for Node.js spawn:

async function grepWithRipgrep(pattern, cwd, options) {
  const rgPath = await Ripgrep.filepath(options.binDir);
  // Returns: /usr/bin/rg or ~/.reason/bin/rg
  const proc = spawn(rgPath, ['--line-number', '--no-heading', pattern], { cwd });
}

Path resolution strategy:

  1. Check memory cache — return if found
  2. Check system installation — return and cache if found
  3. Check local binary path — if found, cache and return; if not, download to appropriate directory

4. Timeout Control

Using AbortController/AbortSignal in Node.js for timeout control:

  • AbortController: The controller that sends “cancel” signals
  • AbortSignal: The signal passed to async operations, enabling cancellation

Three-step implementation:

Step 1: Create timeout signal function

export function createTimeoutSignal(
  timeoutMs: number,
  externalSignal?: AbortSignal
): { signal: AbortSignal; cleanup: () => void; isTimeout: () => boolean } {
  const controller = new AbortController();
  let timedOut = false;

  const timeoutId = setTimeout(() => {
    timedOut = true;
    controller.abort();
  }, timeoutMs);

  const abortHandler = () => {
    clearTimeout(timeoutId);
    controller.abort();
  };
  externalSignal?.addEventListener('abort', abortHandler, { once: true });

  const cleanup = () => {
    clearTimeout(timeoutId);
    externalSignal?.removeEventListener('abort', abortHandler);
  };

  return { signal: controller.signal, cleanup, isTimeout: () => timedOut };
}

Step 2: Create async operation wrapper

export async function withTimeout<T>(
  promiseFactory: (signal: AbortSignal) => Promise<T>,
  timeoutMs: number,
  operation: string,
  externalSignal?: AbortSignal
): Promise<T> {
  if (externalSignal?.aborted) throw createAbortError();

  const { signal, cleanup, isTimeout } = createTimeoutSignal(timeoutMs, externalSignal);

  try {
    const result = await promiseFactory(signal);
    cleanup();
    return result;
  } catch (error) {
    cleanup();
    if (isTimeout() && isAbortError(error)) {
      throw createTimeoutError(operation, timeoutMs);
    }
    throw error;
  }
}

Step 3: Pass cancel signal to async process

await withTimeout(
  (signal) => spawnAsync('long-command', [], { signal }),
  5000,
  'command execution',
  userCancelSignal
);