Skip to main content

gantrygraph

Autonomous agent framework for Python. Screenshot → think → act. LangGraph inside. Zero boilerplate outside.

PyPI License: MIT Python 3.11+

from gantrygraph import GantryEngine, gantry_tool
from gantrygraph.perception import DesktopScreen
from gantrygraph.actions import MouseKeyboardTools
from langchain_anthropic import ChatAnthropic

@gantry_tool
async def read_jira(ticket_id: str) -> str:
    """Fetch a Jira ticket and return its description."""
    return await jira_client.get(ticket_id)

agent = GantryEngine(
    llm=ChatAnthropic(model="claude-sonnet-4-6"),
    perception=DesktopScreen(),
    tools=[MouseKeyboardTools(), read_jira],
    max_steps=50,
)
agent.run("Open PROJ-123 in Jira and submit the fix.")

Full docs at gantrygraph.com.


Why gantrygraph?

gantrygraph Raw LangGraph AutoGen
Visual computer use (screenshot + click) ✅ built-in
MCP tool servers ✅ built-in
@gantry_tool — any function in 1 line partial
Human-in-the-loop (suspend / resume) manual partial
Stealth browser (bot-detection bypass) ✅ built-in
Persistent browser sessions profile_dir
Set-of-Mark: click by element ID, not coords ✅ built-in
Screenshot pipeline (annotate / compress) ✅ built-in
import gantrygraph never fails
Strict-typed (mypy strict) partial

Install

# Core only (no GUI, no browser, no cloud)
pip install gantrygraph

# Desktop automation (screenshot + mouse/keyboard)
pip install 'gantrygraph[desktop]'

# Web scraping / form filling / browser automation
pip install 'gantrygraph[browser]'
playwright install chromium

# REST server (POST /run, SSE streaming)
pip install 'gantrygraph[cloud]'

# Persistent semantic memory (ChromaDB)
pip install 'gantrygraph[memory]'

# Ultra-light TTL memory — Rust HNSW, 48 bytes/vector, entries auto-expire
pip install 'gantrygraph[minivecdb]'

# macOS accessibility tree — interact with any native app, zero vision tokens
pip install 'gantrygraph[desktop-ax]'

# Everything
pip install 'gantrygraph[all]'

Common patterns

Add a custom tool

from gantrygraph import GantryEngine, gantry_tool
from langchain_anthropic import ChatAnthropic

@gantry_tool
def search_orders(query: str, limit: int = 5) -> str:
    """Search the order management system by keyword."""
    rows = db.execute("SELECT * FROM orders WHERE ... LIMIT ?", query, limit)
    return "\n".join(str(r) for r in rows)

agent = GantryEngine(
    llm=ChatAnthropic(model="claude-sonnet-4-6"),
    tools=[search_orders],
)
agent.run("Find all overdue orders and summarise them.")

Use @gantry_tool(destructive=True) to tag a tool as destructive — the engine automatically gates it behind approval_callback without any manual guardrail config.

Filesystem and shell with security

from gantrygraph import GantryEngine
from gantrygraph.actions import FileSystemTools, ShellTools
from gantrygraph.security import WorkspacePolicy, ShellDenylist, BudgetPolicy
from langchain_anthropic import ChatAnthropic

agent = GantryEngine(
    llm=ChatAnthropic(model="claude-sonnet-4-6"),
    workspace_policy=WorkspacePolicy.restricted("/app"),   # auto-wires FS + shell
    tools=[
        ShellTools(
            workspace="/app",
            allowed_commands=["pytest", "ruff", "git"],
            denylist=ShellDenylist.strict(),               # blocks rm -rf, curl|bash, etc.
            max_output_chars=2000,                         # prevent log-file token floods
        ),
    ],
    budget=BudgetPolicy(max_steps=30, max_wall_seconds=120.0),
)
agent.run("Run the test suite, fix any lint errors, and commit the result.")

Browser agent with stealth + persistent session

Stays logged in across runs — no QR scan on every execution.

from gantrygraph import GantryEngine
from gantrygraph.actions import BrowserTools
from langchain_anthropic import ChatAnthropic

# First run: headless=False so you can complete the login once.
# All subsequent runs use headless=True — session is saved on disk.
tools = BrowserTools(
    headless=True,
    stealth=True,                                        # bot-detection bypass (default)
    profile_dir="~/.gantrygraph/profiles/whatsapp",     # persistent cookies + IndexedDB
)

agent = GantryEngine(
    llm=ChatAnthropic(model="claude-sonnet-4-6"),
    tools=[tools],
    perception_mode="axtree",   # accessibility tree instead of screenshots — 80% cheaper
    message_window=20,          # bound context growth on long runs
    enable_caching=True,        # Anthropic prompt cache
    max_steps=30,
)

result = agent.run(
    "Open WhatsApp Web, find 'Mamma', and send the Cacio e Pepe recipe."
)

Vision pipeline — annotated screenshots and precise clicks

Attach a PerceptionPipeline to WebPage and BrowserTools to transform screenshots before they reach the LLM. The SetOfMarkAnnotator draws numbered boxes over every interactive element; the agent then calls browser_click_som(element_id=N) instead of guessing pixel coordinates — 100% precision on every labelled element.

from gantrygraph import GantryEngine
from gantrygraph.actions import BrowserTools
from gantrygraph.perception import WebPage
from gantrygraph.vision import (
    PerceptionPipeline,
    SetOfMarkAnnotator,   # numbered boxes on every interactive element
    Downsample,           # cap resolution before encoding
    ConvertToWebP,        # ~30% smaller payload vs PNG
)
from langchain_anthropic import ChatAnthropic

pipeline = PerceptionPipeline([
    SetOfMarkAnnotator(),        # run first — bounding boxes at full resolution
    Downsample(max_width=1280),  # then shrink
    ConvertToWebP(quality=85),   # then compress
])

web = WebPage(url="https://example.com", vision_pipeline=pipeline)
tools = BrowserTools(web_page=web, vision_pipeline=pipeline)
# pipeline enables browser_click_som — click element #3 on the annotated screenshot

agent = GantryEngine(
    llm=ChatAnthropic(model="claude-sonnet-4-6"),
    perception=web,
    tools=[tools],
    max_steps=30,
)
agent.run("Click the 'Accept cookies' button and fill in the signup form.")

Filters are composable — subclass ImageFilter to add your own (custom crop, watermark removal, domain-specific annotation).

Connect an MCP server

from gantrygraph import GantryEngine
from gantrygraph.mcp import MCPClient
from langchain_anthropic import ChatAnthropic

async def main():
    async with MCPClient("npx -y @modelcontextprotocol/server-github") as mcp:
        agent = GantryEngine(
            llm=ChatAnthropic(model="claude-sonnet-4-6"),
            tools=[mcp],
            max_steps=20,
        )
        result = await agent.arun("Open a PR that adds a CHANGELOG entry for v1.2.0.")
        print(result)

Token cost controls

Four orthogonal knobs that together reduce token spend by 5–10× on long tasks:

from gantrygraph import GantryEngine
from gantrygraph.actions import ShellTools
from langchain_anthropic import ChatAnthropic

agent = GantryEngine(
    llm=ChatAnthropic(model="claude-sonnet-4-6"),
    tools=[ShellTools(max_output_chars=2000)],   # (1) truncate shell output
    perception_mode="axtree",                    # (2) text tree instead of screenshots
    message_window=20,                           # (3) sliding context window
    enable_caching=True,                         # (4) Anthropic prompt cache
)
Control What it does Savings
max_output_chars on ShellTools Truncates stdout/stderr Prevents 100k-token log dumps
perception_mode="axtree" Text accessibility tree vs screenshot ~80% per observe step
message_window=N Keeps only the last N messages Caps O(N²) history growth
enable_caching=True Anthropic prompt cache on system messages Up to 90% input-token discount

See the Cost Optimization guide for a full breakdown.


Security

GantryGraph ships with layered, opt-in security controls:

import os
from gantrygraph import GantryEngine, gantry_tool
from gantrygraph.actions import ShellTools
from gantrygraph.security import (
    GuardrailPolicy, WorkspacePolicy, BudgetPolicy,
    ShellDenylist, GantrySecrets,
)

@gantry_tool(destructive=True)
def drop_table(table: str) -> str:
    """Drop a database table permanently."""
    ...

agent = GantryEngine(
    llm=my_llm,
    tools=[
        ShellTools(
            workspace="/app",
            denylist=ShellDenylist.strict(),   # blocks rm -rf /, dd wipe, curl|bash, etc.
        ),
        drop_table,                             # auto-requires approval
    ],
    workspace_policy=WorkspacePolicy.restricted("/app"),
    guardrail=GuardrailPolicy(requires_approval={"shell_run"}),
    budget=BudgetPolicy(max_steps=50, max_tokens=20_000, max_wall_seconds=300),
    approval_callback=lambda tool, args: input(f"Allow {tool}({args})? [y/N] ") == "y",
    secrets=GantrySecrets({"DB_PASS": os.environ["DB_PASSWORD"]}),
)
Layer Class What it does
Approval gate GuardrailPolicy Require human sign-off before listed tools run
Auto-approval @gantry_tool(destructive=True) Tag a tool as destructive — auto-added to the gate
Shell firewall ShellDenylist Block rm -rf /, fork bombs, curl|bash, SSH key reads
Blind secrets GantrySecrets Keep credentials out of the LLM context window
Path sandbox WorkspacePolicy Restrict file/shell tools to allowed directories
Cost cap BudgetPolicy Hard limit on steps, tokens, and wall-clock time

Architecture

gantrygraph/
  core/         ABCs and shared types — no I/O, no side effects
  engine/       LangGraph graph wiring (observe → think → act → review)
  perception/   Desktop screenshot (mss+PIL), web accessibility (Playwright)
  actions/      Mouse/keyboard (pyautogui), browser (Playwright), filesystem, shell
  mcp/          MCP client — dynamic StructuredTool generation from any MCP server
  memory/       InMemoryStore, ChromaDB, MiniVecDb (Rust HNSW + TTL)
  perception/   DesktopScreen (screenshot), DesktopAXTree (macOS AX API), WebPage (Playwright)
  security/     GuardrailPolicy, WorkspacePolicy, BudgetPolicy, ShellDenylist, GantrySecrets
  swarm/        Multi-agent supervisor pattern
  cloud/        FastAPI REST server + SSE streaming
  telemetry/    OpenTelemetry span exporter
  vision/       Screenshot pipeline (SetOfMarkAnnotator, Downsample, ConvertToWebP, Grayscale)
  tool.py       @gantry_tool decorator

The agent loop is a LangGraph StateGraph:

START → memory_recall → observe → think → act → review
                                                   │
                              ┌────────────────────┘
                              ▼
                          is_done or max_steps?
                              │yes          │no
                             END          observe

Every node is a pure async def. Callbacks (on_event, approval_callback) support both def and async def via ensure_awaitable.


Development

git clone https://github.com/GantryGraph/GantryGraph
cd gantrygraph
pip install -e ".[all,dev]"

pytest tests/unit/           # fast, no display needed
pytest tests/integration/    # needs MCP subprocess + Playwright
mypy src/gantrygraph --strict
ruff check src/ tests/
ruff format src/ tests/

See CONTRIBUTING.md for the full contributor guide.


License

MIT — see LICENSE.

Release files for gantrygraph 0.8.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for gantrygraph 0.8.0
File Size Uploaded
gantrygraph-0.8.0.tar.gz 125.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for gantrygraph 0.8.0
File Interpreter ABI Platform
gantrygraph-0.8.0-py3-none-any.whl Python 3 none any Details

Total release size: 225.7 kB

Release files / gantrygraph-0.8.0.tar.gz

Download URL gantrygraph-0.8.0.tar.gz
Size 125.6 kB
Tags Source
SHA-256 checksum
How to use checksums
266eecf2d9177dc6422e23b2a263f12ae4c55ae7855da2b6af6b4c3efdc1c833
BLAKE2b-256 checksum
How to use checksums
bfc5bdcd76cdfac5839f981bda0a64a2fcb8527c0ecd828b8622eee29280e777
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jun 4, 2026.

Transparency log

Release files / gantrygraph-0.8.0-py3-none-any.whl

Download URL gantrygraph-0.8.0-py3-none-any.whl
Size 100.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
307c3446f9d4f746c2f931a14c0553567f9d0e4b0faaa8c0eef40b71ba0a91e0
BLAKE2b-256 checksum
How to use checksums
cc1625267a7c974d41264ef9f0f4b8ba511f0d72ae3f8028ea325c50da3c7484
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jun 4, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.8.0 This release

2 release files

0.7.0

2 release files

0.6.0

2 release files

0.5.0

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page