dendrux
v0.2.0a1 · alphaGet started

Install dendrux and run an agent that pauses for human approval, then resumes it from a separate process.

Quickstart

In five minutes you'll install dendrux, build an agent that pauses to wait for a human's approval, and resume it from a different process. By the end you'll have seen the one thing dendrux does that other agent libraries don't: survive a process restart mid-run.

Every snippet below was executed against dendrux==0.2.0a1 from PyPI in a fresh conda env. The outputs are real.

1. Install

pip install "dendrux[anthropic]"

That installs dendrux and its Anthropic provider. Swap anthropic for openai if you'd rather use OpenAI. SQLAlchemy and SQLite are bundled as hard dependencies, so there's no separate database setup for local dev.

2. Configure your API key

Pick one of three patterns:

A. Environment variable (recommended for local dev). The provider reads it automatically — no code change needed.

export ANTHROPIC_API_KEY=sk-ant-...
provider = AnthropicProvider(model="claude-sonnet-4-6")  # picks up env var

B. Explicit argument. Make the source of the key visible in code. Useful when you load it from a secrets manager.

import os
provider = AnthropicProvider(
    model="claude-sonnet-4-6",
    api_key=os.environ["ANTHROPIC_API_KEY"],
)

C. .env file. Common in app-style projects.

pip install python-dotenv
echo "ANTHROPIC_API_KEY=sk-ant-..." > .env
from dotenv import load_dotenv
load_dotenv()
provider = AnthropicProvider(model="claude-sonnet-4-6")

If you forget all three, the provider fails fast at construction:

ValueError: AnthropicProvider needs an API key. Pass api_key='sk-...' or
set the ANTHROPIC_API_KEY environment variable.

For OpenAI, the env var is OPENAI_API_KEY and the message points at it instead.

3. Define an agent that pauses for approval

Create agent_def.py:

from dendrux import Agent, tool
from dendrux.llm.anthropic import AnthropicProvider
 
 
@tool()
async def refund(order_id: int) -> str:
    """Issue a refund for the given order. Requires manager approval."""
    return f"Refunded order {order_id}"
 
 
def build_agent() -> Agent:
    return Agent(
        provider=AnthropicProvider(model="claude-haiku-4-5"),
        prompt="You are a support agent. When asked for a refund, call the refund tool.",
        tools=[refund],
        require_approval=["refund"],
        database_url="sqlite+aiosqlite:///./quickstart.db",
    )

Three things worth noticing:

  1. The tool is just a Python function. No registry, no schema. Dendrux reads the type hints and docstring.
  2. require_approval=["refund"] tells dendrux: when the LLM tries to call refund, pause the run and wait for a human to approve or reject.
  3. database_url is what makes the run survive a process exit. Three options:
    • Omit it → in-memory only. Fine for one-shot scripts and tests; the run dies with the process.
    • SQLitesqlite+aiosqlite:///./quickstart.db writes to a single file. Zero setup — dendrux auto-creates tables on first connect. Ideal for local dev and small single-node apps. The .db file is what you back up.
    • Postgrespostgresql+asyncpg://user:pass@host/db for multi-process or production. Same code path. Run dendrux db migrate once to apply the schema (Alembic) before first use.

4. Start a run

starter.py:

import asyncio
from agent_def import build_agent
 
 
async def main() -> None:
    async with build_agent() as agent:
        result = await agent.run("Please refund order 42.")
 
    print(f"Status:    {result.status.value}")
    print(f"Run ID:    {result.run_id}")
    print(f"Iteration: {result.iteration_count}")
 
 
asyncio.run(main())

Run it:

python starter.py

Real output:

Status:    waiting_approval
Run ID:    01KPFXP9M96CXNR9JYT8KJ27J0
Iteration: 1

The agent called the LLM, the LLM decided to call refund, dendrux saw the approval policy, paused the run, and persisted everything to quickstart.db. The Python process exits. The database row remains.

5. Resume from a different process

resumer.py:

import asyncio
import sys
from agent_def import build_agent
 
 
async def main() -> None:
    run_id = sys.argv[1]
    async with build_agent() as agent:
        result = await agent.submit_approval(run_id, approved=True)
 
    print(f"Status:    {result.status.value}")
    print(f"Iteration: {result.iteration_count}")
    print(f"Answer:    {result.answer}")
 
 
asyncio.run(main())

Run it with the run ID from step 4:

python resumer.py 01KPFXP9M96CXNR9JYT8KJ27J0

Real output:

Status:    success
Iteration: 2
Answer:    I've successfully issued a refund for order 42. The refund has been processed and is pending manager approval.

The agent picked up where it left off, executed the refund tool now that the human had approved it, made one more LLM call to summarize, and finished. The starter process never knew the answer; the resumer process never saw the original question. They communicated through quickstart.db.

What you just saw

  • A run paused mid-task and persisted itself.
  • A different OS process resumed that exact run by ID.
  • The full step history (LLM calls, tool calls, the pause itself) is on disk in quickstart.db.

This is the foundation everything else in dendrux is built on. From here: