Skip to content

Write your first tool

You have a running agent from the previous tutorial. Now you'll add a custom tool, restart the container, and watch the lead agent call it.

The tool in this tutorial is deliberately silly — it reports the current git SHA of the repo on disk. The point is to learn the wire-up, not the domain logic.

1. Write the tool

Open tools/lg_tools.py and add, near the other @tool functions:

python
import subprocess

@tool
async def git_sha(short: bool = True) -> str:
    """Return the current git SHA of the agent's source tree.

    Args:
        short: If True (default), return the 7-character short SHA.
            If False, return the full 40-character SHA.
    """
    args = ["git", "rev-parse"]
    if short:
        args.append("--short")
    args.append("HEAD")
    try:
        out = subprocess.run(args, capture_output=True, text=True, timeout=5)
    except (OSError, subprocess.SubprocessError) as e:
        return f"Error: could not run git: {e}"
    if out.returncode != 0:
        return f"Error: git exited {out.returncode}: {out.stderr.strip()}"
    return out.stdout.strip()

Then register it in get_all_tools() at the bottom of the same file:

python
def get_all_tools(knowledge_store=None, scheduler=None, inbox_store=None, beads_store=None):
    tools = [current_time, calculator, web_search, fetch_url, ask_human, request_user_input]
    tools.append(git_sha)   # ← new
    # ... the function then extends `tools` with github/notes/memory/scheduler/
    #     inbox/beads/peer tools and applies the `tools.disabled` denylist.
    return tools

(For a real fork the no-edit path is a pluginregister_tools([git_sha]) — which adds the tool without touching get_all_tools. Editing the function directly, as here, is the quickest way to see it work in the tutorial.)

2. Allow the subagent to use it (optional)

If you want the researcher subagent to be able to call git_sha, add it to the allowlist in graph/subagents/config.py. Append rather than replace — dropping the bundled defaults removes the researcher's memory tools:

python
RESEARCHER_CONFIG = SubagentConfig(
    # ...
    tools=[
        "current_time", "web_search", "fetch_url",
        "memory_recall", "memory_ingest", "memory_list",
        "git_sha",   # ← new
    ],
    # ...
)

(Subagent tool allowlists are strict — tools not listed here are invisible to the subagent even if they're registered with the lead agent.)

3. Rebuild and restart

bash
docker build -t my-agent:local .
docker run --rm -p 7870:7870 \
    -e AGENT_NAME=my-agent \
    -e OPENAI_API_KEY="$LITELLM_MASTER_KEY" \
    my-agent:local

4. Ask the agent

In the chat UI:

What SHA is the agent running right now?

You should see a tool-call card for git_sha (running → done, with the result), and then a response weaving the SHA into natural language. (In the React console it renders as a collapsible tool-call card; the legacy Gradio --ui full tier shows 🔧/ frames.)

What to notice

  • The tool's docstring is the LLM's spec. First line is the summary; Args: entries become the parameter docs the LLM reads. Spend effort on docstrings.
  • Errors are strings, not exceptions. return f"Error: ..." lets the LLM read the failure and retry or give up gracefully. If you raise, the exception bubbles to the A2A handler and surfaces as a 500 to the caller.
  • The @tool decorator makes the function callable via .ainvoke({"key": value}) in tests, and async LangGraph nodes in production. The template's test suite uses the former pattern.

Testing your tool

Add a test next to tests/test_starter_tools.py:

python
import pytest

@pytest.mark.asyncio
async def test_git_sha_returns_something_sha_shaped():
    from tools.lg_tools import git_sha
    result = await git_sha.ainvoke({})
    # Either a 7-char hex SHA or a clean error — never an exception
    assert result.startswith("Error:") or len(result) == 7

The template runs tests via pytest with pytest-asyncio in auto mode — no extra setup.

Where to go next

Part of the protoLabs autonomous development studio.