Skip to content

Why LazyBridge

Claude Code and Codex run as engines inside the same Python Agent contract. One can write while the other reviews, with shared tools and application state. That contract also includes provider APIs, deterministic Plans and human decisions.

The design ideas below explain how those pieces compose. They are API behavior, not a claim of measured savings or superiority over another framework.

1. Recursive composition

Plan is an engine. A named Agent(engine=Plan(...)) can be a Tool of another Agent. Functions, model-driven workers and reusable pipelines therefore enter the same tools=[...] surface.

from lazybridge import Agent, ClaudeCodeEngine, LLMEngine, Plan, Step, Session, from_step

search = Agent(engine=LLMEngine("gpt-5.4-mini"), name="search")
summarise = Agent(engine=LLMEngine("gemini-2.5-pro"), name="summarise")
writer = Agent(engine=ClaudeCodeEngine(), name="write")

research = Agent(
    engine=Plan(Step("search"), Step("summarise")),
    tools=[search, summarise], name="research",
)
article = Agent(
    engine=Plan(Step("research"), Step("write", context=from_step("research"))),
    tools=[research, writer], name="article", session=Session(),
)
result = article("Explain deployment policy")
assert result.ok
print(result.text())

Nested token and cost metadata roll up through Envelope.metadata.nested_*. The outer run measures its own elapsed time; it does not sum parallel latency. Child errors remain visible through tool results and Session events. Supplying a Session lets nested agents inherit the event/trace context when they have no explicit Session of their own.

Deep dive: Layered composition.

2. Plans validate before execution

Plan construction checks structural wiring. Binding it to an Agent supplies the tool map for tool-name and compatible typed-handoff checks. These errors can be found before invoking a model; they are not a proof of every runtime value or external side effect.

from lazybridge import Agent, Plan, PlanCompileError, Step
from lazybridge.testing import MockAgent

writer = MockAgent("draft", name="write")
plan = Plan(Step("search"), Step("write"))
try:
    Agent(engine=plan, tools=[writer], name="article")
except PlanCompileError as exc:
    print("Missing search tool:", exc)
else:
    raise AssertionError("Expected the missing-tool check")

The compiler checks declared constraints, including:

  • Duplicate step names and missing tool targets once tools are bound.
  • Invalid references to unavailable steps.
  • Incompatible sentinels within a parallel band.
  • Invalid placement of from_parallel_all.
  • Structurally incompatible typed handoffs when types can be derived.
  • Malformed route declarations and invalid reconvergence references.

Runtime parsing and validation still matter for model output, dynamic tool data and branches that cannot be checked statically. Other frameworks also offer structural checks; this describes LazyBridge's particular boundary.

Deep dive: Plan guide.

3. One application Tool contract

Tool.wrap() normalizes application capabilities. Replace a stub function with a named sub-agent or a Plan-backed pipeline while keeping the outer tool contract.

Capability How it enters Execution boundary
Python function Tool.wrap(fn) or tools=[fn] Python Tool
Named Agent agent.as_tool() or tools=[agent] Nested Agent through Tool
Plan-backed Agent tools=[pipeline] Deterministic engine through Tool
MCP catalogue LazyTools provider in tools Connector-expanded Python Tools
Verified tool view agent.as_tool(verify=judge) Tool with bounded review
Provider-native capability LLMEngine.native_tools Provider runtime, separate from Tool.wrap
Coding runtime built-in Engine policy/config Claude built-in or Codex sandbox action

Claude Code exposes Python Tools through its temporary in-process MCP adapter; Codex exposes them as App Server dynamic tools. Both dispatch through Tool.run. Native filesystem and shell permissions do not sandbox a Python tool's side effects.

MCP, connectors, code support and repository jobs belong to LazyTools. See its code bridge for detached work, durable tickets and opposite-engine review; Core does not duplicate those APIs.

Deep dive: Everything is a tool.

4. Quality control at every node

Structured output, runtime validation and reviewer feedback can compose at any level, including a critical sub-agent exposed as a Tool.

from pydantic import BaseModel
from lazybridge import Agent, ClaudeCodeEngine, CodexEngine

class Report(BaseModel):
    title: str
    summary: str
    word_count: int

def validate_report(report: Report) -> Report:
    if report.word_count <= 50:
        raise ValueError("Expand the report beyond 50 words")
    return report

judge = Agent(name="judge", engine=CodexEngine(
    system='Start with "approved" or "rejected: <reason>". Check completeness.',
))
writer = Agent(
    name="write", engine=ClaudeCodeEngine(), output=Report,
    output_validator=validate_report, verify=judge, max_verify=3,
)
result = writer("Write a report on weekday deployment; include an accurate word count.")
assert isinstance(result.payload, Report)

output= parses and validates the declared schema, with repair attempts bounded by max_output_retries. output_validator= must return the validated value or raise on failure; returning a boolean would replace the payload, not reject it.

verify= passes output to a judge. Rejection feedback goes through context, preserving the original task. max_verify=3 means at most three producer attempts, each reviewed. Exhaustion returns the last output without an approval flag: result.ok means execution succeeded. Use deterministic tests and explicit acceptance records before publishing or merging a result.

Choose Codex to review a Claude producer, or Claude to review a Codex producer. Core accepts other judges too; opposite-engine selection is explicit policy.

Deep dive: verify= and Claude Code writes, Codex reviews.

5. Observability is part of the contract

Every call returns an Envelope with payload, error and usage metadata. Session logging is opt-in: supply a Session, or use verbose=True for a console Session. Store holds application records; it is not the Session event log.

from pathlib import Path
from tempfile import TemporaryDirectory
from lazybridge import Agent, ClaudeCodeEngine, JsonFileExporter, Session

with TemporaryDirectory() as directory:
    log = Path(directory) / "events.jsonl"
    session = Session(exporters=[JsonFileExporter(path=str(log))])
    agent = Agent(name="observed", engine=ClaudeCodeEngine(), session=session)
    result = agent("Report readiness for review")
    session.close()
    assert result.ok and log.read_text(encoding="utf-8")

Session exporters can write JSON Lines, structured logs or application callbacks. Session(db=...) persists events in SQLite; batching and queue-full policy belong to Session. Store(db=...) separately persists key/value application records.

The optional OpenTelemetry exporter emits GenAI spans for agents, model calls and tool dispatch, with nested parenting and reported token/cost attributes. Use the otel extra and choose an exporter/backend explicitly. This example uses an in-memory sink, with no network export:

from lazybridge import Agent, Session
from lazybridge.testing import MockAgent

try:
    from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter
except ImportError:
    print("Install lazybridge[otel] for OpenTelemetry export")
else:
    from lazybridge.ext.otel import OTelExporter
    sink = InMemorySpanExporter()
    session = Session(exporters=[OTelExporter(exporter=sink, batch=False)])
    worker = MockAgent("observed", name="worker")
    pipeline = Agent.chain(worker, name="trace-demo", session=session)
    assert pipeline("Run the trace example").ok
    session.close()

Deep dive: Session, exporters and OpenTelemetry.

Continuity and engine swaps

Keep the same tools, Memory, Store and Session references when constructing an Agent with a different engine. Native continuity is a separate choice: persist_session/session_id for Claude, persist_thread/thread_id for Codex, or a session_alias bound by SessionRegistry to engine kind and cwd.

Resumed native history omits repeated Memory priming while retaining explicit context. A runtime session slot is not a durable alias, and a Claude transcript cannot be transferred into a Codex thread simply by swapping engines.

See coding agents and SessionRegistry. The LazyTools quota routing benchmark is in progress.

Next steps

Quickstart → Comparison with other frameworks Layered composition