Build agents in plain Python.

Bridle gives you three calls — step, branch, loop — that ask the model for a typed answer. Your code handles the rest: ifs, loops, function calls. No new framework to learn.

EXAMPLE — BRIEF_WRITER
python
from bridle import agent, branch, loop, step
from pydantic import BaseModel

class Source(BaseModel):
    url: str
    summary: str

class Brief(BaseModel):
    headline: str
    body: str

@agent(input=str, output=Brief, model="claude-sonnet-4-6")
def brief_writer(topic: str) -> Brief:
    sources = loop(
        f"gather sources on {topic}",
        schema=Source,
        until=lambda acc: len(acc) >= 3,
    )
    if not branch("is the evidence sufficient?", context=sources):
        return brief_writer(f"{topic} — go deeper")
    return step("write the brief", schema=Brief, context=(topic, sources))
  1. 01Typed input and output

    @agent declares what goes in and what comes out. The model's reply is validated against Brief before your function returns it.

  2. 02Your code decides when to stop

    loop calls the model until your Python predicate is true. Here it gathers sources until there are three.

  3. 03A typed yes or no

    branch returns a real Python value — a bool by default, or any Enum or Literal you pass. Use it in a normal if.

THE PRIMITIVES

Three calls. Each one returns a typed value.

STEP
step(prompt, *, schema, context=None, tools=())

Ask the model for one typed value. It can call tools along the way. You get back a validated instance of schema.

step docs
BRANCH
branch(prompt, *, schema=bool, context=None)

Ask the model for a decision. Defaults to True or False. Pass an Enum or Literal for more than two answers. Use it in an if.

branch docs
LOOP
loop(prompt, *, schema, until, tools=(), max_iterations=32)

Call the model in a loop until your predicate returns true. Hits the cap and raises LoopExhaustedError if the predicate never holds.

loop docs
WRAPPERS

Add caching, retries, and timeouts without changing your code.

Wrap a call to change how it runs. Stack wrappers in any order — the result is still a call.

cacheretrytimeoutwith_modelfallbackmocklog
python
cache(retry(timeout(step("..."), seconds=10), attempts=3))
bridlev0.1.0MIT