Changelog¶
All notable changes to this project will be documented in this file. Format follows Keep a Changelog. Versioning follows Semantic Versioning.
[Unreleased]¶
Added¶
- New extra
lazybridge[docparse](griffe) for multi-line-aware docstring parameter parsing, same library used by PydanticAI and the OpenAI Agents SDK. Auto-detects Google/NumPy/Sphinx style. Included in thetestandallextras so CI exercises it, not just its fallback.
Fixed¶
- A parameter description wrapped across multiple physical lines is no
longer silently truncated to its first line.
_parse_docstring_paramsnow triesgriffefirst (whenlazybridge[docparse]is installed), which correctly joins wrapped continuation lines (collapsed to single spaces, consistent with the first-paragraph tool description below); the previous regex-only parser — kept as the fallback when griffe isn't installed — only ever captured a single physical line per parameter and dropped the rest with no warning. Newtests/unit/test_docstring_multiline_params.pycovers Google/NumPy/ Sphinx wrapped params, the griffe-unavailable fallback path, and a regression guard for short/ambiguous docstrings where style auto-detection itself returns nothing (falls back to the same:param-marker heuristic the regex parser already used, not blindly to Google).
Changed¶
- Auto-derived tool description now uses the docstring's first paragraph,
not just its first physical line (
ToolSchemaBuilder.build_artifact, only whenTool.wrap/.build()receives no explicitdescription=). Wrapped lines within that paragraph are collapsed to single spaces. A well-formed Google-style docstring (one-line summary, blank line, thenArgs:/extended prose) is unaffected — this only changes tools whose summary itself wraps across multiple physical lines before the blank line. Text after the first blank line (extended description,Args:, internal notes) was never included and still isn't. The old comment claiming this protected the compile-artifact fingerprint was incorrect:func_source_hashalready hashes the full function source, docstring included, so any docstring edit already invalidates the fingerprint regardless of how much of it becomes the description.
[1.0.2] — 2026-07-13¶
Found by a targeted cross-process stress-testing pass over the checkpoint /
resume machinery (Plan crash-resume, ReplanEngine failure-recovery).
Fixed¶
Plancrash-resume now feeds the first resumed step the previous step's output, not the plan's start input._run_implseededprev_envfrom the start envelope, so a resumed step whose implicitfrom_previnput came from a step that completed before the crash silently received the original task instead of the upstream result — a plainPlan(Step(a), Step(b), …)chain with nowrites=lost the link across the checkpoint boundary (no error, wrong data). It now reconstructsprev_envfrom the restored step-result history. Steps that pass data viawrites=/kv, or that referencefrom_start/from_step/from_parallel, were never affected. This complements the 1.0.1 fix (which stopped resume from re-invoking completed steps but did not restore the chain value). Regression test added:test_e2e_resume_preserves_from_prev_chain_value.- Resuming a step that failed after it already succeeded no longer
reprocesses its own output. Edge of the fix above:
_routing()(or a durable-write) can raise after a step is appended to the checkpoint'shistoryand beforenext_stepadvances, so the resultingfailedcheckpoint'snext_stepstill points at that just-completed step. Reconstructingprev_envunconditionally fromhistory[-1]there fed the retried step its own recorded output instead of the upstream step's. Now only steps back tohistory[-2]when resuming afailed/cancelledcheckpoint whosenext_stepequals the last history entry (the retry-of-a-succeeded-step signature); a cleanrunningself-loop is unaffected. Regression test added:test_e2e_resume_after_post_success_routing_failure_uses_upstream. - Windows-only test failures resolved. e2e /
run_manytests now close theirStore(viacontextlib.closing) so the temp SQLite file unlocks beforeTemporaryDirectorycleanup, andtest_ext_core_boundaryreads package sources as UTF-8. Five tests that failed only on Windows (PermissionError [WinError 32]/UnicodeDecodeErrorunder cp1252) now pass; CI on Linux was already green.
Testing¶
ReplanEnginefailure→resume→recover contract pinned. Failure injection (a transient worker, a parallel-band branch, or the planner itself failing) confirmed the engine already recovers correctly viaresume=True— re-running only the failed round, never a completed one — and that an errorEnvelopefrom a worker surfaces as an error rather than a phantom-empty success. No behaviour change; added as regression coverage since no existing test exercised this path.
[1.0.1] — 2026-07-06 — first Stable release¶
Version starts at 1.0.1, not 1.0.0: an earlier 1.0.0 shipped in
April 2026 under the old LazyAgent/LazyTool namespace and was
rolled back (see Migrating from 1.0.0).
That version number is retired for good — this release starts clean at
1.0.1 rather than reusing it.
lazybridge.__stability__ moves "beta" → "stable"; PyPI classifier
moves Development Status :: 4 - Beta → Development Status :: 5 -
Production/Stable. The core public API contract (Agent, Plan,
Tool, Envelope, Guardrails, Checkpoint/resume) will not break
without a major version bump going forward.
Changed¶
- Guardrails and Checkpoint/resume promoted Alpha → Stable in the
maturity table (
docs/index.md), backed by a live adversarial/load stress-testing pass:LLMGuardresisted a deliberate tag-injection smuggling attempt,GuardChaincorrectly threads modifications and blocks across chained guards, andPlancheckpoint/resume correctly resumed after a forced step failure without re-invoking (re-billing) the already-completed step. Native tools,HumanEngine/SupervisorEngine, Evals, and the Visualizer remain Alpha/Experimental — not exercised by this pass, unchanged. The two Planned items (provider fallback chains, automatic PII redaction beyond credential shapes) remain unimplemented; still explicitly listed rather than quietly dropped.
Fixed¶
asyncio.iscoroutinefunctionreplaced withinspect.iscoroutinefunctioninlazybridge/tools.py,lazybridge/guardrails.py, andlazybridge/engines/plan/_plan.py. Theasyncioversion is deprecated and slated for removal in Python 3.16; behavior is identical. Found via DeprecationWarning surfaced by the live stress-test suite.Agent(verify=...)no longer recurses infinitely. Found live by the pre-v1 stress notebook:_run_body's verify branch calledverify_with_retry, which called back into the fullagent.run()— which re-entered the same verify branch, recursing untilRecursionErroron anyAgent(verify=...)invocation. The suite never caught it because the judge tests driveverify_with_retrywith mock agents that don't re-enter.verify_with_retrynow accepts arun=override andAgent._run_bodypasses its engine-only runner (_run_engine), so each verify attempt re-executes the engine (plus structured-output validation) without re-entering the guard/verify pipeline. Regression tests cover the callable-judge, Agent-judge, and retry-with-feedback paths.- Verify retries keep the original attachments and payload. The
rebuilt post-rejection envelope carried only
task+ feedbackcontext, silently dropping the original env'simages,audio, andpayload— every retry ran without the input the first attempt had (Codex review finding on the recursion-fix PR).
[0.10.0] — 2026-07-02 — v1 stabilization bridge¶
The bridge release before 1.0: every finding from the v1 deep audit of
the core is fixed here, the plan runtime is decomposed into focused
modules, and the public API gets its final pre-1.0 cleanup. Package
stability moves alpha → beta (Development Status :: 4). The plan:
this release settles across the dependent Lazy* projects, then 1.0.0 is
tagged from it without further changes.
Migration summary (breaking / deprecated):
Agent(output=Model)that exhaustsmax_output_retriesnow returnsok=Falsewitherror.type == "OutputValidationError"instead ofok=Truewith the raw string. Check the error type; the raw payload is preserved on the envelope.Agent.stream(timeout=)is now a total-stream deadline (was per-chunk); the stream also enforces the output guard on completion and fails over tofallback=when the engine dies before the first token.lazybridge.Task→lazybridge.ReplanTask(deprecated alias warns, removed in 1.0).CacheConfig→ import fromlazybridge.core.types.PROVIDER_ALIASES→ callLLMEngine.provider_aliases().- Routing (
routes=/routes_by=) into aparallel=Truestep is now aPlanCompileError(it silently lost the rejoin jump at runtime). Plan.to_dict()is now v2 (recordsStep.output/Step.inputby name); pass the types in thefrom_dictregistry ({"type:<Name>": <class>}). v1 payloads still load.
Added¶
- No-extras test environment is green. The suite now passes with no
provider SDK installed:
tests/conftest.pyinstalls a MagicMockopenaistub before any lazybridge import (the per-filesys.modulesstubs came too late once the provider module was imported, leaving_openai = Nonebound forever — 15 failures), andtest_store_encryption.py's skip guard no longer crashes collection on hosts wherecryptography's Rust extension panics (the pyo3PanicExceptionis matched by name; the old import-then-catch bound()into the except clause and raisedTypeError).
Fixed¶
- Memory summaries now accumulate across compressions. Repeated compression overwrote the previous summary — the summarizer never saw it, so the second compression permanently discarded everything the first had captured (the oldest context), silently. Both the LLM path and the keyword-extraction fallback now fold the prior summary into the new one.
- Unannotated tool params survive strict mode. In signature mode an
unannotated parameter produced an empty
{}subschema (bypassing_annotation_to_schema's documented{"type": "string"}fallback); strict-mode validators on OpenAI/Gemini reject or drop{}, making the parameter vanish from the tool signature. $defsname collisions fail loud on flatten._flatten_refsmerged same-named definitions last-write-wins, silently inlining the wrong shape when two distinct models shared a class name. Conflicting shapes now raiseValueErrorwith a rename hint (identical duplicates still merge).- LLMGuard async timeout enforced once.
_ajudge's sync-callable fallback routed through_judge, which enforcestimeoutagain on its own daemon thread — double enforcement, plus a leaked daemon thread per call whenever the outer deadline fired first. The async path now calls a single untimed judging round-trip under the outerasyncio.wait_for. DeduplicateGuardis silent by default.verbosedefaulted toTrueand wrote to stdout viaprint()from library code; it now defaults toFalseand routes throughlogging(INFO when verbose, DEBUG otherwise). The module also gains behavioral test coverage (block splitting, near-dup prefixes, short-block preservation).__version__source-tree fallback re-aligned withpyproject.toml(was stale at 0.9.0), with a test guarding the sync.- Cancelled Plan/Replan runs no longer poison the checkpoint key.
A run unwound by cancellation (e.g. a consumer breaking out of
plan.stream()early), byconclude(), or by an unexpected exception escaped past the per-step checkpointing and left the key stuck inclaimed/runningunder a deadrun_uid— every subsequenton_concurrent="fail"run raisedConcurrentPlanRunErroruntil the key was manually cleared. Both engines now write a best-effort terminal checkpoint on non-local exits (cancelledon cancellation,doneon conclude — with the conclude answer cached for Replan — andfailedon unexpected exceptions), and_claim_checkpointtreatscancelledas claimable by fresh runs and adoptable byresume=True(which continues from the recordednext_step/ round). - Plan serialization carries
Step.output/Step.input(to_dict v2).to_dict()silently dropped both, sofrom_dict()rebuilt every step withoutput=str: structured steps degraded to raw strings and anyroutes_by=plan failed recompilation (PlanCompileError) after a round-trip. Types are now recorded by name and rebound via thefrom_dictregistry ("type:<Name>"or bare"<Name>"key) with a loudKeyErrorwhen missing. v1 payloads still load (missing keys default tostr/Any). - Routing into a parallel band is now a compile error.
routes=/routes_by=targeting aparallel=Truestep compiled cleanly but the band dispatcher advances linearly and never consults theafter_branchesrejoin state — the jump was silently lost and a stale entry leaked.PlanCompilernow rejects it with a fix hint (wrap the parallel work in anAgent(engine=Plan(...))branch step). - Per-step checkpoint cost no longer quadratic.
_save_checkpointre-serialized the entire growing history (model_dumpof every envelope) on every step. The serialized history is now maintained incrementally alongside the in-memory one. EventLog.flush()afterclose()no longer stalls. Pushing a flush sentinel to a queue whose writer thread has exited blocked for the full timeout;flush()is now a no-op once closed or when the writer thread is not alive.EncryptedStoreAdaptercontext manager + keyed bulk-read errors. The adapter now implements__enter__/__exit__(parity with the baseStore), andread_all()/items()name the offending key when they hit a plaintext row in a mixed store.- Shared-engine event misattribution. An engine is a shareable object,
but
Agent.__init__stampedengine._agent_name = self.name— so with two Agents on one engine, every event and usage row was attributed to whichever agent was constructed last, deterministically. The identity is now bound per-invocation via a context variable (lazybridge.engines.base.bind_agent_name/resolve_agent_name):Agentbinds its name around eachengine.run()/engine.stream()call and all engines (LLM, Plan, Replan, Supervisor, Human) resolve the context-bound name first. The_agent_nameattribute is kept as a fallback for code that drives an engine directly. - Structured output on the streaming path.
LLMEngine._stream_turnrebuilt theCompletionResponsefrom stream chunks without ever readingchunk.parsed/chunk.validation_error/chunk.validated, so any streamed run withoutput=Modelsilently degraded to a raw string (and burned the output-validation retries). The reconstructed response now carries all three fields through. Agent.stream()pipeline parity withrun(). Streaming applied only the input guard. Now: the output guard runs on the accumulated text when the stream completes (a block raisesValueErrorand skips the Store write — tokens already delivered cannot be retracted, but buffering consumers can discard); the fallback agent takes over when the engine fails before the first token (after tokens, the error propagates); andtimeout=is now a total-stream deadline, the same meaning it has inrun()(it was per-chunk, i.e. effectively unbounded — stall detection between chunks remainsLLMEngine(stream_idle_timeout=)).verify=andoutput=validation remain run()-only and are documented as such.- Executor retry classification. The last-resort string scan in
_is_retryablecould retry permanent client errors whose message merely contained "timeout" / "connection" (e.g. a 400invalid 'timeout' parameter). A structured 4xx status (other than 408/429) now short-circuits to non-retryable before the string scan. - Memory records the answer actually returned. When a
structured-output correction retry produced the accepted answer, memory
kept the first (rejected) draft — history diverged from the returned
Envelope.
Agent._validate_and_retrynow amends the last turn via the newMemory.amend_last(assistant). - Cross-session sub-agent pinning is now visible. A sub-agent that
inherited its session from one orchestrator and is then passed to a
second orchestrator with a different session stays pinned to the first
(unchanged — we never steal a session), but the second construction now
emits a
UserWarningexplaining where the child's events flow and how to choose explicitly.
Refactoring (no behaviour change)¶
_plan.pysplit into focused submodules. The 1,700-line runtime monolith is now_plan.py(scheduler/orchestration, ~1,180 lines) plus_checkpoint.py(the CAS checkpoint state machine, asCheckpointMixin),_resolve.py(sentinel resolution + band aggregation,ResolveMixin), and_fanout.py(run_many/arun_many,FanoutMixin).Planinherits all three, so every method keeps its original name and signature. The 170-line inline parallel-band block in_run_implis now the_run_parallel_bandmethod with an explicit state contract.- One provider registry. The provider-name → class map lived in two
hand-maintained copies (
Executor._resolve_providerandLLMEngine._provider_class) that had already drifted on thelitellmspecial case. Both now resolve throughlazybridge.core.providers._registry.provider_class(lazy per-provider import preserved). Executorretry loops deduplicated into a shared_next_retry_delay(classification + backoff + warning), keeping sync and async semantics in lock-step._parse_data_urishared byImageContent.from_data_uriandAudioContent.from_data_uri(byte-identical copies collapsed)._safe_register_agent/_safe_register_tool_edgecollapsed onto a single warn-on-failure_safe_graph_callhelper.
Documentation¶
- ReplanEngine checkpoint granularity made explicit. Checkpoints are
per-ROUND, not per-task: a crash mid-round re-executes the entire round
on
resume=True(planner re-asked, every task re-dispatched), so tasks with external side effects must be idempotent. This was always the behaviour; it is now documented on the engine.
Changed¶
- v1 API pass — three top-level names deprecated (removal in 1.0).
Task→ renamedReplanTask(the bare name was too generic for a top-level export and collided with user code).lazybridge.engines.replan.Taskremains a plain alias;lazybridge.Taskstill resolves but emits aDeprecationWarning.CacheConfig→ import fromlazybridge.core.types(it is engine configuration, not primary API). Top-level access warns.PROVIDER_ALIASES→ callLLMEngine.provider_aliases(). The constant was an import-time snapshot that silently diverged from the live registry afterregister_provider_alias. Top-level access warns and now returns a fresh snapshot. All three are out of__all__(star-imports no longer pick them up); the public-API snapshot test, SKILL.md, and reference docs are updated.StoreEntry.written_atdocumented as informational metadata. The Store has no TTL/expiry mechanism and never consultswritten_at; the docstring now says so explicitly (agent_idcarries provenance stamps such as Plan'splan-run:<run_uid>).- BREAKING — exhausted output validation is now an error, not a silent
success.
Agent(output=Model)used to returnok=Truewith the raw, unvalidated string payload aftermax_output_retriesfailed correction attempts — callers could not distinguish "validated" from "gave up", andresult.payload.fieldblew up downstream. The final envelope now carrieserror.type == "OutputValidationError"(ok=False,retryable=False) with the raw payload preserved on the envelope for inspection. Migration: code that relied on receiving the unvalidated string onok=Trueshould check forerror.type == "OutputValidationError"and readresult.payload(still the raw model output) from the error envelope. - Deduplicated the encrypted-Store CAS equality check.
EncryptedStoreAdapter.compare_and_swapcompared the decrypted plaintext againstexpectedthrough a private_plain_eqthat was a byte-for-byte copy oflazybridge.store._json_eq(JSON-shape equality via_to_jsonable, so a Pydantic model compares equal to the dict it round-trips to). The adapter now calls the shared_json_eqdirectly, keeping its CAS rule in lock-step withStore.compare_and_swapand removing the copy. Behaviour is unchanged. - Unified the synchronous→async bridge. The logic that runs a
coroutine to completion from synchronous code (detect the event-loop
state, then run on a fresh loop / in-loop under nest_asyncio / on a
worker thread) lived in six near-identical, subtly-divergent copies:
Agent.__call__,ParallelAgent.__call__,Tool.run_sync,Memory._drive_to_completion,MockAgent.__call__, andPlan.run_many. Only theAgentcopy handled nest_asyncio (Jupyter/Spyder) and suppressed httpx/anyio "Event loop is closed" GC noise; onlyMemoryhonoured a timeout; the others skipped the in-loop nest_asyncio branch — so the same call took a worker-thread path in a notebook whileAgent.__call__ran in-loop, a source of intermittent, path-dependent behaviour. All six now delegate to a single private helper,lazybridge._asyncbridge.run_coroutine_blocking, so every synchronous entry point crosses the boundary with identical semantics. Observable effects:Tool.run_sync,MockAgent.__call__, andPlan.run_manynow take the in-loop path under nest_asyncio and suppress loop-closed cleanup noise;Memory's summariser path now also propagates the caller'scontextvars(OTel spans / request-ids / structured-logging context) into the worker loop.Memory'stimeoutcontract is unchanged. The helper takes a coroutine factory rather than a live coroutine, so a failure anywhere in dispatch can never strand a "coroutine was never awaited" object, and itstimeoutis applied withasyncio.wait_forinside the executing loop (the coroutine is actually cancelled on expiry, not left running detached).
[0.9.2] — 2026-06-12¶
Added¶
- True token streaming for
PlanandReplanEngine.Agent(engine=plan).stream(...)previously awaited the entire plan and yielded the final text once; it now streams tokens live from each sequential step's LLM engine via an ambient token sink (lazybridge/core/streaming.py) thatLLMEngine.run()adopts. Parallel bands are suppressed (no token interleaving), nested agents-as-tools stay silent (theLLMEngine.stream()contract), plans with no streaming-capable step fall back to yielding the final text once, and closing the stream early cancels the in-flight run. NewPlan(stream_buffer=N)bounds the token queue exactly likeLLMEngine(stream_buffer=N). TheReplanEngineplanner's structuredPlanRoundoutput is loop control and is kept out of the stream. See Guides → Full → Plan → Streaming. Post-review hardening: the closing sentinel is skipped on cancellation (in both the ambient-sink runner andLLMEngine.stream's loop), so a consumer that disconnects while the bounded queue is full can no longer deadlockaclose()onsink.put(None). - Checkpoint-epoch stamping +
Plan.store_write_is_current(). Every durableStep(writes=...)Store write (sequential, parallel band, and resume replay) now carriesagent_id="plan-run:<run_uid>", matching therun_uidpersisted in the checkpoint snapshot. Sidecar consumers reading the Store out-of-band can callPlan.store_write_is_current(store, checkpoint_key=..., key=...)to detect the documented crash-window staleness mechanically instead of diffing against the checkpointkvby hand. - Example-rot guard.
tests/unit/test_examples_integrity.pychecks that every file underexamples/compiles and that everylazybridgeimport in it resolves against the installed package, so a public API rename can no longer silently break the examples. ReplanEngine— guardian of the dynamic replan loop. The adaptive counterpart toPlanfor pipelines whose shape is decided at runtime by a planner agent. The planner is a tool in the parentAgent'stool_map(built withoutput=PlanRound, located byplanner_name); it is called every round and the tasks it emits are dispatched viatool.run(**task.kwargs)— agents, plain functions, and pool routes alike, with no special-casing. Tasks flaggedparallel=Truerun concurrently viaasyncio.gather. Passstore=+checkpoint_key=to persist round state after every round andresume=Trueto continue from the last checkpoint; same compare-and-swap single-writer semantics asPlan(ConcurrentPlanRunErroron a contended key).max_rounds(default20) caps the loop; adone=Trueround must carry afinal_answer.PlanRoundandTask(lazybridge.engines.replan, re-exported fromlazybridge) — the planner's structured output schema.PlanRoundcarriesreasoning, a list ofTask, adoneflag, and the terminalfinal_answer;Taskis one tool call (tool+kwargs+parallel). Added to the public API snapshot. New guide:docs/guides/full/replan-engine.md; reference entries indocs/reference/engines.md.
Fixed¶
ext.plannersDAG builder —add_stepnow exposesfrom_parallel_all. The incremental builder tool'stask_kindannotation wasLiteral["literal", "from_prev", "from_step", "from_parallel"], omittingfrom_parallel_alleven thoughStepSpec, the step validator,_resolve_task, andPLANNER_GUIDANCEall already supported it — so the value the guidance steers the planner toward was not selectable through the generated tool schema. Added"from_parallel_all"to theadd_stepLiteral and documented it in the tool docstring. Additive; no existing behaviour changes.
[0.9.1] — 2026-05-28 — Store.items(prefix=) range scan¶
Added¶
Store.items(prefix=)— returns(key, value)pairs restricted to keys starting withprefixvia a single indexed B-tree range scan (WHERE key >= ? AND key < ?). Sub-linear in total keyspace size; O(M) in the number of matching keys. The in-memory path filters under the store lock usingstr.startswith. Passprefix=None(default) orprefix=""to iterate the full store. TheEncryptedStoreAdapterdelegates to the inner store and decrypts each returned value._prefix_upper_bound(prefix)— private helper that computes the exclusive upper bound for the B-tree scan. Handles the U+10FFFF edge case by falling back to a Python-levelstartswithfilter.
Compatibility¶
Additive — no existing behaviour changed. LazyPulse 0.2.0 uses this method
to replace its O(N+1) _scan_records implementation.
[0.9.0] — 2026-05-24 — lazytoolkit extraction (Phase 3: shims removed)¶
Removed (breaking)¶
The lazy deprecation shims left behind by the 0.8 extraction are gone.
Import from lazytools directly.
lazybridge.ext.mcp→ uselazytools.connectors.mcp(pip install 'lazytoolkit[mcp]').lazybridge.ext.gateway→ uselazytools.connectors.gateway.lazybridge.external_tools.read_docs→ uselazytools.documents(pip install 'lazytoolkit[docs]').lazybridge.external_tools.doc_skills→ uselazytools.skills.- The whole
lazybridge.external_toolsnamespace is deleted.
The old paths now raise ModuleNotFoundError instead of emitting a
DeprecationWarning. lazybridge still has no runtime dependency on
lazytools.
[0.8.0] — 2026-05-24 — lazytoolkit extraction (Phases 0–2)¶
The concrete, dependency-carrying tools moved to the new sibling package
lazytoolkit (repo: selvaz/LazyTools). LazyBridge keeps only the minimal
runtime + framework extensions.
Moved (lazy deprecation shims left behind; removed in 0.9)¶
lazybridge.ext.mcp→lazytools.connectors.mcp(pip install 'lazytoolkit[mcp]').lazybridge.ext.gateway→lazytools.connectors.gateway.lazybridge.external_tools.read_docs→lazytools.documents(pip install 'lazytoolkit[docs]').lazybridge.external_tools.doc_skills→lazytools.skills.
Old import paths still work and emit a DeprecationWarning pointing at the new
location. The shims are lazy (PEP 562 __getattr__) so import lazybridge
never imports lazytools — lazybridge has no runtime dependency on the
toolkit. The mcp and tools extras were removed (use lazytoolkit[mcp] /
lazytoolkit[docs]).
[0.7.9] — 2026-05-10 — simplification release¶
The headline change: deletion-led simplification. The framework
had no users yet, so we ship breaking changes without deprecation
paths or shims. Net public surface change: −1 in
lazybridge.__all__ (50 → 49), 5 deleted Agent.from_* class
methods, 9 silent-fallback paths converted to explicit errors, and
the entire report_builder subsystem extracted to its own repo.
Zero new public concept.
The single LLM-friendliness lever is consistency: one canonical form per concept, errors always raise, no opt-in modes.
See docs/migrations/0.7-to-0.79.md for per-deletion before/after
codemod snippets.
Breaking — extraction¶
lazybridge.external_tools.report_builderextracted to the sibling reposelvaz/LazyReport(PyPI:lazybridge-reports). Every import pathlazybridge.external_tools.report_builder.*is gone — replace withlazybridge_reports.*after installing the new package. The five optional extras[report],[report-charts],[report-citations],[report-fallback],[pdf]are gone fromlazybridge'spyproject.toml; their replacements live aslazybridge-reports[charts,citations,fallback,pdf]. No shim — there is no fallback import path.- New optional extra:
[encryption]→cryptography>=42,<46for the newlazybridge.store.encryption.EncryptedStoreAdapter(Fernet at-rest encryption forStorevalues, withMultiFernetkey rotation).
Breaking — deletions¶
- 5
Agent.from_*factories deleted:from_model,from_engine,from_chain,from_plan,from_parallel. All five were pure-alias forwarders (verified by audit). Use the canonicalAgent(engine=...)ctor or the kept-because-non-trivial factories (Agent.chain,Agent.parallel,Agent.from_provider). - 3 config dataclasses deleted:
AgentRuntimeConfig,ResilienceConfig,ObservabilityConfig. These were wrapper-of-flat-kwargs configs whose only behaviour was aflat kwarg > config object > defaultprecedence merge that required a private_UNSETsentinel on every kwarg. The precedence game and_UNSETare gone with them.CacheConfigis kept — it carries real semantic value (enabled/ttl) consumed byLLMEngine. mode="auto"graceful-fallback ladder removed fromTool/tool(). Both default tomode="signature"now; passmode="hybrid"ormode="llm"plusschema_llm=to opt into LLM-driven schema generation. Passingmode="auto"raisesValueError._ParallelAgentrenamed toParallelAgentand its return contract changed.ParallelAgent.__call__andrun()now return ONEEnvelopewhose.payloadis the labelled-text join across every branch (with transitive cost rollup inmetadata.nested_*and first-error short-circuit in.error) — restoring the framework invariant that every Agent returnsEnvelope. For typed per-branchlist[Envelope], call the newrun_branches(task)async helper.wrap_toolmade private (_wrap_tool). Use the publictool(...)factory instead.LLMEngine(tool_choice="parallel")raisesValueError(was a 0.7-eraDeprecationWarningthat downgraded to"auto").\n Concurrent tool execution is the default and not configurable.Old doc/directory deleted (1.2 MB, zero references).pythonpath = ["lazybridge"]removed frompyproject.toml(unused).
Breaking — silent fallbacks → explicit errors¶
from_step("typo")/from_parallel("typo")no longer warn + fall back to the start envelope — they raisePlanRuntimeErrorwith the actual step history and a typo-aware "Did you mean?" hint.LLMEnginewith an unknown model raisesValueErrorinstead of silently routing to Anthropic. SetLLMEngine.set_default_provider("...")for the legacy behaviour.- MCP server emitting a non-
objectinputSchemaraisesValueError(was silently coerced to an empty parameter set). Envelope.text()on a non-JSON-serialisable payload raisesTypeError(wasstr(payload)fallback).Memory(summarizer_timeout < 5.0)warns at construction (timeout almost always fires for typical summariser shapes).BaseProvider._resolve_modelraisesValueErrorwhen nothing is configured (was empty string fallback).Agent(engine=<non-LLM>)requires an explicitname=(was silently named"agent"and collided when used as a tool).Agent(model=..., engine=<non-LLM>)raisesValueError(was silently dropped).
Added¶
lazybridge.matrix— declarative provider-capability lookup.provider_capabilities()andnative_tool_support()aggregate the per-providerClassVarflags into a single typed dict for docs / introspection / capability-aware error messages.BaseProvidercapabilityClassVarflags:supports_streaming/supports_structured_output/supports_thinking. Subclasses override when a backend doesn't.-
Standard error-message format — every
PlanCompileError/PlanRuntimeError/UnsupportedFeatureErrorfollows::Step '
' (# ) — = . Defined steps: [...].> Did you mean ' '? (when applicable) Fix: . -
OTel
gen_ai.agent.nesting_levelattribute on agent spans — dashboards filtering on=0get clean root-only views. Session.emitexporter-exception dedup — warn-once per(exporter class, exception class)pair with a count of suppressed identical failures.test_public_api_snapshot.py— pinslazybridge.__all__and locks the deleted-in-0.7.9 names as permanently gone.
Fixed (bug fixes from Phase 1)¶
- B1: DeepSeek provider — defensively rebuild
params['messages']rather than mutating in place. - B2: Anthropic
_compute_costacceptscached_input_tokens=0and applies the standard 10% cache-read rate; cost telemetry was over-counted on cached calls pre-fix. - B4: Anthropic provider warns on URL-source
AudioContent(was silently dropped). - B6: Plan compiler typo-aware "Did you mean?" suggestions on
from_step/from_agent/from_memoryunknown-target errors. - B7: Plan serialisation raises
ValueErroron unknown sentinelkind(was silently fallback tofrom_prev). - B8: OTel exporter logs SDK exceptions at WARNING (was swallowed).
- B9: Blackboard planner closure state resets per
run()call (was leaking between successive runs). - B10: Plan compiler rejects sentinels referencing auto-named
_anon_<id>steps (LLMs cannot meaningfully produce that name). - B11: Plan resume replays Store sidecar writes from the checkpoint
kvso external consumers see complete state after a crash in the checkpoint→Store-write window. - I5: Anthropic adaptive-thinking warning corrected for Opus 4.7
(pre-fix said "use 'effort'" but Opus 4.7 only accepts
display).
Documentation¶
SKILL.mdrewritten for the 0.7.9 surface; canonical Plan block, default-model fallback advice viafrom_provider(tier=...), anti-pattern entries for every deletion.docs/migrations/0.7-to-0.79.md(new) — per-deletion before/after snippets and a TL;DR table.docs/reference/configs.mdrewritten — onlyCacheConfigremains documented; rest of file explains the deletion.docs/reference/engines.mdadds thethinking=knob.docs/reference/providers.mdadds thestop_reasonnormalisation table (GoogleMAX_TOKENSmapping fix).docs/guides/mid/parallel.mdrewritten for the new single-Envelope return contract.- the MCP guide example now shows
allow=filtering as best practice. examples/verify_judge_loop.py(new),examples/guardrails_demo.py(new), env preflight inexamples/daily_news_report.py.
Tooling¶
lazybridge.skill_docs._buildrecovered + wired into thetest.ymltypecheck job (drift gate that asserts every public symbol in__all__is mentioned in SKILL.md).docs.ymlassertssite/llms.txtandsite/llms-full.txtare non-empty (≥1 KB) aftermkdocs build --strict.- Top-level
permissions: contents: readadded to.github/workflows/test.yml(least-privilege baseline).
[Unreleased] — 2026-05-05 — bug-fix and routing hardening¶
Breaking¶
LLMEngine.stream_idle_timeoutdefault changed fromNoneto90.0s. Old default left provider streams unbounded — a half-open HTTP/2 connection (TCP RST never delivered, PING dropped) would pin a worker indefinitely. New default raisesStreamStallErrorafter 90 s of inter-chunk silence, which is large enough to absorb provider-side thinking pauses on Opus / Gemini Pro. Passstream_idle_timeout=Noneto opt out explicitly — a one-shotUserWarningflags the choice because the failure mode (worker pinned forever) is silent and hard to diagnose. The class-level default exposed for__new__/ subclass paths shifts the same way. No code change is required for callers that already pass an explicit value.
Features¶
Step.after_branches— exclusive-branch rejoin point. Set alongsideroutes/routes_byto route to exactly one branch and skip all sibling steps; execution resumes at the named step after the branch completes. Withoutafter_branches, routing is a detour (linear progression resumes from the routed-to step's declared position). SeeStepdocstring for the full example.
Hardening¶
MCP.stdio()now warns on unrestricted tool surface. When bothallow=anddeny=are omitted a one-shotUserWarningreminds the caller that every tool the subprocess advertises will reach the LLM. Trust model is unchanged (stdio is still audit-on-init, not deny-by-default likeMCP.http); silence the warning by passingallow=["*"]once you've audited the surface, or restrict it with a glob. SeeSECURITY.mdfor the full guidance.- CI now enforces skill-doc drift.
test.ymlrunspython -m lazybridge.skill_docs._build --checkin the typecheck job: a fragment edit without a re-render now fails the PR instead of slipping through (this was the documented contract; CI just hadn't been wired). mkdocs build --strict. A missing nav target or unresolved cross-reference indocs/now fails the docs workflow instead of shipping an empty page.
Documentation¶
- README — added an "alpha (0.7.x)" status callout up top, and
expanded the Full tier sentinel list to cover all five exports
(
from_prev/from_start/from_step/from_parallel/from_parallel_all). - SECURITY.md — new "MCP Servers — Tool Surface Audit" section
documenting the deny-by-default contract on
MCP.httpand the audit-on-init warning onMCP.stdio. - API reference —
_UNSETsentinels in generated signatures now have an explicit explanation at the top of the reference page, including theLLMEngine.stream_idle_timeoutsemantics. - Skill docs reference grouping fixed —
from_parallel_all,GuardError, andEventExporterare now classified under their proper categories instead of falling through to "Core types". - CHANGELOG hygiene — the second
[Unreleased]section was renamed to[0.7.0 — short-term audit hardening]so the file no longer carries two unreleased headers.
Bug Fixes (Critical)¶
- Store SQLite CAS: open transaction on
JSONDecodeError— theexcept sqlite3.Errorclause incompare_and_swapdid not catchjson.loadsfailures on corrupt rows, leavingBEGIN IMMEDIATEopen on the thread-local connection and poisoning every subsequent call on that thread. Widened toexcept (sqlite3.Error, ValueError)(json.JSONDecodeErroris aValueErrorsubclass). - Store in-memory: mutable references break CAS invariants —
read()andwrite()returned / stored the live Python object, so callers mutating the result could silently alter the stored value and defeatcompare_and_swap. Both paths now go through_deep_copy_safe(deep-copy with a non-copyable fallback). LLMEnginetool_choice="any"infinite loop — after the model satisfied the "must call at least one tool" contract on the first turn,provider_tcstayed"any", forcing every subsequent turn to also call a tool. The loop never exited untilmax_turnswas exhausted. Fixed:provider_tcis reset to"auto"immediately after the first tool-result turn.LLMEnginetool_choice="any"sent as literal tool name — Anthropic and OpenAI rejecttool_choice="any"as an unknown tool name. The framework now maps"any"→"required"when building the provider request so the wire value is always a recognised constant.- Plan: parallel-band failure checkpoint pointed at failing step —
when a step inside a parallel band failed, the checkpoint recorded
current_stepas the failing step rather than the band-start, soresume=Truere-entered mid-band in an inconsistent state. Now points at the band-start step. - Agent: failed structured-output retries contaminated memory —
correction retries in
_validate_and_retrywere called with the livememoryobject, so each failed attempt added a garbage turn to the agent's conversation history. Retries now passmemory=None; only the final accepted result reaches memory. Agent.stream(): input guard bypassed —acheck_inputwas not called in the streaming path, soguard=had no effect when callers usedasync for token in agent.stream(...). The guard check now runs before the first token is emitted.
Bug Fixes (High)¶
LLMGuardsync path:timeout=ignored —_judgeinvokedself._agent(prompt)directly on the calling thread without any deadline. Fixed by running the judge in a daemon thread and callingthread.join(timeout=self._timeout).- Memory
strategy="sliding"silently disabled withmax_tokens=None—_plan_compressiongated all compression onbool(self.max_tokens), sostrategy="sliding"with the defaultmax_tokens=Nonenever triggered. Fixed: only"auto"requires a token budget;"sliding"and"summary"compress by turn count independently ofmax_tokens. - Memory:
_overflow_warnedflag shared between turn-cap and summarizer timeout — a summariser timeout silenced the turn-cap warning (or vice versa) because both used the same flag. Split into_overflow_warned(turn cap) and_summarizer_warned(summariser timeout). - Predicates:
empty()/not_empty()treated0/Falseas empty — onlyNoneand zero-length containers (str,list,dict,tuple,set,frozenset) are now considered empty. Numerics and booleans are always non-empty; useeq(0)/eq(False)for those cases. - Google provider:
finish_reasonnever mapped to"max_tokens"— theMAX_TOKENSstop reason from the Google API was not translated, so callers inspectingstop_reasonalways sawNoneinstead of"max_tokens". - Tool schema:
model_dump()destroyed Pydantic model args —Tool.definition()calledmodel_dump()on the entire arguments dict, collapsing Pydantic model instances to plain dicts before the schema was built. Fixed:getattrper field preserves the original objects.
[0.7.0] — pre-1.0 reset, simplified namespace layout¶
Major reorganization. The framework is dropping back to pre-1.0 and
reshaping its namespace boundaries before stabilizing. Everything is
alpha.
Breaking¶
- Version downgrade:
1.0.0 → 0.7.0. The 1.0 release was premature given the API churn since; 0.7.x is the honest baseline. - Single stability tier: every surface is
alpha. Thestable / beta / alpha / domain4-tier taxonomy is removed. Per-module__stability__and__lazybridge_min__markers are removed; onlylazybridge.__stability__ = "alpha"remains. - Namespace reorganization — domain modules moved out of
lazybridge.ext.*: lazybridge.ext.read_docs→lazybridge.external_tools.read_docslazybridge.ext.doc_skills→lazybridge.external_tools.doc_skillslazybridge.ext.data_downloader→lazybridge.external_tools.data_downloaderlazybridge.ext.stat_runtime→lazybridge.external_tools.stat_runtimelazybridge.ext.report_builder→lazybridge.external_tools.report_builderlazybridge.ext.external_tools→lazybridge.ext.gateway(file rename to free the namespace)lazybridge.ext.*is now reserved for framework extensions that augment the agent runtime (mcp,otel,hil,evals,gateway,planners,viz).- New namespace
lazybridge.external_tools.*— domain tool packages (returnslist[Tool]).
Removed¶
lazybridge.ext.veoandlazybridge.ext.quant_agent— neither was ready for use and they only created confusion. Re-introduce later if the underlying integrations stabilize.lazybridge.external_tools.stat_runtime(statistical / econometrics sandbox) andlazybridge.external_tools.data_downloader(Yahoo / FRED / ECB market-data ingestion) — same rationale: scope-creep domain examples that distract from the framework's actual surface. The matching[stats]and[downloader]optional-deps extras are also removed frompyproject.toml.
Tool factory shape (breaking)¶
- All surviving
external_tools/*factories standardize ondef X_tools(*, ...) -> list[Tool]— keyword-only arguments, always returning a list. Single-tool cases return a 1-element list. report_tools(*, output_dir=...)fragment_tools(*, bus, default_section=None, step_name=None)skill_tools(*, skill_dir, ...)(wasskill_tool(skill_dir, ...) -> Tool)skill_builder_tools(*, ...)(wasskill_builder_tool(...) -> Tool)read_docs_tools(*, base_dir=None)— new factory wrappingread_folder_docsas a Tool.
Boundary¶
- New CI check (
tools/check_ext_imports.py):ext/andexternal_tools/may only import from publiclazybridge.*, never from internallazybridge.core.*or other private submodules.
Migration¶
# before
from lazybridge.ext.read_docs import read_docs_tools
from lazybridge.ext.external_tools import ExternalToolGateway
# after
from lazybridge.external_tools.read_docs import read_docs_tools
from lazybridge.ext.gateway import ExternalToolGateway
[0.7.0 — short-term audit hardening] — bundled into the 0.7.0 cut¶
Closes the high-severity findings from the deep architecture audit
(plan §5.1). All changes are additive; defaults shift only on
Session(batched=True) (on_full="hybrid" instead of "drop") which
strictly improves the safety of the existing path — critical events
that previously could be dropped under saturation now block the
producer. Pass on_full="drop" to opt back into the legacy policy.
Hardening¶
- OTel GenAI conventions (audit H-D).
OTelExporternow emitsgen_ai.system/gen_ai.request.model/gen_ai.usage.*/gen_ai.tool.*attributes per the OpenTelemetry Semantic Conventions for Generative AI, and constructs a real parent-child span hierarchy (invoke_agent → chat,invoke_agent → execute_tool) with cross-agent context propagation through OTel contextvars. Tool spans correlate viatool_use_idso N parallel invocations of the same tool no longer collide. Span registry is per-instance so multipleOTelExporters in a process don't fight over the global tracer provider. Memory.summarizer_timeout=(audit H-B). Default 30 s. An LLM summariser that hangs no longer blocksadd()— the keyword fallback runs and a one-shot warning surfaces. Compression also computes the summary OUTSIDEMemory._lock, so concurrentadd()calls progress while a slow summariser is in flight.- Per-event-type back-pressure in
EventLog(audit H-A). New defaulton_full="hybrid"— the writer queue blocks the producer for audit-critical events (AGENT_*/TOOL_*/HIL_DECISION) but drops cheap telemetry (LOOP_STEP/MODEL_REQUEST/MODEL_RESPONSE) under saturation. Override the set viaSession(critical_events=...)."block"and"drop"policies remain available unchanged. - MCP
_tools_cacheTTL + invalidation (audit H-E). Newcache_tools_ttlparameter onMCPServer/MCP.stdio/MCP.http(default 60 s) and aninvalidate_tools_cache()method. An MCP server that hot-loads or unloads tools is eventually reflected in the agent's tool list instead of forever-stale. - Loud surfacing of malformed tool-call arguments (audit M-A).
Provider
_safe_json_loadshelpers (OpenAI, LiteLLM) now tag the raw argument blob with_parse_erroron JSON decode failure or non-object payload.LLMEngine._exec_toolshort-circuits on the tag and emits a structuredTOOL_ERROR(type: "ToolArgumentParseError",parse_error,raw_arguments) instead of letting the tool fail downstream with a misleading "missing required field" message. Tool events also carrytool_use_idfor downstream correlation.
Tests / CI¶
- New
tests/unit/test_audit_short_term.py(17 tests) covering each of the above plus the streaming + tool-call accumulation regression for Gemini / DeepSeek shape (audit M-B). - Coverage policy widened (audit M-I):
lazybridge/ext/{otel,mcp,hil, planners,evals}are now in scope for the gate (previously omitted wholesale). Domain extensions (stat_runtime,data_downloader,doc_skills,veo,quant_agent,read_docs,external_tools) remain omitted because their dedicated test suites live undertests/unit/ext/and are skipped by the default run. Gate stays at 70 with broader coverage; target for 1.1 is 80. - New CI workflows (audit M-J):
release.yml(PyPI Trusted Publishing onv*.*.*tags),codeql.yml(weekly scheduled SAST + per-PR),dependabot.yml(weekly action + pip updates with major SDK pins preserved). Pre-commit hooks now run as a CI job.
[1.0.0] — 2026-04-26 — initial public release¶
Historical: this entry describes the deleted 4-tier stability taxonomy (
stable / beta / alpha / domain) and the original namespace layout. Both were removed in 0.7.0; entries below are kept for historical accuracy only.
Core¶
Agent— universal façade with swappable engines (LLMEngine,Plan, plusHumanEngine/SupervisorEnginefromlazybridge.ext.hil). One call surface (agent.run/agent(...)/agent.stream) regardless of engine.- Tool-is-Tool: plain functions,
Agentinstances,Agent.as_tool()results, and tool providers (e.g. an MCP server) all plug intotools=[...]with the same dispatch contract. Nested agents inherit the outer session for end-to-end observability. - Compile-time DAG validation:
PlanCompilerrejects duplicate step names, forward references, brokenfrom_step/from_parallel/from_parallel_allsentinels, and parallel-band misuse before any LLM call. - Crash-resume:
Plancheckpoints toStoreviacompare_and_swap. Concurrent runs on the samecheckpoint_keycollide at claim time;resume=Trueadopts an in-flight checkpoint. - Parallel tool dispatch: when an LLM emits multiple tool calls in
one turn, the engine runs them concurrently via
asyncio.gather. - Structured output:
output=SomeBaseModelflips the engine into schema-validated mode with retry-with-feedback up tomax_output_retries. - Sources / Memory / Store / Session / Guard: composable
observability and state primitives.
Sessionis SQLite-backed with thread-local connections and a batched event-log writer. - Sync façade:
agent("…")works inside or outside a running event loop. When invoked from inside one, the worker-thread loop inherits the caller'scontextvarscontext so OTel spans / request IDs / structured-logging context flow through.
Providers¶
AnthropicProvider,OpenAIProvider,GoogleProvider,DeepSeekProvider,LiteLLMProvider,LMStudioProvider.- Provider-tier aliasing (
super_cheap→cheap→medium→expensive→top) keeps preview / date-pinned model strings in one place per provider. - LMStudio adapter is a thin
OpenAIProvidersubclass that targets the local server (http://localhost:1234/v1by default), pinned to Chat Completions, with zero cost and no native tools. - Native server-side tools per provider:
WEB_SEARCH/CODE_EXECUTION/FILE_SEARCH/COMPUTER_USE/GOOGLE_SEARCH/GOOGLE_MAPS. - Prompt-caching forwarded via
cache=True(Anthropic explicit, OpenAI / DeepSeek auto, Google via separate API).
Extensions (lazybridge.ext)¶
Extensions ship at __stability__ = "alpha" by default and may break
between minor releases. Promotion: alpha → beta → stable → core. See
docs/guides/core-vs-ext.md for the policy.
ext.hil—HumanEngine(approval gate) andSupervisorEngine(full REPL with tool calls, retry-with-feedback, store access).ext.planners—make_planner(DAG builder) andmake_blackboard_planner(todo-list).ext.mcp— Model Context Protocol integration at the tool boundary.MCP.stdio/MCP.http/MCP.from_transportbuild a tool provider that drops intoAgent(tools=[server]).ext.otel— OpenTelemetry exporter forSession.ext.evals—EvalSuite/EvalCase/llm_judge/ built-in matchers.ext.stat_runtime— sandboxed DuckDB query engine with AST-validated SQL (sqlglot DuckDB dialect; defence-in-depth regex layer for environments without sqlglot).ext.data_downloader,ext.quant_agent,ext.doc_skills,ext.read_docs,ext.veo— domain extensions.
Documentation¶
- Per-tier guides (
docs/tiers/{basic,mid,full,advanced}.md). - Decision trees (
docs/decisions/) — when to use which primitive. - Recipes (
docs/recipes/) — tool calling, structured output, pipeline with resume, human-in-the-loop, MCP, orchestration tools. - LLM-assistant skill (
lazybridge/skill_docs/) ships with the package; same content as the site, signature-first for LLM consumption. Single-source viapython -m lazybridge.skill_docs._build.