Skip to content

Blog

This blog is an experiment: written and published by LazyBridge agents

Everything on this blog runs through a small pipeline of LazyBridge agents — one researches a topic, one drafts the post, one gives it an editorial pass, and one checks it against what LazyBridge actually ships before anything is published. A human reviews and approves every post before it goes live; no post is auto-published without that check.

We're publishing it in the open, disclosed like this, because it's also the most honest showcase we have: LazyBridge's own marketing content is produced by a real LazyBridge pipeline, not by a copywriter. If a post ever gets something about the framework wrong, that's a bug in the pipeline — open an issue and we'll fix it.

Claude Writes, Codex Reviews — No Second API Key Required

Written by LazyBridge agents, reviewed by a human before publishing

This post came out of a LazyBridge agent pipeline — research, draft, and editorial pass, each a separate agent — and was approved by a human before it went live. More on why in the blog's About note.

You've got a writer agent turning out a small utility function — say, one that decides whether it's actually safe to deploy right now. It mostly works. Then one day someone points out it waves a green light at 3:00pm on the dot on a Friday, because the boundary check used > instead of >=, and you spend twenty minutes tracing a very avoidable incident back to a one-character bug a second pair of eyes would have caught in five seconds. So you think: fine, I'll add a second agent whose whole job is to review the first agent's output before it ships.

And then you stop mid-keystroke, doing the math. If the writer costs you some amount per call against a metered API key — a key you pay for by the token, the way ANTHROPIC_API_KEY or OPENAI_API_KEY billing works — then a reviewer that runs on every single output, plus retries when it rejects something, could easily double or triple that bill for what's essentially a safety net. Is a nagging feeling of "this output looks fine, probably" really worth 3x the spend?

That's a legitimate thing to hesitate over. It's also the wrong question to be asking about LazyBridge's ClaudeCodeEngine and CodexEngine, because neither one touches a metered API key at all.

What a "coding CLI" is, and why it changes the billing question

A coding CLI here means a local program you're already logged into on your machine — claude (the Claude Code CLI) or codex (the Codex CLI or desktop app) — authenticated against your existing Claude Pro/Max or ChatGPT/Codex plan, the same way you'd authenticate a browser session. No ANTHROPIC_API_KEY, no OPENAI_API_KEY, nothing that shows up as per-token usage on an API dashboard.

ClaudeCodeEngine drives the local claude CLI directly and runs requests against your Pro/Max subscription login. CodexEngine drives the local codex CLI (or the Codex desktop app, resolved via a codex_executable() lookup) and runs requests against your ChatGPT/Codex plan login. What changes is which execution path a given agent's requests travel down — subscription-authenticated CLI process vs. metered API key — not whether the requests are somehow free. They aren't: your Claude or ChatGPT plan has its own usage limits and terms, and running two agents through two CLIs still uses two allotments of something.

Write, review, retry — not a routing graph

The pattern LazyBridge gives you for "have a second agent check the first agent's work" is verify=, and the loop underneath it is called verify_with_retry(). In plain terms:

  1. Run the writer agent on the task.
  2. Hand the writer's output, plus the original task, to whatever you passed as verify=, and ask it a yes/no question: approved, or rejected with a reason?
  3. Parse the verdict. Anything that reads as approval passes. Anything that reads as rejection — or that the parser doesn't recognize — is treated as REJECTED. The failure mode is fail-safe, not fail-open.
  4. If rejected, retry. But not the reviewer's complaint in isolation — LazyBridge re-runs the original task, with the rejection feedback appended as extra context, so the writer gets another full attempt informed by what went wrong.
  5. This repeats up to max_verify attempts (default 3). If the last attempt is still rejected, LazyBridge returns it anyway — you get a result, not an exception (more on what that means for you below).

The important design choice here is what verify= actually requires: anything with a .run() method — any Agent, running on any engine — or a plain callable. That's it. There's no separate routing graph, no special "reviewer" subclass. Wiring a Claude-writes/Codex-reviews loop is just picking two engines and passing one Agent as the verify= argument to another's .run() call.

The scenario: should I deploy right now?

To make this concrete — and because not every worked example needs to be a boring input-sanitizer — here's a task every developer has an opinion about: write should_i_deploy(now), which looks at the current time and returns whether it's safe to push to production, plus a message explaining why or why not. The rule: blocked from 3:00pm onward on a Friday, blocked all day on the weekend, fine otherwise.

The subtlety is in exactly where "3:00pm onward" starts:

friday_cutoff = datetime.time(15, 0, 0)
is_friday_afternoon = weekday == 4 and now.time() >= friday_cutoff

That >= is doing real work. Write > instead, by a single-character slip, and 3:00pm precisely stops being blocked — the one moment the rule exists to name explicitly turns into the one moment it silently lets through. It's the classic shape of a boundary bug: everything either side of the line behaves correctly, so a handful of obvious manual checks (2:00pm, 4:00pm, Saturday) all pass, and the actual mistake only shows up if someone happens to test the exact second the rule changes. That's precisely the kind of thing a bored human reviewer skims past and a second agent, asked specifically to check the logic rather than skim the vibe, is well-suited to catch.

The full loop: Claude writes, Codex reviews

Now for the actual write/review/retry example, targeting that same task. This needs the claude and codex CLIs installed and logged in — both under your existing subscriptions, no API keys involved:

from lazybridge import Agent
from lazybridge.engines import ClaudeCodeEngine, CodexEngine

TASK = (
    "Write a Python function `should_i_deploy(now)` where `now` is a "
    "datetime.datetime. Return a tuple (bool, str): False with a witty, "
    "dry, deadpan-sysadmin-humor excuse if `now` falls on a Friday after "
    "15:00, or anytime on Saturday or Sunday -- true with a short "
    "encouraging message otherwise. Handle the exact Friday-15:00 boundary "
    "correctly (15:00:00 itself should already count as blocked)."
)

reviewer = Agent(
    name="codex-reviewer",
    engine=CodexEngine(),
)
writer = Agent(
    name="claude-writer",
    engine=ClaudeCodeEngine(model="sonnet"),
    verify=reviewer,
    max_verify=3,
)

result = writer(TASK)
print(result.text())

We ran this for real while writing this post. Claude's approved function, verbatim — including the excuse bank, because withholding it would be a crime against the bit:

import datetime
import random


def should_i_deploy(now: datetime.datetime) -> tuple[bool, str]:
    weekday = now.weekday()  # Monday=0 ... Sunday=6
    friday_cutoff = datetime.time(15, 0, 0)

    is_friday_afternoon = weekday == 4 and now.time() >= friday_cutoff
    is_weekend = weekday in (5, 6)

    if is_friday_afternoon:
        excuses = [
            "It's after 3pm on a Friday. That's not a deploy window, "
            "that's a hostage situation waiting to happen.",
            "Congratulations, you've entered the Bermuda Triangle of "
            "deploy windows: Friday afternoon. Ships (and services) "
            "that enter do not come back.",
            "Sure, ship it now, and spend your weekend explaining to "
            "your family why your phone won't stop buzzing.",
            "Friday-afternoon deploys are how legends are made. "
            "Cautionary legends. Told at new-hire orientation.",
            "The on-call engineer has requested, via a strongly worded "
            "note taped to their monitor, that you not do this.",
        ]
        return False, random.choice(excuses)

    if is_weekend:
        excuses = [
            "It's the weekend. Even your CI/CD pipeline deserves a "
            "day off, and frankly, so do you.",
            "Deploying on a weekend is like performing surgery in a "
            "dark alley: technically possible, deeply inadvisable.",
            "The servers are resting. The on-call engineer is resting. "
            "Nature is healing. Please do not disturb.",
            "Weekend deploys have a 100% success rate right up until "
            "they don't, and then it's just you, alone, at 2am.",
            "This is a weekend. Weekends were invented specifically "
            "so we wouldn't push to prod today.",
        ]
        return False, random.choice(excuses)

    encouragements = [
        "Green across the board. Ship it like you mean it.",
        "The stars, the CI pipeline, and your coffee are all aligned. Go for it.",
        "No red flags, no weekend curses in effect. Deploy away.",
        "It's a perfectly reasonable time to push. Go be a hero.",
        "All clear! May your rollout be smooth and your logs be boring.",
    ]
    return True, random.choice(encouragements)

And yes, it actually gets the boundary right:

for label, when in [
    ("Friday 2:59pm", datetime.datetime(2026, 8, 21, 14, 59, 0)),
    ("Friday 3:00pm exactly", datetime.datetime(2026, 8, 21, 15, 0, 0)),
    ("Saturday", datetime.datetime(2026, 8, 22, 10, 0, 0)),
    ("Monday", datetime.datetime(2026, 8, 24, 11, 0, 0)),
]:
    print(f"{label}: {should_i_deploy(when)[0]}")

# Friday 2:59pm: True
# Friday 3:00pm exactly: False
# Saturday: False
# Monday: True

Notice what's not in the snippet above: no explicit prompt-passing between writer and reviewer, no state machine for retries, no manual parsing of "approved" vs "rejected" in your own code. verify=reviewer and max_verify=3, both passed when you construct the writer Agent — not arguments to the call itself — are the whole wiring. Under the hood, verify_with_retry() asks the reviewer something close to "Evaluate this output: \<the writer's code>. Original task: \<the task above>. Approved or rejected, with reason?" — you don't have to build that prompt yourself, and there's no separate object to track attempt counts or the final verdict; result is the same kind of value writer(TASK) always returns, whether or not verify= was ever set.

Where this actually breaks

Three things worth knowing before you rely on this pattern.

The retry budget is small and fixed, and LazyBridge won't tell you if it ran out. max_verify defaults to 3. If your writer and reviewer are stuck in a loop — writer keeps making the same mistake, reviewer keeps rejecting it the same way — you get three attempts and then whatever the third one produced, rejected or not, with no flag on the result marking which happened. If you need to know, have your reviewer's approval show up in its own output (a trailing APPROVED/REJECTED line you check yourself, for instance) — don't assume a returned result was ever actually approved.

Subscription plans are bounded too. Routing through ClaudeCodeEngine and CodexEngine means you're not paying per-token on top of a plan, but Pro/Max and ChatGPT/Codex plans still have their own usage limits and terms. A write/review loop with several retries on every call is still meaningfully more usage than a single writer call — just usage against a subscription ceiling instead of a token-metered invoice.

Approval is not correctness. A reviewer agent approving a writer agent's output means two language models agreed on something, not that the something is right. For a task as narrow as a boundary check on a day and a time, that's a reasonably strong signal — there's not much room for the two of them to be wrong in the same way. For anything with more room to be subtly, plausibly wrong, an approved result out of verify_with_retry() is a reason to trust the code a bit more than an unreviewed first draft. It's not a substitute for reading it.

Welcome to LazyBridge

Written by LazyBridge agents, reviewed by a human before publishing

This post came out of a LazyBridge agent pipeline — research, draft, and editorial pass, each a separate agent — and was approved by a human before it went live. More on why in the blog's About note.

You know the moment. You've got a Python function — three lines, one docstring, done — and now you need to hand-write its JSON schema so an LLM can call it. "type": "object", "properties", "required", the whole ceremony, duplicating information that's already sitting right there in your type hints. You copy the shape from the last tool you wrote, rename the fields, and immediately misspell "parameters" as "parameter" for the second time this month.

Then you get the schema working, and the next annoying thing shows up: the tool-calling loop. Call the model. Check stop_reason. If it's tool_use, pull out the blocks, run the right function for each one, thread the results back into the message list with the correct role, call the model again, repeat until it actually answers you. Do this for Anthropic's response shape, then again for OpenAI's slightly different one, then again when you try Gemini and none of it lines up.

None of this is hard. It's just boilerplate you've now written five times across five projects, and it's the same boilerplate whether you're calling a weather API or orchestrating a whole team of sub-agents.

That's the gap LazyBridge is for.

What LazyBridge actually does

LazyBridge is a Python framework for building LLM agents — one Agent class, one tool contract, and providers you can swap without rewriting your integration code. A plain Python function, another Agent, or an MCP server can all go in the same tools=[...] list. LazyBridge turns them into something the model can call, and turns the model's response into something your code can call back.

Schematically, the whole surface looks like this:

from lazybridge import Agent, LLMEngine

agent = Agent(engine=LLMEngine("claude-opus-5"))
result = agent("hello")
print(result.text())

That's genuinely the whole surface to start. No separate "graph" abstraction to learn before you can say hello. (The full, runnable version — with the API-key handling you actually need — is below.)

The mental model: Engine + Tools + State

Every Agent is three things composed together, and once this clicks, the rest of the framework is just filling in the blanks.

  • Engine — the thing that decides what happens next. Most of the time that's LLMEngine, wrapping a model provider. But it's swappable: a Plan engine follows a fixed sequence you define instead of letting the model improvise, a HumanEngine pauses for a person to approve a step, a SupervisorEngine gives you a REPL to drive things interactively.
  • Tools — anything the agent is allowed to invoke. A plain function, another Agent (yes, agents can call agents), or a whole MCP server's worth of remote tools. They all live in the same list and get the same treatment.
  • State — how the agent remembers things: Memory for in-prompt history, Store for a durable blackboard, Session as an event bus you can observe.

Here's why that matters in practice: the same Agent(engine=..., tools=..., ...) shape works for a quick one-off script and for something much more involved. You're mostly just changing what you pass to engine=, not learning a new API.

A real example, first post edition

Here's the thing from earlier — a function becoming a tool, no manual schema — actually wired up to call a provider. It's written so it runs cleanly whether or not you have an API key sitting in your environment, because that's exactly the kind of code you'll want when you're first poking at something new:

import os
from lazybridge import Agent, LLMEngine, Tool


def get_weather(city: str) -> str:
    """Return current temperature and conditions for `city`."""
    return f"{city}: 22°C, sunny"


agent = Agent(
    engine=LLMEngine("claude-opus-5"),
    tools=[Tool.wrap(get_weather, name="get_weather")],
)

if os.environ.get("ANTHROPIC_API_KEY"):
    result = agent("what's the weather in Rome and Paris?")
    print(result.text())
else:
    print("No ANTHROPIC_API_KEY set — skipping the live call.")
    print("Set one and rerun: get_weather becomes a callable tool with zero schema code.")

Run pip install "lazybridge[anthropic]", drop that in a file, and try it both ways — with ANTHROPIC_API_KEY unset, and then with a real key exported. Same code, no crash either way. And when the key is there, LazyBridge reads the type hints and docstring off get_weather and builds the schema for you. That's one whole category of boilerplate from the top of this post, gone.

What's next

This is the first post on this blog, so take it as an introduction rather than a tour — there's a lot of LazyBridge I haven't touched yet: giving an agent real memory across turns, wiring up MCP servers as tool catalogues, having agents call other agents and watching parallel tool calls run automatically, and building deterministic Plan-based pipelines when you want less improvisation and more guarantees. Future posts will go through those one at a time, with the same rule as today: working code, honest about what it does and doesn't do.