jeevesagent.tools.builtin
=========================

.. py:module:: jeevesagent.tools.builtin

.. autoapi-nested-parse::

   Built-in tools for filesystem + shell agents.

   Four tools that any :class:`~jeevesagent.Agent` can register to
   gain the canonical "Claude-Code-shaped" capability set:

   * :func:`read_tool` — read a text file
   * :func:`write_tool` — create or overwrite a file
   * :func:`edit_tool` — find-and-replace inside an existing file
   * :func:`bash_tool` — run a shell command, capture output

   All four are **factory functions**: they take a ``workdir`` (and
   in the case of ``bash_tool`` a few safety knobs) and return a
   ready-to-register :class:`Tool` instance. The closure captures the
   workdir, so the resulting tool is workdir-scoped — the model can't
   escape it via ``../`` traversal or absolute paths.

   Usage::

       from jeevesagent import (
           Agent, read_tool, write_tool, edit_tool, bash_tool,
       )

       agent = Agent(
           "You are a research agent.",
           model="gpt-4.1-mini",
           tools=[
               read_tool(workdir="/tmp/agent_work"),
               write_tool(workdir="/tmp/agent_work"),
               edit_tool(workdir="/tmp/agent_work"),
               bash_tool(workdir="/tmp/agent_work", timeout=30.0),
           ],
       )

   Or as a bundle::

       from jeevesagent import filesystem_tools, bash_tool

       agent = Agent(
           "...",
           model="...",
           tools=filesystem_tools("/tmp/agent_work") + [bash_tool("/tmp/agent_work")],
       )

   Safety
   ------

   * **Workdir-scoped by default.** Read / write / edit refuse paths
     that resolve outside the workdir. ``bash_tool`` runs commands
     with the workdir as ``cwd``.
   * **Timeout on bash.** Default 30 seconds; override via the
     ``timeout`` kwarg. Commands that exceed the timeout are killed.
   * **Destructive-command denylist.** ``bash_tool`` rejects a small
     set of obviously-dangerous patterns (``rm -rf /``, ``sudo``,
     ``mkfs``, etc.) by default. Override via the ``allow_pattern``
     callable for advanced use.
   * **Edit requires unique match.** ``edit_tool``'s ``old_string``
     must appear EXACTLY once in the file (unless ``replace_all=True``
     is passed in the call) — forces the model to provide enough
     context for unambiguous edits, the same approach Claude Code
     takes.

   These will be the foundation of the upcoming Deep Agent architecture
   (planner + filesystem state + subagent registry).



Exceptions
----------

.. autoapisummary::

   jeevesagent.tools.builtin.PathEscapeError


Functions
---------

.. autoapisummary::

   jeevesagent.tools.builtin.bash_tool
   jeevesagent.tools.builtin.default_workdir
   jeevesagent.tools.builtin.edit_tool
   jeevesagent.tools.builtin.filesystem_tools
   jeevesagent.tools.builtin.read_tool
   jeevesagent.tools.builtin.write_tool


Module Contents
---------------

.. py:exception:: PathEscapeError

   Bases: :py:obj:`ValueError`


   Raised when a tool argument resolves outside its workdir.

   Initialize self.  See help(type(self)) for accurate signature.


.. py:function:: bash_tool(workdir: pathlib.Path | str | None = None, *, name: str = 'bash', timeout: float = 30.0, allow_pattern: collections.abc.Callable[[str], bool] | None = None, extra_env: dict[str, str] | None = None) -> jeevesagent.tools.registry.Tool

   Build a :class:`Tool` that runs a shell command with the
   workdir as the current working directory.

   Default safety:

   * Commands matching the built-in destructive patterns
     (``rm -rf /``, ``sudo``, ``mkfs``, fork bombs, ...) are
     rejected before being executed.
   * Commands run with a default ``timeout`` of 30 seconds; the
     subprocess is killed on timeout.
   * The shell is invoked via ``/bin/sh -c <command>``, so
     pipelines + redirections work the way you'd expect.

   Knobs:

   * ``allow_pattern`` — a callable that takes the command string
     and returns True if the command should run. When provided, it
     OVERRIDES the default deny list — you take full responsibility.
   * ``extra_env`` — extra environment variables merged into the
     subprocess env.
   * ``timeout`` — seconds before the command is killed.

   ``workdir`` is optional; ``None`` uses the framework's default
   tempdir (shared with the other built-in tools).


.. py:function:: default_workdir() -> pathlib.Path

   Return the framework's default workdir for built-in tools,
   creating it lazily on first call.

   The directory is a fresh tempdir under ``$TMPDIR/jeeves_agent_*``,
   created once per process. All built-in tool factories share it
   when called without an explicit ``workdir`` argument, so an
   Agent that registers ``read_tool()`` and ``write_tool()`` (no
   args) sees the same place.

   The directory is NOT auto-cleaned at process exit — leave that
   to the OS's tempdir cleanup so debug data survives a crash.


.. py:function:: edit_tool(workdir: pathlib.Path | str | None = None, *, name: str = 'edit') -> jeevesagent.tools.registry.Tool

   Build a :class:`Tool` that does find-and-replace inside an
   existing file under ``workdir``.

   The tool's signature seen by the model:
       ``edit(path: str, old_string: str, new_string: str,
              replace_all: bool = False)``

   Behaviour matches Claude Code's Edit tool:

   * ``old_string`` must be EXACTLY present in the file. Mismatch
     (whitespace, indentation, line breaks) → error.
   * ``old_string`` must appear EXACTLY once in the file unless
     ``replace_all=True`` is passed — forces the model to give
     enough surrounding context for unambiguous matches.
   * ``new_string`` replaces ``old_string`` (or every occurrence
     if ``replace_all=True``).

   ``workdir`` is optional; ``None`` uses the framework's default
   tempdir (shared with the other built-in tools).


.. py:function:: filesystem_tools(workdir: pathlib.Path | str | None = None) -> list[jeevesagent.tools.registry.Tool]

   Return all three filesystem tools (read + write + edit)
   bound to a single workdir. ``bash_tool`` is excluded — pair
   them only when you want shell access too.

   ``workdir`` is optional; ``None`` uses the framework's default
   tempdir (shared with bash_tool() called the same way).


.. py:function:: read_tool(workdir: pathlib.Path | str | None = None, *, name: str = 'read', line_limit: int = _DEFAULT_READ_LINE_LIMIT) -> jeevesagent.tools.registry.Tool

   Build a :class:`Tool` that reads a text file under ``workdir``.

   The tool's signature seen by the model:
       ``read(path: str, offset: int = 0, limit: int | None = None)``

   Returns the file's text with line numbers prefixed (one
   line per output line), in the same format Claude Code's Read
   tool uses — that lets the ``edit`` tool work without ambiguity
   later. Long files are truncated to ``line_limit`` lines per
   call; pass ``offset`` / ``limit`` to read further chunks.

   Errors (file-not-found, path-escape) are returned as a string
   starting with ``"ERROR: "`` rather than raising — the model
   sees them as a tool result and can adjust.

   ``workdir`` is optional; ``None`` uses the framework's default
   tempdir (shared with the other built-in tools called without
   a workdir).


.. py:function:: write_tool(workdir: pathlib.Path | str | None = None, *, name: str = 'write', create_parents: bool = True) -> jeevesagent.tools.registry.Tool

   Build a :class:`Tool` that writes / overwrites a text file
   under ``workdir``.

   The tool's signature seen by the model:
       ``write(path: str, content: str)``

   Overwrites existing files. With ``create_parents=True`` (the
   default), missing parent directories are created automatically.

   Returns a confirmation string with the byte count, or an
   ``"ERROR: "``-prefixed message on failure.

   ``workdir`` is optional; ``None`` uses the framework's default
   tempdir (shared with the other built-in tools).


