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
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 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 | |
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; a modified input replaces the
task sent to the provider. When the stream completes,
acheck_output runs on the accumulated text: a block raises
:class:ValueError after the tokens have been delivered
(streaming is irrevocable) and skips the Store write, so
consumers that buffer the stream can still discard it.
Fallback. When the engine fails before the first token is
emitted and fallback= is configured, the fallback agent's
stream() takes over — same contract as :meth:run, with the
primary's failure threaded into the fallback's context. Once
tokens have been yielded the error propagates instead (the
partial output cannot be retracted).
Timeout. self.timeout is the total deadline for the
stream — the same meaning it has in :meth:run. Stall
detection between chunks is the engine's job
(LLMEngine(stream_idle_timeout=...)).
Not applied in streaming (use :meth:run when you need
them): verify= and output= validation — tokens are
emitted as they arrive, so there is no pre-delivery result to
judge or validate.
Multimodal: pass images= / audio= to attach blocks to
the streamed turn — same coercion semantics as :meth:run.
Source code in lazybridge/agent.py
687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 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 | |
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
866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 | |
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.