Progressive complexity¶
Simple use cases stay simple. Complex workflows are possible without changing the core mental model.
LazyBridge is designed so the framework grows in complexity only when your problem grows. You learn the next rung when you need it, not before. Every rung is additive: the code you wrote at level 1 still works at level 9.
The ladder¶
1. Single agent
2. Agent with tools
3. Structured output
4. Sequential chain ──┐
5. Parallel fan-out ├── multi-agent composition
6. Agent as tool of another agent ──┘
7. Deterministic Plan
8. Plan with parallel band + routing
9. Checkpoint + resume
10. Human-in-the-loop gate
11. Session, exporters, OpenTelemetry
12. Custom engine / custom provider
You can stop at any rung. Most production systems live between rungs 4 and 9. Rung 12 exists; you're unlikely to need it.
The minimal change at each rung¶
The point of the ladder is that each step adds one concept to what you already know. Nothing rewinds.
1 — Single agent¶
from lazybridge import Agent, LLMEngine
agent = Agent(
engine=LLMEngine("claude-haiku-4-5"),
)
result = agent("Hello")
print(result.text())
Agent(engine=...) with each argument on its own line is the
canonical shape — every rung from here on adds to it without changing
it. Shorter forms (Agent("claude-haiku-4-5") and the
string-positional shortcut Agent("claude-haiku-4-5")) are sugar:
convenient for one-liners, but they hide the engine choice you'll need
to configure as soon as the agent does anything non-trivial. Learn the
canonical form first; reach for sugar only when you can already write
the canonical version from memory.
2 — Agent with tools¶
agent = Agent(
engine=LLMEngine("claude-haiku-4-5"),
tools=[Tool.wrap(get_weather, name="get_weather")],
)
result = agent("What's the weather in Paris?")
print(result.text())
The agent decides when to call the tool. You added a list element. The function's signature, type hints, and docstring become the tool schema — no JSON to write.
3 — Structured output¶
from pydantic import BaseModel
class Summary(BaseModel):
headline: str
bullets: list[str]
agent = Agent(
engine=LLMEngine("claude-haiku-4-5"),
output=Summary,
)
result = agent("Summarise the news")
print(result.payload.headline) # read .payload, not .text(), with output=
You added an output= argument. The framework validates the payload
against the model and re-prompts on validation errors.
4 — Sequential chain¶
from lazybridge import Agent, LLMEngine, Plan, Step
researcher = Agent(
engine=LLMEngine("claude-haiku-4-5"),
name="research",
tools=[web_search],
)
writer = Agent(
engine=LLMEngine("claude-haiku-4-5"),
name="write",
)
pipeline = Agent(
engine=Plan(Step("research"), Step("write")),
tools=[researcher, writer],
)
print(pipeline("Topic: AI agents in 2026").text())
The canonical sequential form is a Plan of named steps — same shape
you'll use for routing, parallel bands, and checkpoints later in the
ladder, so the mental model stays uniform as the workflow grows.
For a purely linear handoff with no other plan features,
Agent.chain(researcher, writer) is sugar for exactly the form above.
Reach for it when you want a one-liner; reach for the explicit Plan
when you can already see a router or a parallel band coming.
5 — Parallel fan-out¶
multi = Agent.parallel(researcher_a, researcher_b, researcher_c)
env = multi("Same task for everyone") # → Envelope (labelled-text join in .text())
# For typed per-branch access (advanced):
# branches = await multi.run_branches("Same task for everyone") # → list[Envelope]
Agent.parallel(...) is composition sugar for scripted fan-out: the
agents run concurrently against the same input and you get back a
single Envelope whose .text() is the labelled-text join across
branches. For typed per-branch access call
await parallel.run_branches(task) — that path returns list[Envelope].
Use it when you want every branch unconditionally and you care about
each individual result.
Use a Plan parallel band (rung 8) instead when you want concurrent
steps that aggregate into the next step via from_parallel_all, or
when only one branch should run based on a router. Use tools=[a, b, c]
when you want the LLM to decide which sub-agent to call.
6 — Agent as tool¶
supervisor = Agent(
engine=LLMEngine("claude-haiku-4-5"),
tools=[researcher], # researcher's name= becomes the tool name
)
The supervisor decides when to delegate to the researcher. This is the
hierarchical / sub-agent pattern, expressed as a tool list. Use
researcher.as_tool("alias") only when you need a surface name
different from the agent's name=.
7 — Deterministic Plan¶
from lazybridge import Plan, Step
pipeline = Agent(
engine=Plan(
Step("research"),
Step("write"),
),
tools=[researcher, writer],
)
print(pipeline("Topic: AI agents in 2026").text())
When you want the LLM to stop deciding the order, swap the engine for
Plan. The agent's interface stays identical: same agent(task) call,
same Envelope back. Steps reference sub-agents by name= (the same
name they have in tools=[...]); the plan is validated at construction
so broken references fail fast, before any LLM call.
8 — Parallel band + routing¶
These are two distinct patterns that look superficially alike; keep them separated until you know you want both.
8a — Exclusive routing with a rejoin point¶
triage picks one of legal / technical; the chosen step
runs, then control jumps straight to reply because triage
declared after_branches="reply". Without after_branches,
control would fall through to the next declared step in linear
order and both branches would still run — see the
routes_by reference for the full
semantics.
from typing import Literal
from pydantic import BaseModel
from lazybridge import Agent, Plan, Step
# routes_by="field" requires the step's output= to be a Pydantic
# model with that field declared as ``Literal[...]`` (or
# ``Literal[...] | None``). Both the routing Step AND the agent
# answering it must carry ``output=Triage`` so the payload exposes
# ``.category``.
class Triage(BaseModel):
category: Literal["legal", "technical"]
pipeline = Agent(
engine=Plan(
Step("triage",
output=Triage,
routes_by="category",
after_branches="reply"),
Step("legal"), # one of these runs
Step("technical"), # (routes_by picks one)
Step("reply"), # rejoin point
),
tools=[triage, legal, technical, write_reply], # triage agent has output=Triage too
name="exclusive_routing", # required for non-LLM engines
)
Execution order when triage.payload.category == "legal":
triage → legal → reply. technical does not run.
8b — Parallel band fanning into a join step¶
No router; legal and technical run concurrently (the
contiguous parallel=True band is gathered into a single
asyncio.gather); reply then sees both outputs aggregated via
from_parallel_all("legal").
from lazybridge import Agent, Plan, Step, from_parallel_all
pipeline = Agent(
engine=Plan(
Step("triage"),
Step("legal", parallel=True),
Step("technical", parallel=True),
Step("reply", context=from_parallel_all("legal")), # aggregates the band
),
tools=[triage, legal, technical, write_reply],
name="parallel_band",
)
Execution order: triage → (legal ∥ technical) → reply.
Mixing routing with parallel=True on the routed targets is
intentionally allowed but not what most users want — routing
disables the band gather semantics for the routed step. Pick one of
the two patterns above first; only combine when you really need
both.
9 — Checkpoint + resume¶
from lazybridge import Plan, Step, Store
store = Store(db="runs.db")
pipeline = Agent(
engine=Plan(*steps, store=store, checkpoint_key="ticket-42"),
tools=[...],
)
# crash...
resumed = Agent(
engine=Plan(*steps, store=store, checkpoint_key="ticket-42", resume=True),
tools=[...],
)
Pass a persistent Store and a key. After a crash, build the same plan
with resume=True and call it again — execution picks up at the failed
step.
10 — Human-in-the-loop¶
from lazybridge import Agent, Plan, Step
from lazybridge.ext.hil import HumanEngine
approval = Agent(engine=HumanEngine(timeout=300), name="approve")
pipeline = Agent(
engine=Plan(
Step("draft"),
Step("approve"),
Step("send"),
),
tools=[draft, approval, send],
)
A human approval is just another agent — the engine is HumanEngine
instead of LLMEngine, but the agent slots into the plan exactly like
any other tool.
human_agent(timeout=300, name="approve") is sugar for the
Agent(engine=HumanEngine(...)) line above; supervisor_agent(...)
does the same for SupervisorEngine. Use the sugar for one-liners; use
the canonical form when you need to see the engine choice at the call
site.
11 — Observability¶
from lazybridge import Agent, LLMEngine, Session, JsonFileExporter
session = Session(exporters=[JsonFileExporter("events.jsonl")])
agent = Agent(
engine=LLMEngine("claude-haiku-4-5"),
session=session,
)
Add a Session once at the top. Every nested agent, tool call, and
step emits events to it. The OpenTelemetry exporter is one extra
import away.
12 — Custom engine / provider¶
When you need an engine LazyBridge doesn't ship, implement BaseEngine.
When you need a provider LazyBridge doesn't ship, implement BaseProvider.
Both are stable extension points.
The runnable mapping¶
The repository ships nine example files — one per rung you're likely to care about. Most are walked through under Recipes; all are runnable directly from the command line.
| Rung | Example file |
|---|---|
| 2 | examples/langgraph/01_react_agent_weather.py |
| 4 | examples/crewai/02_research_and_report.py |
| 6 | examples/langgraph/02_supervisor_research_math.py |
| 7 | examples/patterns/plan_tool.py |
| 7-8 | examples/patterns/agent_builds_plan.py |
| 7-8 | examples/patterns/dynamic_planner.py |
| 11 | examples/viz_demo.py |
The anti-pattern worth naming¶
The biggest mistake new users make is starting at rung 7.
Reach for Plan when you actually need determinism, validation, parallel
bands, or checkpoints. Reach for sub-agents when a sub-agent has a clear
distinct responsibility. Don't reach for either because they look more
serious. A single Agent with three tools is often the right shape for
a task that looks complex on the whiteboard.
The framework rewards picking the lowest rung that solves your problem.
See also¶
- Mental model — the Engine + Tools + State decomposition that all twelve rungs share.
- Everything is a tool — the composition rule that makes rungs 4-6 a one-line change.
- Quickstart — rungs 1 and 2, end to end.