Bridle
Concepts

Code does control flow, the model does judgment

The split that runs through every Bridle program. Code decides what runs next; the model produces values.

A Bridle program has two layers. Python code holds the control flow — the ifs, the fors, the function calls, the exceptions. The model holds the judgment — producing typed values your code then routes on.

The split is the whole idea.

What goes on each side

Code does control flow. Sequencing, branching, looping, function calls, exception handling, retries. The shape of the program is fixed before any model call runs.

The model does judgment. Read this paragraph and produce a Plan. Read these sources and decide if the evidence is sufficient. Read this draft and produce a Brief. Each is a typed transformation: known input, known output schema, the model fills the middle.

You will recognize this if you have written async/await. The runtime decides when to suspend; you write the function. In Bridle, you decide when to call the model; the model decides what value to produce.

Why the split

Three things fall out of it.

Determinism where you want it. Loops terminate when your predicate says so, not when the model decides it is bored. Branches go where your if sends them, not where a router agent decides. The non-determinism is bounded to the inside of each typed hole.

Debuggability. Every model call is a discrete event in the trace, with its prompt, its inputs, its output, and the schema the output had to satisfy. You read the trace from top to bottom and you know what happened.

Testability. Each call is mockable. Wrap any step with mock(call, value) and you have a deterministic substitute. The control flow that calls the step is plain Python — test it the way you test any function.

A concrete example

The brief writer:

@agent(input=str, output=Brief, model="claude-sonnet-4-6")
def brief_writer(topic: str) -> Brief:
    plan = step("draft a research plan", schema=Plan, context=topic)

    sources: list[Source] = []
    for t in plan.topics:
        found = loop(
            f"gather sources on {t.title}",
            schema=Source,
            until=lambda acc: len(acc) >= 3,
            tools=[search],
        )
        sources.extend(found)

    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))

Walk it. Python's for iterates the topics. Python's if branches on the verdict. Python's recursion handles "not enough — try again." The model does four things: produce a Plan, produce Sources, produce a bool, produce a Brief. Each is a typed value with a fixed schema.

The model never picks the next state. It does not get to. Your code already knows where to go for each possible value the model could return.

What the model still owns

Inside a step, the model still has agency: it can call tools in any order, retry its own approach, refine its output before returning. That part is open-ended on purpose — judgment is what you are paying for.

What it does not own: where the program goes after the step returns. That is yours.

Next

On this page