Agent¶
The single class you build, configure, run, and compose. Every other
primitive in LazyBridge — engines, tools, plans, sessions, memories,
guards — is something you wire into an Agent.
Signature¶
from lazybridge import Agent
agent = Agent(
engine=..., # required: LLMEngine / Plan / HumanEngine / SupervisorEngine / custom
tools=[...], # callables, Tools, Agents, ToolProviders
output=str, # str (default) or a Pydantic model class
memory=None, # Memory instance for conversation continuity
store=None, # Store instance for cross-run / cross-agent state persistence
session=None, # Session for event tracking + observability
name=None, # surface name (used as a tool name when this Agent is composed)
description=None, # human-readable description (LLM-facing when used as a tool)
verbose=False, # print turn-by-turn updates to stdout
model=None, # convenience: tier alias or model ID when engine= is a provider string
sources=(), # static documents prepended to every turn
guard=None, # Guard / GuardChain — input/output filtering
verify=None, # Agent or callable — judge-and-retry loop
max_verify=3, # retries when verify=...
native_tools=None, # list[NativeTool | str] — provider-hosted tools
allow_dangerous_native_tools=False, # security gate: opt-in for CODE_EXECUTION / COMPUTER_USE
output_validator=None, # callable validator over the payload
max_output_retries=2, # retries on output validation failure
timeout=None, # total deadline for the run (seconds)
max_retries=3, # provider transient-error retries
retry_delay=1.0, # base delay between retries (exponential backoff)
fallback=None, # secondary Agent invoked on primary failure
cache=False, # bool or CacheConfig — prompt caching
)
# Calling
result = agent(task) # sync, returns Envelope (canonical)
result = await agent.run(task) # async equivalent
async for chunk in agent.stream(task): ... # streaming form
Use await agent.run(...) inside async runtimes
The sync form agent(task) is safe in scripts, notebooks, and
plain Python REPLs. When called from inside a running event
loop — FastAPI / Starlette / aiohttp request handlers, async
workers, Jupyter cells that are already inside an await — it
detects the loop and dispatches the underlying coroutine onto a
worker thread, then blocks the caller until that thread finishes.
The bridge keeps notebook ergonomics intact, but inside an async
server it stalls the calling task and burns a thread for every
request. Prefer await agent.run(task) directly in async code.
For factory and composition shortcuts (Agent.from_provider,
Agent.chain, Agent.parallel), see Canonical vs sugar.
Synopsis¶
An Agent is the composition Engine + Tools + State:
- The engine decides what happens next.
LLMEngineis the most common — an LLM that picks tools and arguments dynamically. Swap it forPlanto get deterministic orchestration,HumanEngineto gate at a human approval, orSupervisorEnginefor a REPL. - Tools are everything the agent can invoke. Plain Python
functions, other agents,
Plan-backed pipelines, MCP servers, provider-native capabilities — they all live intools=[...]. - State is what persists across or alongside the run.
Memorycarries conversation history;Sessionrecords events; the resultEnvelopecarries the typed payload plus token / cost / latency metadata.
Calling agent(task) runs the engine to completion and returns an
Envelope. The same Agent shape supports a one-shot helper, a
hierarchical multi-agent system, and a checkpointed production
pipeline — only the engine= argument changes.
When to use it¶
- Any single LLM interaction — one-shot call, tool use, or
structured output.
Agentis the only class you need for Basic-tier work; everything else is opt-in via keyword args. - Building blocks for composition. Two agents passed in
tools=[...]of a third agent forms the supervisor pattern. A list of agents passed toPlanbecomes a deterministic pipeline. The sameAgentis the unit at every level. - Tier upgrades. Add
output=for structured output,memory=for conversation continuity,session=for observability,verify=for high-stakes outputs,cache=Truefor prompt caching — without changing the run-loop you've already written.
When NOT to use it¶
- Pure deterministic logic. Don't wrap arithmetic, file parsing,
or HTTP calls in an
Agent— they go intools=[...]or remain plain functions. The agent's job is to decide when to call them. - Streaming-only callsites where you can drop to
LLMEnginedirectly. If you genuinely don't need tools, memory, sessions, guards, or any other agent-level feature,LLMEngine(...).stream(...)works — but in practice this is rare; almost every use grows into needing at least one of those features. - Cases where you want a graph DSL. LazyBridge expresses
composition in plain Python (
Agent,Plan,Step). If you need a separate graph definition language, you're on the wrong framework.
Example¶
from lazybridge import Agent, LLMEngine, Session
from pydantic import BaseModel
# 1) Minimal agent.
agent = Agent(
engine=LLMEngine("claude-haiku-4-5"),
)
result = agent("hello")
print(result.text())
# 2) Tools — auto-schema from type hints + docstring.
def search(query: str) -> str:
"""Search the web for ``query`` and return the top three hits."""
return "..."
researcher = Agent(
engine=LLMEngine("claude-haiku-4-5"),
tools=[search],
name="research",
)
print(researcher("AI news April 2026").text())
# 3) Structured output — read .payload, not .text().
class Summary(BaseModel):
title: str
bullets: list[str]
summariser = Agent(
engine=LLMEngine("claude-haiku-4-5"),
output=Summary,
)
result = summariser("Summarise LazyBridge in three bullets.")
print(result.payload.title)
print(result.payload.bullets)
# 4) Tool-is-Tool composition (Agents wrap Agents).
editor = Agent(
engine=LLMEngine("claude-haiku-4-5"),
tools=[researcher], # researcher.name="research" becomes the tool name
name="editor",
)
print(editor("find papers on bees and write a one-paragraph summary").text())
# 5) output_validator — application invariants on top of Pydantic.
class DateRange(BaseModel):
start_date: str
end_date: str
def chronological(payload: DateRange) -> DateRange:
"""Re-prompt up to max_output_retries times if start > end."""
if payload.start_date > payload.end_date:
raise ValueError(
f"start_date ({payload.start_date}) must precede end_date ({payload.end_date})"
)
return payload
extractor = Agent(
engine=LLMEngine("claude-haiku-4-5"),
output=DateRange,
output_validator=chronological,
max_output_retries=2,
)
# 6) Streaming — same Agent, drop down to .stream() for partial output.
import asyncio
from lazybridge import Agent, LLMEngine
async def stream_brief() -> None:
agent = Agent(engine=LLMEngine("claude-haiku-4-5"))
async for chunk in agent.stream("Outline LazyBridge in five bullets."):
print(chunk, end="", flush=True)
asyncio.run(stream_brief())
# 7) Cache — explicit TTL on Anthropic.
from lazybridge import CacheConfig
cached = Agent(
engine=LLMEngine("claude-haiku-4-5"),
cache=CacheConfig(ttl="1h"), # "5m" (default) or "1h" on Anthropic
)
# 8) Production-shape: timeout + cache + provider fallback + tracing.
fallback_agent = Agent(
engine=LLMEngine("gpt-5"),
tools=[search],
name="fallback",
)
prod = Agent(
engine=LLMEngine("claude-haiku-4-5"),
tools=[search],
timeout=30.0,
cache=True,
fallback=fallback_agent,
session=Session(db="events.sqlite"),
)
prod("draft a one-pager on the LazyBridge audit findings")
Pitfalls¶
output=SomeModel+.text()— calling.text()on a structured envelope returns the JSON dump of the payload, which is rarely what you want. Read.payloadinstead.verify=semantics — the judge must return a verdict starting with"approved"(case-insensitive) to accept. Anything else is treated as rejection plus feedback for the next attempt. Bound the loop withmax_verify=....guard=blocks the engine. A blocked input or output produces an errorEnvelopewithout invoking the engine —result.okisFalse,result.error.typeis"GuardError". Don't expect the agent to "see" the rejected text and self-correct; guards are hard gates.timeout=None(default) leaves the run unbounded. Tool calls inside a runaway agent can block forever. Pick a deadline that matches your SLO.fallback=runs the fallback's full pipeline — its tools, memory, and guards — on the same envelope, with the primary's error threaded intocontext. Configure compatibleoutput=andtools=on both agents, or the fallback may fail differently.output_validator=is a callable applied to the payload after Pydantic validation passes (or directly whenoutput=str). Receives the payload, returns the validated payload (may transform). Raise to reject — the framework re-prompts up tomax_output_retriestimes with the validator's error message threaded back into the prompt. Useful for application-level invariants that aren't expressible in the Pydantic schema (e.g. "thestart_datefield must come beforeend_date").cache=Trueenables prompt caching where the provider supports it (Anthropic explicit, OpenAI / DeepSeek auto). PassCacheConfig(ttl="1h")for the longer Anthropic TTL.- Nested agents inherit the caller's
session=when they have none of their own. This is what gives you transitive cost rollup and a single graph view of the whole tree — pass an explicitsession=Noneon a sub-agent only when you genuinely want it invisible. -
Fleet config via dict spread — the 0.7-era
runtime/resilience/observabilityconfigs were deleted in 0.7.9 (they carried aflat kwarg > config object > defaultprecedence game with a private_UNSETsentinel value — distinct from the Plansentinelsmodule (from_step/from_prev/ …) and an LLM trap). Share kwargs across a fleet via a Python dict::PROD_DEFAULTS = dict(timeout=60, max_retries=5, cache=True, session=sess) Agent(**PROD_DEFAULTS, engine=LLMEngine("model"), name="agent-X")
See also¶
- Tool — how plain Python functions become tools the agent can call.
- Envelope — the typed result every agent returns.
- Native tools — provider-hosted alternatives
via
native_tools=[...]. - Mental model — the Engine + Tools + State decomposition.
- Canonical vs sugar — every factory and shortcut, with its canonical equivalent.