Agent + Envelope¶
The universal wrapper and the typed result object every run produces.
For narrative usage see Guides → Basic → Agent
and Guides → Basic → Envelope. If
you're migrating from a 0.7-era surface (Agent.from_* factories,
config dataclasses, _ParallelAgent), see the
0.7 → 0.7.9 migration guide.
lazybridge.Agent ¶
Agent(engine: str | Any | None = None, tools: list[Tool | Callable | Agent] | None = None, output: type = str, memory: Any | None = None, store: Any | None = None, sources: list[Any] | None = None, guard: Any | None = None, verify: Agent | Callable[[str], Any] | None = None, max_verify: int = 3, name: str | None = None, description: str | None = None, session: Any | None = None, verbose: bool = False, model: str | None = None, native_tools: list[Any] | None = None, allow_dangerous_native_tools: bool = False, output_validator: Callable[[Any], Any] | None = None, max_output_retries: int = 2, timeout: float | None = None, max_retries: int = 3, retry_delay: float = 1.0, fallback: Agent | None = None, cache: bool | Any = False)
Universal agent — Agent(engine, tools, state).
Every Agent has the same shape, regardless of what it does:
engine— the brain: decides what happens (LLM, Plan, Human, …)tools— the capabilities: what the agent can invoke- state —
memory,session,guard,verify,output
Canonical composition — give each sub-agent an explicit name=
and pass it directly in tools=[...]::
from lazybridge import Agent, LLMEngine, Plan, Step, tool, from_prev, from_step
search = tool(search_web, name="search", description="Search the web.")
researcher = Agent(
name="research",
engine=LLMEngine("claude-haiku-4-5", system="You are a research expert."),
tools=[search],
)
writer = Agent(
name="write",
engine=LLMEngine("gpt-5.4-mini", system="You are a concise writer."),
)
# Deterministic orchestrator — Plan engine
pipeline = Agent(
name="pipeline",
engine=Plan(
Step("research"),
Step("write", task=from_prev, context=from_step("research")),
),
tools=[researcher, writer], # agents passed directly
session=sess,
)
# Dynamic orchestrator — LLM engine
orchestrator = Agent(
name="orchestrator",
engine=LLMEngine("claude-opus-4-8"),
tools=[researcher, writer],
session=sess,
)
The engine is the only thing that changes. Everything else — tools, memory, session, guard, output — is the same surface on every Agent.
String shortcut — Agent("claude-opus-4-8") is sugar for
Agent(engine=LLMEngine("claude-opus-4-8")). Use the explicit
form when you need to configure the engine (system=, max_turns=,
thinking=, etc.).
The name chain — Agent(name=...) is the authoritative key that
connects every part of the system::
Agent(name="research") → tool map key when passed in tools=[researcher]
Step("research") → looks up "research" in tool map ✓
from_step("research") → reads output of step "research" (in-Plan) ✓
from_agent("research") → reads stored output of "research" (cross-run) ✓
from_memory("research") → reads live memory of "research" ✓
Advanced alias / backward compat — .as_tool("alias") remains
available when you need a different name than the agent's own::
tools=[researcher.as_tool("deep_research")]
Factory methods that build real structure (not pure aliases) live on the class:
Agent.chain(a, b)— sequential: builds aPlanof oneStepper agent.Agent.parallel(*agents)— scripted fan-out: returns aParallelAgentwhose__call__yields oneEnvelope(labelled-text join across every branch, with transitive cost rollup). For typed per-branchlist[Envelope]access callparallel.run_branches(task)(async).Agent.from_provider(provider, tier="medium")— resolves a tier alias (cheap/medium/top/ …) to that provider's current model.
Extension engines live in :mod:lazybridge.ext to respect the
core/ext import boundary::
from lazybridge.ext.hil import HumanEngine, SupervisorEngine
Agent(engine=HumanEngine(timeout=60), tools=[approve])
Agent(engine=SupervisorEngine(tools=[...]))
Source code in lazybridge/agent.py
107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 | |
stream
async
¶
stream(task: str | Envelope, *, images: list[Any] | None = None, audio: Any | None = None) -> AsyncGenerator[str, None]
Stream LLM tokens across the full tool-calling loop.
Guard enforcement. self.guard is checked via
acheck_input before the first token is emitted. A blocked
input raises :class:ValueError immediately; a modified input
(GuardAction.modify) replaces the task in the envelope sent
to the provider. This is identical to the guard contract in
:meth:run — streaming mode does not bypass the guard.
Honours self.timeout between chunks so a stalled provider
can't hang the caller. Each __anext__ is wrapped in
asyncio.wait_for (per-chunk, not whole-stream) so short
chunks don't consume the deadline budget.
Multimodal: pass images= / audio= to attach blocks to
the streamed turn — same coercion semantics as :meth:run.
Source code in lazybridge/agent.py
602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 | |
as_tool ¶
as_tool(name: str | None = None, description: str | None = None, *, verify: Agent | Callable[[str], Any] | None = None, max_verify: int = 3) -> Tool
Wrap this agent as a :class:Tool (advanced / compatibility API).
The canonical way to use a sub-agent is to give it an explicit
name= and pass it directly in tools=[...]::
# Canonical
researcher = Agent(name="research", engine=LLMEngine(...))
orchestrator = Agent(..., tools=[researcher])
.as_tool() remains available for local aliases and
backward compatibility::
# Advanced alias — use a different name than the agent's own
tools=[researcher.as_tool("deep_research")]
# Backward compat — existing code that already calls as_tool()
tools=[researcher.as_tool("research")]
The tool schema is (task: str) -> Envelope.
Verify (Option B) — wrap the call in a judge/retry loop so every invocation is vetted before returning::
judge = Agent(engine=LLMEngine(
"claude-opus-4-7",
system="Reply 'approved' or 'rejected: <reason>'.",
))
synth = Agent(name="synth", engine=LLMEngine(...))
orchestrator = Agent(
...,
tools=[synth.as_tool("synthesize", verify=judge, max_verify=2)],
)
verify can be either an :class:Agent (its run method is
called with the output) or a plain callable taking the output text
and returning a verdict string / bool. On rejection, the judge's
feedback is injected into the next attempt's task.
Source code in lazybridge/agent.py
727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 | |
definition ¶
derive ¶
Return a NEW Agent: a clone of self plus extra tools and/or overrides.
Does not mutate self. The returned Agent runs full constructor validation, so the build-time guarantee (construction implies a valid graph) is preserved unchanged.
Use when an existing agent needs context-specific capabilities — a pool handle, a one-shot tool, a different name in a sub-context — without rebuilding it field by field.
debater = base.derive(tools=[pool.as_tool(), conclude, vote_tool])
renamed = base.derive(name="alias")
tools= are APPENDED to the agent's existing tools. Any other keyword
in overrides replaces the corresponding constructor argument.
Source code in lazybridge/agent.py
from_provider
classmethod
¶
Construct an Agent for provider using its tier alias for model selection.
Tiers (super_cheap / cheap / medium / expensive /
top) resolve to each provider's current lineup, so preview and
date-pinned model names stay in one place::
Agent.from_provider("anthropic", tier="top")
Agent.from_provider("openai", tier="cheap", tools=[search])
Source code in lazybridge/agent.py
chain
classmethod
¶
Run agents sequentially: output of each becomes input to the next.
Source code in lazybridge/agent.py
parallel
classmethod
¶
parallel(*agents: Agent, concurrency_limit: int | None = None, step_timeout: float | None = None, **kwargs: Any) -> ParallelAgent
Deterministic fan-out: run agents concurrently on the same task.
Returns a :class:ParallelAgent whose __call__ produces a
single :class:Envelope — labelled-text join of every branch's
output, with transitive cost rollup. For typed access to per-branch
envelopes call ParallelAgent.run_branches(task) (async).
Use this when you know you want N things to happen in
parallel. If you want the LLM to decide whether to call agents
in parallel (and which, and how), don't use this — pass them as
tools=[...] on a regular Agent instead; the engine emits
parallel tool calls automatically when the model requests them.
Source code in lazybridge/agent.py
lazybridge.ParallelAgent ¶
ParallelAgent(agents: list[Agent], *, concurrency_limit: int | None = None, step_timeout: float | None = None, name: str = 'parallel', description: str | None = None, session: Any | None = None)
Deterministic fan-out over N agents — the shape behind :meth:Agent.parallel.
Pre-scripted parallel runner. Every input agent receives the same
task; the N branch results are folded into a single :class:Envelope
via labelled-text join — same shape as :class:Plan's
from_parallel_all aggregator. Cost roll-up is transitive.
The first non-None branch error propagates as the wrapper's
error so downstream consumers can short-circuit.
Prefer :class:Agent with tools=[...] when you want the engine
(LLM, Supervisor, Plan) to decide dynamically which tools to invoke
and when — parallel execution is automatic on that path.
Per-branch typed access: call :meth:run_branches (async) when you
need list[Envelope] rather than the joined wrapper.
Source code in lazybridge/agent.py
run_branches
async
¶
Async per-branch entry point — returns one Envelope per
input agent in input order. Use this when you need typed
access to individual branch results; for the framework-uniform
single-Envelope view, use :meth:run or __call__.
Source code in lazybridge/agent.py
run
async
¶
Run every branch and return one folded :class:Envelope.
The wrapper's payload is the labelled-text join of every
branch's .text(); metadata.nested_* rolls every branch's
cost up so the outer envelope reports total spend. The first
non-None branch error propagates as the wrapper's error.
For typed per-branch access, call :meth:run_branches.
Source code in lazybridge/agent.py
as_tool ¶
Expose the fan-out runner as a single :class:Tool.
Just delegates to :meth:run — same labelled-text Envelope as
every direct caller sees, so a ParallelAgent passed in
tools=[...] produces output identical to a hand-call.
Source code in lazybridge/agent.py
lazybridge.Envelope ¶
Bases: BaseModel, Generic[T]
Typed envelope carrying a payload of type T.
Envelope[str] → payload is a string. Envelope[MyModel] →
payload is an instance of MyModel. Envelope (no parameter)
defaults to T = Any for maximum flexibility.
Multimodal attachments (images= / audio=) ride alongside
task and reach the LLMEngine's user-message builder verbatim.
Steps in a Plan see them only on step 0; downstream steps receive
upstream output (text), not the original attachments.
__str__ ¶
Stringification falls through to :meth:text.
Needed because tools that return an Envelope (agent-as-tool)
cross back into content blocks expected by the LLM API, which
serialise the value via str(...). Without this, any such
tool would produce "task=… context=…" garbage instead of
the agent's actual answer.
Source code in lazybridge/envelope.py
Multimodal content blocks¶
For mixed-modality inputs (text + image + audio), pass images= and
audio= kwargs on agent(...), await agent.run(...), or
async for chunk in agent.stream(...). Bare URL strings, Path
objects, raw bytes, and dict payloads are coerced into the typed
blocks below automatically — use these constructors directly only when
you need to override the auto-detected MIME type. Narrative coverage
lives in Guides → Mid → Multimodal.
lazybridge.ImageContent
dataclass
¶
ImageContent(url: str | None = None, base64_data: str | None = None, media_type: str = 'image/jpeg', type: ContentType = ContentType.IMAGE)
from_data_uri
classmethod
¶
Parse data:image/png;base64,<...> style URIs.
Source code in lazybridge/core/types.py
lazybridge.AudioContent
dataclass
¶
AudioContent(url: str | None = None, base64_data: str | None = None, media_type: str = 'audio/wav', type: ContentType = ContentType.AUDIO)
Audio attachment for multimodal LLM input.
Provider support varies by model — see
:meth:BaseProvider.supports_audio. Anthropic / OpenAI accept
base64 only; Google Gemini accepts both URL and base64.
from_data_uri
classmethod
¶
Parse data:audio/flac;base64,<...> style URIs.