Extension engines & integrations¶
Framework extensions that live under lazybridge.ext.* — pip install
lazybridge ships them by default (except OTelExporter, which requires the
[otel] extra).
Connectors moved (0.8). The MCP connector and the external tool gateway are no longer
lazybridge.ext.*— they moved to the LazyTools package (lazytools.connectors.{mcp,gateway},pip install lazytoolkit). The oldlazybridge.ext.{mcp,gateway}deprecation shims were removed in 0.9 — import fromlazytoolsinstead.
For narrative usage see the corresponding guides: HumanEngine, SupervisorEngine, MCP, Evals, OpenTelemetry, Visualizer.
Human-in-the-loop¶
lazybridge.ext.hil.HumanEngine ¶
HumanEngine(*, timeout: float | None = None, ui: Literal['terminal', 'web'] | _UIProtocol = 'terminal', default: str | None = None)
Presents the task to a human and returns their response as an Envelope.
With output=PydanticModel, terminal prompts each field; web renders a form. Emits the same 8 event types as LLMEngine for transparent observability.
Source code in lazybridge/ext/hil/human.py
lazybridge.ext.hil.SupervisorEngine ¶
SupervisorEngine(*, tools: list[Tool | Callable | Any] | None = None, agents: list[Any] | None = None, store: Store | None = None, input_fn: Callable[[str], str] | None = None, ainput_fn: Callable[[str], Awaitable[str]] | None = None, timeout: float | None = None, default: str | None = None)
Human-in-the-loop engine with tool-calling and agent retry.
Source code in lazybridge/ext/hil/supervisor.py
lazybridge.ext.hil.human_agent ¶
human_agent(*, timeout: float | None = None, ui: Literal['terminal', 'web'] | Any = 'terminal', default: str | None = None, **agent_kwargs: Any) -> Agent
Build a human-input :class:Agent (approval gate / form-style HIL).
Symmetric counterpart of Agent.from_<kind>(...) for the
:class:HumanEngine. Use this for synchronous human input —
a prompt at the terminal or a web form — rather than the full REPL
of :func:supervisor_agent.
Engine kwargs (timeout, ui, default) configure the
:class:HumanEngine; remaining **agent_kwargs flow to the
unified Agent constructor::
from lazybridge.ext.hil import human_agent
human_agent(timeout=60.0, default="approve")("Approve deploy?")
Source code in lazybridge/ext/hil/__init__.py
lazybridge.ext.hil.supervisor_agent ¶
supervisor_agent(*, tools: list[Any] | None = None, agents: list[Any] | None = None, store: Any | None = None, input_fn: Callable[[str], str] | None = None, ainput_fn: Callable[[str], Awaitable[str]] | None = None, timeout: float | None = None, default: str | None = None, **agent_kwargs: Any) -> Agent
Build a human-supervised :class:Agent (REPL + tool dispatch + retry).
Symmetric counterpart of Agent.from_<kind>(...) for the
:class:SupervisorEngine. Kept on the ext side rather than as
Agent.from_supervisor to respect the core/ext import boundary
(see docs/guides/core-vs-ext.md).
Engine kwargs (tools, agents, store, input_fn /
ainput_fn, timeout, default) configure the
:class:SupervisorEngine; remaining **agent_kwargs (memory= /
session= / output= / verify= / fallback= / guard= /
name= / etc.) flow to the unified Agent constructor::
from lazybridge.ext.hil import supervisor_agent
supervisor_agent(
tools=[search],
agents=[researcher], # human can `retry researcher: <feedback>`
session=sess,
name="ops-supervisor",
)("publish a policy brief")
Source code in lazybridge/ext/hil/__init__.py
MCP integration¶
Moved to lazytools.connectors.mcp — see the MCP guide
and the LazyTools overview. Install with
pip install lazytoolkit[mcp].
Evaluation framework¶
lazybridge.ext.evals.EvalSuite ¶
lazybridge.ext.evals.EvalCase
dataclass
¶
lazybridge.ext.evals.EvalResult
dataclass
¶
Assertion helpers¶
Ready-made assertion callables for EvalCase (compose your own too):
lazybridge.ext.evals.exact_match ¶
lazybridge.ext.evals.contains ¶
lazybridge.ext.evals.not_contains ¶
lazybridge.ext.evals.min_length ¶
lazybridge.ext.evals.max_length ¶
lazybridge.ext.evals.llm_judge ¶
Returns a judge function using an agent to evaluate output.
Verdict recognition uses the same robust normaliser as
:func:lazybridge._verify.verify_with_retry (W1.1): the judge may
return any of approved / accept / allow / pass /
ok / yes / good / valid (synonyms,
case-insensitive, prefix-anchored) to approve. Explicit reject
prefixes (rejected / deny / block / fail / no
/ bad / invalid) and any unrecognised verdict are treated
as rejection (fail-safe) — so a judge that fails to produce a
clean verdict never accidentally passes a bad output.
Source code in lazybridge/ext/evals/__init__.py
Planners¶
Multi-step planning agents (lazybridge.ext.planners). orchestrator_agent /
blackboard_orchestrator_agent are the canonical factories; make_planner /
make_blackboard_planner are backward-compat aliases for the same callables.
See the Planners guide.
lazybridge.ext.planners.blackboard_orchestrator_agent
module-attribute
¶
lazybridge.ext.planners.make_plan_builder_tools ¶
Five builder tools that share state via closure.
Returns [create_plan, add_step, inspect_plan, run_plan, discard_plan].
The state is per-factory-instance — call make_plan_builder_tools
fresh for each planner agent (or each session) if you want isolated
blackboards. run_plan and discard_plan consume the plan from
the dict, so memory stays bounded as long as the planner finishes its
plans. max_plans is a hard cap on concurrent in-progress plans
(oldest-evicted on overflow) so a misbehaving planner can't leak memory.
Source code in lazybridge/ext/planners/builder.py
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 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 | |
lazybridge.ext.planners.make_execute_plan_tool ¶
Deprecated alias for :func:make_plan_builder_tools. Returns the
list of builder tools rather than a single Tool.
Source code in lazybridge/ext/planners/builder.py
lazybridge.ext.planners.PlanSpec ¶
Bases: BaseModel
The argument shape of execute_plan.
lazybridge.ext.planners.StepSpec ¶
Bases: BaseModel
One node in the plan DAG.
Tiered approval gate¶
Declarative, per-agent ApprovalGate policy for coding engines
(lazybridge.ext.approval) — an ordered Rule table assigns each tool one of
four tiers (allow/session/ask/deny); unmatched calls are denied by
default. TieredGate implements ApprovalGate directly (no wrapper).
Promoted from the approval-lab prototype, with session grants scoped to
(provider, kind, name, cwd, policy fingerprint) and every decision recorded
in a structured AuditRecord.
lazybridge.ext.approval.TieredGate
dataclass
¶
TieredGate(channel: Channel, rules: tuple[Rule, ...], on_record: Any = None, _session_grants: set[tuple[str, str, str, str, str]] = set(), log: list[AuditRecord] = list())
ApprovalGate implementation driven by an ordered rule table.
Implements :class:~lazybridge.engines.coding.ApprovalGate directly
(structural typing — async def __call__(self, request) -> ApprovalDecision
is the whole contract): no bridge class re-declares the signature.
lazybridge.ext.approval.Rule
dataclass
¶
Rule(tier: Tier, name_pattern: str, arg_pattern: str | None = None, kind_pattern: str = '*', provider_pattern: str = '*')
fingerprint ¶
Short stable hash of this rule's shape, used to scope session grants.
Two rules that look identical fingerprint identically even if they
are different Rule instances (e.g. rebuilt on every process
start) — the fingerprint is over content, not identity.
Source code in lazybridge/ext/approval/tiered.py
lazybridge.ext.approval.AuditRecord
dataclass
¶
AuditRecord(timestamp: float, provider: str, kind: str, tool_name: str, cwd: str | None, tier: str, action: str, message: str, arguments_hash: str, arguments_preview: str, responder: str | None = None)
One structured audit entry — every decision the gate makes, allowed or not.
arguments_hash/arguments_preview stand in for the raw arguments so
the audit log stays safe to retain and to display even when a call
carries large payloads or sensitive-looking values (file contents,
tokens pasted into a command). The hash is over the exact JSON the rule
matched against, so two records with equal hashes really did see equal
arguments.
lazybridge.ext.approval.Channel ¶
Bases: Protocol
A human-facing yes/no prompt. Implementations: terminal, chat, queue.
Only the ask coroutine is part of the structural contract. A channel
MAY additionally expose a name: str attribute identifying itself
(e.g. "terminal", "telegram") — :class:TieredGate reads it via
getattr for the audit trail and falls back to the class name when
absent, so plain implementations need not declare it.
lazybridge.ext.approval.TerminalChannel ¶
Ask on stdin. y/yes/si/s/ok approve; anything else denies.
Generic and dependency-free — useful for local development and for
exercising :class:TieredGate in tests without a real chat backend.
Production agents that run unattended (scheduled jobs, always-on
processes) need a different channel; this one blocks on a human being at
the terminal.
Durable background delegation¶
Fire-and-forget delegation for long-running agents
(lazybridge.ext.delegation) — Store-backed job records kept separate from
the process-local asyncio tasks that execute them, process-lifetime
serialized consultant conversations, process-local capped parallel fan-out,
and delegation linked to a DurableBlackboard task. Job
records survive restarts only when backed by a persistent Store; executing
jobs do not resume after restart, and callers should invoke
JobRegistry.reclaim_interrupted() during startup.
Promoted from LazyCEO's generic background-delegation infrastructure after
live production use. Tools produced by the fire-and-forget delegation
factories must be awaited via Tool.run from the delegating agent's
persistent event loop. Tool.run_sync is unsupported because its normal
short-lived-loop paths cancel pending background tasks when the outer call
returns. The synchronous status/result tools built by JobRegistry are not
subject to this restriction.
lazybridge.ext.delegation.JobRegistry ¶
Store-backed registry shared by every background delegation tool.
Source code in lazybridge/ext/delegation/jobs.py
write ¶
write(job_id: str, objective: str, *, tool_name: str, status: str, plan_id: str | None = None, task_index: int | None = None, plan_task_text: str | None = None, result: str | None = None, error: str | None = None, engine: str | None = None, model: str | None = None, effort: str | None = None, execution_started: bool | None = None, cost_usd: float | None = None, cost_unknown: bool | None = None, created_at: str | None = None, finished_at: str | None = None, owner_pid: int | None = None, owner_boot_id: str | None = None, extra: dict[str, Any] | None = None) -> None
Write one complete job record, omitting optional fields set to None.
One construction path matters here: the promoted implementation used both a helper and a hand-built consultant dict. Keeping every writer on this method prevents their durable record shapes from drifting.
Deliberately NOT a read-modify-write merge onto whatever is
currently stored: every call rebuilds the record from exactly the
fields it passes, the same contract this method has always had.
A field that must survive from one write to the next (created_at,
most notably) is the CALLER's job to carry forward and re-pass, not
something this method infers by peeking at the old record -- that
keeps this method's behaviour obvious from its own call site alone.
Source code in lazybridge/ext/delegation/jobs.py
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 | |
begin_execution ¶
CAS a queued running record to started, preserving unknown fields.
False means recovery or another worker won. Never starts a missing, terminal, or already-started record. Legacy missing markers are allowed.
Source code in lazybridge/ext/delegation/jobs.py
update ¶
update(job_id: str, changes: dict[str, Any], *, only_active: bool = True, expected_started: bool | None = None) -> bool
CAS a partial update, retaining fields the caller does not own.
expected_started optionally protects setup cleanup from overwriting
another worker that crossed the execution boundary in the meantime.
Source code in lazybridge/ext/delegation/jobs.py
reclaim_interrupted ¶
Mark orphaned in-progress jobs interrupted -- but only the
ones whose OWNER is actually dead.
Meant to be called at process startup. A background asyncio Task
does not survive across processes, so an old running or
awaiting_approval record left by a process that has since
exited has no coroutine (or live approval wait) left to resume.
interrupted is more accurate than failed and avoids
silently presenting the job as active.
Ownership-aware: a record stamped with owner_pid/
owner_boot_id (see :func:current_owner_fields) is left alone
when :func:_job_owner_is_alive confirms that process is still
running -- more than one process can share a Store and be
concurrently alive at once (the exact case the un-ownership-aware
version of this method could not tell apart from a genuinely dead
writer), and this is what makes that safe. A record with no owner
stamp at all answers "not confirmed alive" the same way the
un-ownership-aware version always treated every in-progress record,
so a caller that never adopts :func:current_owner_fields keeps
the exact old behaviour. Promoted from LazyCEO's
simple.agent's owner-aware startup recovery.
Compare-and-swap is essential regardless: a genuine old-process
completion (or an owner dying between the read above and this
write) may land between this scan and the update. A lost CAS skips
that job so its real done/failed result is never
clobbered.
Source code in lazybridge/ext/delegation/jobs.py
find ¶
Find a job by full id or the short prefix shown by check_jobs.
An 8-character uuid4 prefix collision needs thousands of jobs in one Store, so first match wins deliberately rather than adding ambiguity handling to the model-facing lookup path.
Source code in lazybridge/ext/delegation/jobs.py
checker_tool ¶
Build the compact, many-job polling tool.
Source code in lazybridge/ext/delegation/jobs.py
result_tool ¶
Build the untruncated escape hatch for compact checker previews.
Source code in lazybridge/ext/delegation/jobs.py
lazybridge.ext.delegation.make_background_delegate ¶
make_background_delegate(*, tool_name: str, label: str, engine_factory: Callable[..., Any], registry: JobRegistry, background_tasks: set[Task[Any]], notify: Callable[[str], None] | None, doc: str, pre_confirm: Callable[[str], Awaitable[bool]] | None = None, validate_model: Callable[[str | None], str | None] | None = None, model: str | None = None, effort: str | None = None, admission_gate: Callable[[], Awaitable[Any]] | None = None, accept_model_override: bool = False, accept_effort_override: bool = False, extra_params: Mapping[str, ExtraParam] | None = None, guard: Callable[[str, dict[str, Any]], Any] | None = None, job_runner: JobRunner | None = None) -> Tool
Build a fire-and-forget delegate with durable status reporting.
Fire-and-forget is intentional: with max_concurrent_inbound=1, an
in-turn await on a real delegated task (often the longest call an agent
makes) blocks every other message. pre_confirm exists because once a
sub-agent's own actions are not individually gated, the reliable human
decision point is before the whole delegated objective starts.
validate_model and model together let a caller reject a model
its OWN engine_factory was built to use (e.g. a non-Anthropic model
handed to a Claude Code engine) before anything is recorded or spawned.
By default model is not a per-call argument on the delegate tool
this builds (the engine a caller wants is already fixed by whatever
engine_factory closes over); it exists purely so this validation can
run against the SAME value, once, up front. Passing
accept_model_override=True (and likewise accept_effort_override
for effort) exposes a per-call override instead -- see below. This
package carries no opinion about which models are valid for which
engine -- that policy belongs to the caller, passed in as
validate_model.
accept_model_override/accept_effort_override, when set, add an
optional model/effort parameter to the generated delegate
tool. A call that omits it falls back to this function's own
model/effort default -- the override is additive, never a
requirement to pass one every call. The RESOLVED value (override, or
the default) is what validate_model is run against and what is
passed to engine_factory as a model=/effort= keyword (see
extra_params below for how that call is shaped). This is the
generic answer to a caller (LazyCEO's codex_write/claude_write)
whose own sub-agent engine needs a different model or reasoning effort
on a given call, not only the one fixed at tool-build time.
extra_params declares further per-call parameters beyond
objective/model/effort -- LazyCEO's own motivating case is
a repo argument, resolved and checked against a policy on every
call. Each is forwarded verbatim as a keyword argument to
engine_factory (which must then accept it) and, together with the
resolved model/effort, to guard. When none of
accept_model_override, accept_effort_override, or
extra_params is set (the default), engine_factory is still
called with zero arguments, exactly as before -- the generated tool's
signature and JSON Schema are BYTE-IDENTICAL to the pre-1.8 shape. Once
any of them is set, the tool is instead built via
:meth:~lazybridge.Tool.from_schema with an explicit schema (see
_build_delegate_schema), because the real Python callable
underneath has to accept an arbitrary **extra whose shape varies
per call site -- signature introspection cannot describe that.
guard, when given, is called with (objective, call_kwargs)
-- call_kwargs being exactly what will be passed to
engine_factory if this call proceeds (the resolved model/
effort when their overrides are enabled, plus every extra_params
value) -- and runs FIRST, before validate_model, before
pre_confirm, before any job record is written or any task spawned.
Returning a non-None string rejects the call with that string
(the same "REJECTED: ..." convention every other free precondition in
this package uses); returning None allows it through. May be a sync
or an async callable. This is the generic per-call validation hook a
caller's own policy (e.g. LazyCEO's project-work gate) runs through --
this package has no opinion about what it checks.
admission_gate runs once at scheduling time without pre_confirm,
or once after approval when confirmation is enabled. A refused call without
confirmation returns the caller's rejection text before registration. Grants
belong to the attempt: release after execution, refund on every path that
never starts, including setup/scheduling failure and cancellation before
the task's first step. Supplying admission opts into the CAS JobRunner
boundary unless a custom runner is supplied. No admission policy lives here.
Source code in lazybridge/ext/delegation/background.py
510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 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 686 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 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 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 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 | |
lazybridge.ext.delegation.make_claude_delegate_engine_factory ¶
make_claude_delegate_engine_factory(*, workspace_root: Path, gate: Any, model: str) -> Callable[[], Any]
Build a factory that creates one gated Claude Code engine per job.
Source code in lazybridge/ext/delegation/background.py
lazybridge.ext.delegation.make_parallel_delegate ¶
make_parallel_delegate(*, engine_factory: Callable[[], Any] | None = None, registry: JobRegistry, background_tasks: set[Task[Any]], notify: Callable[[str], None] | None = None, doc: str, max_parallel_objectives: int = 8, max_in_flight_delegate_tasks: int = 12, workspace_root: Path | None = None, gate: Any = None, model: str = 'sonnet', admission_gate: Callable[[], Awaitable[Any]] | None = None, job_runner: JobRunner | None = None, structured_outcomes: bool = False) -> Tool
Build a capped, fire-and-forget parallel delegation tool.
The per-call cap bounds the resource commitment one LLM tool call can
make. The in-flight cap is deliberately process-local and cooperative,
not a fleet-wide guarantee; it coordinates only callers sharing this
background_tasks set.
admission_gate, when given, is checked ONCE PER OBJECTIVE, not once
for the whole batch -- this tool is exactly the shape an admission
policy exists for: N engines started from one decision. Checking once
for the batch would let a single admission buy every objective's worth
of whatever that policy limits; skipping the per-objective check
entirely would let a fan-out start above a ceiling a single call would
have respected. The checks run BEFORE any objective is scheduled, and
the first refusal aborts the WHOLE batch -- nothing partially admitted
is started, so a batch too large for the current admission state needs
a smaller batch, not a partial one nobody asked for. Every admission
already granted earlier in THIS batch is given back (via
:func:~lazybridge.ext.delegation.admission.refund_admission) the
moment a later objective is refused, or the post-check capacity race
below fires -- the batch starts nothing, so nothing it already
reserved may stay reserved either. This mirrors LazyCEO's own
run_parallel batch-rollback behaviour. Once an objective's own job
is actually scheduled, its admission is released (not refunded) when
that job finishes, success or failure -- see
:func:_run_with_admission_release.
run_parallel itself must be a COROUTINE function, not a plain one:
:class:~lazybridge.Tool dispatches a synchronous tool's function
through loop.run_in_executor, on a worker thread with no running
event loop -- asyncio.create_task inside _track would raise
there. Declaring it async routes execution through the caller's
own event loop instead, where a task can actually be created. Found
by Codex review before this ever shipped.
Source code in lazybridge/ext/delegation/background.py
1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 | |
lazybridge.ext.delegation.make_persistent_consultant ¶
make_persistent_consultant(build: Any, *, tool_name: str, registry: JobRegistry, background_tasks: set[Task[Any]], notify: Callable[[str], None] | None = None, doc_suffix: str = '', accept_fresh: bool = False, accept_model_override: bool = False, accept_effort_override: bool = False) -> Tool
Build one serialized, handle-remembering consultant conversation.
The lock guards the background job rather than the tool call, preserving low turn latency. Without it, overlapping calls can both read the same stale handle, open separate conversations, and leave only the final reply's handle remembered.
accept_fresh, when set, adds an optional fresh: bool = False
parameter to the generated ask tool: a call with fresh=True
starts a brand-new thread (passes None for the remembered handle
instead of the current one) rather than continuing the running
conversation -- whatever handle that new thread gets back still becomes
the one subsequent calls continue, exactly like today's single running
conversation.
accept_model_override/accept_effort_override, when set, add an
optional model/effort parameter the same way, forwarded to the
wrapped consultant call for that one call only. Each requires the
wrapped consultant function to actually accept a model/effort
parameter of its own -- checked once at build time (like the
thread_id/session_id detection just below), raising
:class:TypeError immediately rather than failing on the first call
that tries to use it.
When none of accept_fresh, accept_model_override, or
accept_effort_override is set (the default), the generated tool's
signature is exactly ask(question: str), byte-identical to the
pre-1.8 shape.
Source code in lazybridge/ext/delegation/background.py
952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 | |
lazybridge.ext.delegation.make_plan_delegate ¶
make_plan_delegate(*, engine_factory: Callable[[], Any] | None = None, registry: JobRegistry, background_tasks: set[Task[Any]], board: Any, owner: str, notify: Callable[[str], None] | None = None, doc: str | None = None, max_parallel_objectives: int = 8, max_in_flight_delegate_tasks: int = 12, workspace_root: Path | None = None, gate: Any = None, model: str = 'sonnet', admission_gate: Callable[[], Awaitable[Any]] | None = None, job_runner: JobRunner | None = None, rollback_claim: Callable[[JobContext], Any] | None = None, structured_outcomes: bool = False, stop_on_refusal: bool = False) -> Tool
Build a tool that claims durable-plan tasks before delegation.
Like run_parallel, this must be a coroutine function -- see that
function's docstring for why a synchronous one would break
asyncio.create_task inside _track when dispatched through
:class:~lazybridge.Tool's executor path for plain functions.
Unlike run_parallel, admission_gate here is checked PER ITEM,
immediately before that item's own board.claim_task -- not in a
separate whole-batch pre-pass -- because each item's claim and job
record are already independent of every other item's in this loop. A
refusal costs that one item nothing: nothing was claimed yet, so
nothing needs unwinding on the board. The unwind this function HAS
always had to do -- a claim that succeeds but whose job then fails to
even get scheduled (registry.write/_track raising) -- now also
refunds that item's granted admission (via
:func:~lazybridge.ext.delegation.admission.refund_admission)
alongside releasing the board claim (board.mark_failed): the
attempt that admission was reserved for never actually ran. Once a
job IS scheduled, its admission is released (not refunded) when that
job finishes, success or failure -- see
:func:_run_with_admission_release.
Source code in lazybridge/ext/delegation/background.py
1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669 1670 1671 | |
lazybridge.ext.delegation.make_claude_writer ¶
make_claude_writer(*, workspace_root: Path, gate: Any, registry: JobRegistry, background_tasks: set[Task[Any]], model: str = 'sonnet', effort: str | None = None, notify: Callable[[str], None] | None = None, doc: str, admission_gate: Callable[[], Awaitable[Any]] | None = None, validate_model: Callable[[str | None], str | None] | None = None, max_turns: int = 60, accept_model_override: bool = False, accept_effort_override: bool = False, accept_session_override: bool = False, session_alias: str | None = None, session_registry: SessionRegistry | None = None, accept_session_alias_override: bool = False, engine_factory: Callable[..., Any] | None = None, extra_params: Mapping[str, ExtraParam] | None = None, guard: Callable[[str, dict[str, Any]], Any] | None = None, job_runner: JobRunner | None = None) -> Tool
Build a per-action-gated Claude Code workspace writer.
session_alias and session_registry resume a durable conversation by
name, distinct from the native session id. See the Codex writer for the
optional per-call alias and factory/guard/lifecycle hooks.
admission_gate is consulted at scheduling time before registration.
Its grant is refunded for work that never starts and released on completion.
max_turns defaults to 60 (the SDK's own default is 20 --
too low for a real multi-file objective; see the comment on the
engine construction below). A caller whose own sub-agents need more
headroom still (LazyCEO settled on 100 after 20 proved too low
live) passes a larger value here.
accept_model_override/accept_effort_override/
accept_session_override expose an optional per-call model/
effort/session parameter on the generated tool -- see
make_background_delegate's own docstring for
accept_model_override/accept_effort_override. session is
new here: when accepted, it is forwarded as
ClaudeCodeEngine(session_id=...), resuming that session instead of
starting a new one. All three default to False, keeping today's
fixed-at-build-time behaviour the default.
Source code in lazybridge/ext/delegation/writers.py
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 | |
lazybridge.ext.delegation.make_codex_writer ¶
make_codex_writer(*, workspace_root: Path, gate: Any, channel: Any, registry: JobRegistry, background_tasks: set[Task[Any]], model: str | None = None, effort: str | None = None, notify: Callable[[str], None] | None = None, doc: str, confirmation_prompt: str = 'About to delegate to Codex: {preview}\n\nProceed?', validate_model: Callable[[str | None], str | None] | None = None, admission_gate: Callable[[], Awaitable[Any]] | None = None, writable_roots: Sequence[str] | None = None, accept_model_override: bool = False, accept_effort_override: bool = False, accept_session_override: bool = False, session_alias: str | None = None, session_registry: SessionRegistry | None = None, accept_session_alias_override: bool = False, engine_factory: Callable[..., Any] | None = None, extra_params: Mapping[str, ExtraParam] | None = None, guard: Callable[[str, dict[str, Any]], Any] | None = None, job_runner: JobRunner | None = None) -> Tool
Build a one-shot-confirmed Codex workspace writer.
session_alias and session_registry resume a durable conversation by
name; accept_session_alias_override exposes that distinct per-call name.
Native session still means a provider id. engine_factory, extra_params,
guard, and job_runner pass through the background builder's hooks.
validate_model and admission_gate are passed straight through to
:func:~lazybridge.ext.delegation.background.make_background_delegate --
see that function's own docstring for what each hook does and when it
runs. This module carries no model-validation or admission POLICY of
its own; both are entirely the caller's.
writable_roots, when given, extends the Codex sandbox's writable
paths beyond workspace_root itself via
:class:~lazybridge.engines.coding.CodexPolicy's own field of that
name -- the prime use case is workspace_root being a git
worktree, whose index and refs live under the main repository's
.git (git rev-parse --git-common-dir), outside the worktree
directory and otherwise unwritable. Resolving which extra path(s) a
given worktree needs is left entirely to the caller (this module has
no git-topology opinion of its own); pass the result here.
accept_model_override/accept_effort_override/
accept_session_override expose an optional per-call model/
effort/session parameter on the generated tool -- see
make_background_delegate's own docstring for accept_model_override/
accept_effort_override. session is new here: when accepted, it
is forwarded as CodexEngine(thread_id=...), resuming that thread
instead of starting a new one. All three default to False, keeping
today's fixed-at-build-time behaviour the default.
Source code in lazybridge/ext/delegation/writers.py
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 | |
lazybridge.ext.delegation.session_id_key ¶
Return the stable Store key for a workspace's resumable session.
This is parameterized by workspace_root, not one fixed key, because
a Claude Code SDK session created under one cwd cannot be resumed from a
different one. The short hash keeps paths out of the Store namespace
while remaining stable across restarts in the same workspace.
Resolved to an absolute, normalized path before hashing -- an
unresolved relative root would scope the key to its literal spelling
rather than the actual workspace: Path("project") launched from
two different parent directories would otherwise hash to the SAME
key for two different working directories, and a relative vs.
absolute spelling of the same directory would hash to two DIFFERENT
keys for the same one. Either mistake resumes an incompatible session
or fails to find the existing one. Found by Codex review before this
ever shipped.
Source code in lazybridge/ext/delegation/jobs.py
Durable knowledge base¶
Store-backed, cross-session "lessons" a long-running agent can record after
non-obvious success (lazybridge.ext.knowledge). With a persistent Store,
these reusable notes outlive the plan, task, conversation, and process in
which they were learned. save_lesson() is create-only; updates and
retractions are explicit optimistic-CAS operations through
revise_lesson().
Promoted from LazyCEO's lazyceo.lessons module after live production use.
Unlike lazybridge.Memory, which represents conversation-history context,
the knowledge base holds independently searchable retrospective lessons;
unlike DurableBlackboard, it does not represent task state.
lazybridge.ext.knowledge.DurableKnowledgeBase ¶
A durable, Store-backed collection of "lessons" -- reusable notes an agent records after genuinely non-obvious success, retrievable by keyword search across every future session.
Keyed under a configurable prefix so multiple independent
knowledge bases (e.g. one per application, or one shared fleet-wide
base) can live in one Store without key collisions -- construct
one DurableKnowledgeBase per prefix a caller needs; the object
itself holds no connection of its own beyond the Store it's
given, so constructing one is cheap.
prefix matching is a literal string prefix (same semantics as
:meth:Store.items's own prefix=), NOT a namespace boundary: a
base whose prefix is itself a prefix of another base's prefix (e.g.
"app:" and "app:team:") is NOT isolated from it -- listing the
outer base's lessons will include the inner base's too, since every
inner key literally starts with the outer prefix. Choose prefixes that
are NOT string-prefixes of each other if isolation between two bases
matters.
Source code in lazybridge/ext/knowledge/durable.py
save_lesson ¶
Create the lesson for topic. Rejected (not overwritten) if
one already exists under that slug -- the caller is pointed at
:meth:revise_lesson with the current revision, so an update is
always an explicit, CAS'd action, never an accidental blind
overwrite.
Also rejected if topic would produce a slug longer than
:data:MAX_SLUG_LENGTH -- see that constant's docstring for why.
Known accepted gap: create-only relies on
:meth:Store.compare_and_swap's own expected=None contract,
which -- by that method's own documented semantics -- cannot
distinguish "key absent" from "key present holding a literal JSON
null". A record ever written as bare null (nothing in this
codebase does that) would be silently treated as absent and
overwritten rather than rejected. Not fixed here: it would need a
new Store-level primitive to atomically distinguish the two cases,
and every other create-only-via-CAS caller in this codebase leans
on the identical contract. Found by Codex review before this ever
shipped.
Source code in lazybridge/ext/knowledge/durable.py
revise_lesson ¶
revise_lesson(*, slug: str, expected_revision: int, what_worked: str, gotchas: str | None = None, status: LessonStatus = 'active', correction_reason: str = '') -> str
Update (or retract) an existing lesson, CAS'd against
expected_revision so a correction can't silently clobber a
concurrent edit -- a mismatch is rejected with instructions to
re-read the current version first, the same shape as
:class:~lazybridge.ext.planners.DurableBlackboard's stale-claim
rejections.
status="retracted" keeps the record (a tombstone with
correction_reason explaining why) instead of deleting it --
retracted lessons are excluded from
:meth:search_lessons/:meth:find_lessons by default, but the
history of "this used to be believed" survives.
gotchas defaults to None, meaning "leave it as it is" --
unlike what_worked (always required, since a revision without
new content isn't really a revision), a caller correcting
what_worked alone shouldn't have to also re-paste an unrelated
gotchas just to avoid silently wiping it. Pass gotchas=""
explicitly to actually clear it. Found by Codex review before this
ever shipped, after an earlier version defaulted to "" and
clobbered any existing gotchas on every revision that didn't
happen to repeat it.
Source code in lazybridge/ext/knowledge/durable.py
find_lessons ¶
find_lessons(query: str, *, limit: int = 5, include_retracted: bool = False) -> list[dict[str, Any]]
The lessons best matching query, most relevant first.
A small lexical scorer, not semantic search: query terms (stopwords and single characters dropped) are matched against each lesson's topic (weight 3), what_worked (weight 2), and gotchas (weight 1) token counts. Ranked first by how many DISTINCT query terms hit at all, then by total weighted count -- a lesson matching two different words beats one matching the same word three times, which plain substring-count scoring (an earlier version of this module) could get backwards.
Returns raw records (not rendered text) so both
:meth:search_lessons and a caller's own forced-retrieval hook
(e.g. wired into an agent's task-claim loop, the way LazyCEO's own
claim_next() does -- see this module's own docstring) share
one scoring implementation instead of drifting apart. Call this
directly from such a hook rather than relying on the agent to
remember to call :meth:search_lessons itself.
Source code in lazybridge/ext/knowledge/durable.py
search_lessons ¶
Keyword-match query against saved lessons; a short ranked
list (never full text). See :meth:find_lessons for the scoring
rule.
Source code in lazybridge/ext/knowledge/durable.py
lazybridge.ext.knowledge.LessonStatus
module-attribute
¶
lazybridge.ext.knowledge.render_lesson_line ¶
One line: slug/revision/staleness flag, topic, and a short gist -- never the full text, so a caller injecting several of these (e.g. into an agent's own task-claim result) can't flood context the way dumping every lesson's full content on every tick would.
topic, what_worked, AND slug are all collapsed to
single-line text (embedded newlines replaced with spaces) and
length-capped -- an earlier version truncated only what_worked,
so a caller free to write an arbitrarily long or multiline topic
(nothing validates it beyond :func:slugify needing SOME alphanumeric
content) could still make one "line" span many lines or dwarf the
gist entirely; a second version fixed topic but missed that
:func:slugify doesn't cap length either, so the SLUG itself (derived
directly from an uncapped topic) could still blow the bound this
function exists to enforce. Found by Codex review before this ever
shipped (twice: once for topic/gist, again for the slug).
Source code in lazybridge/ext/knowledge/durable.py
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 398 399 400 401 402 403 404 405 406 407 408 | |
lazybridge.ext.knowledge.slugify ¶
A stable, key-safe identity for a topic.
Two calls with the same wording (modulo case, punctuation, or Unicode
normalization form -- see :func:_normalize) collide on purpose --
that collision is what makes a second save_lesson on the same
topic get refused (and pointed at revise_lesson) instead of
silently landing as an unrelated new record.
Built on the same combining-mark-aware :func:_tokenize used for
search terms (word-runs joined by -), not a plain \W-based
regex -- a regex splitter would treat a combining mark as a
separator, which is invisible for scripts NFKC precomposes (Latin
accents) but for scripts that represent vowels/tone purely as marks
with no precomposed form (Devanagari matras, Arabic harakat, Hebrew
niqqud) would make two topics differing ONLY in which mark is
attached to a base letter collapse onto the SAME slug -- e.g. Hindi
"कि" (ka + vowel sign i) and "कु" (ka + vowel sign u) both
reducing to slug "क". That's a real, not cosmetic, harm: the second
save_lesson call would be wrongly rejected as a duplicate of the
first via the create-only CAS check, even though the two topics are
genuinely different. Found by Codex review before this ever shipped.
Source code in lazybridge/ext/knowledge/durable.py
OpenTelemetry exporter¶
lazybridge.ext.otel.OTelExporter ¶
Export events as OpenTelemetry spans (requires opentelemetry-sdk).
Emits gen_ai.* attributes per the OpenTelemetry Semantic
Conventions for GenAI, with proper parent-child span hierarchy.
Install: pip install lazybridge[otel]
Thread-safe: Session.emit can fan events out to this exporter
from multiple worker threads, so the in-flight span registry is
guarded by a lock. Call :meth:close to flush any spans that are
still open (e.g. when a run is cancelled before agent_finish).
The exporter sets each span as the current OTel context span while it is open, so nested agents (Agent-as-tool) automatically inherit the outer tool span as their parent without any explicit correlation id — OTel's contextvars-based propagation does the work.
Source code in lazybridge/ext/otel/exporter.py
close ¶
Flush any spans still open — e.g. after a cancelled run.
Idempotent. Without this, a run that crashes before emitting
agent_finish leaves its spans stuck for the life of the
process, and the OTel contextvars stay attached to nothing.
Source code in lazybridge/ext/otel/exporter.py
flush ¶
Drain pending spans from the BatchSpanProcessor.
No-op when batch=False (SimpleSpanProcessor flushes synchronously).
Call before process exit to ensure all spans reach the collector.
Source code in lazybridge/ext/otel/exporter.py
Visualizer¶
lazybridge.ext.viz.Visualizer ¶
Visualizer(session: Session, *, store: Store | None = None, host: str = '127.0.0.1', port: int = 0, auto_open: bool = True)
Live or replay visualizer.
Use as a context manager so the HTTP server is shut down cleanly when the with-block exits. The browser stays open across that boundary; the user is expected to close the tab themselves.
Source code in lazybridge/ext/viz/visualizer.py
open ¶
Block the caller until Ctrl+C, useful for replay scripts.
Replaceable delegation lifecycle (1.8)¶
make_background_delegate, make_parallel_delegate, and make_plan_delegate
accept job_runner: JobRunner | None = None. Omitting it preserves the existing
worker. JobRunner(prepare=..., register=..., execute=..., finalize=..., rollback=...)
exposes four sync or async phases, each receiving the same JobContext.
The default prepare constructs both engine and Agent; register calls
JobRegistry.begin_execution(job_id) to CAS a running, unstarted record to
started; execute runs the prepared Agent; finalize stores the result using
JobRegistry.update(job_id, changes, only_active=True) while preserving unknown fields.
A custom register returns False when its CAS loses. No execution or terminal
rewrite follows that refusal. The rollback callback runs once on refusal, phase
failure, or cancellation, including partial setup. Its context exposes phase,
error, started, prepared, result, and caller-owned metadata; callers can
capture claim ownership and pre-claim attempts there. Reservation settlement is
owned by the runner: refund before execution, release after execution starts.
Callbacks should not also settle that reservation. Exceptions propagate after
cleanup; a failing Store makes terminal recording best effort. No lifecycle can
write a terminal record while its Store is unavailable.
Background admission without confirmation (1.8)¶
make_background_delegate(..., admission_gate=...) checks once before registration
when pre_confirm is absent. A refusal returns rejection_text() directly and
creates no job. With confirmation, admission remains after approval. A grant is
owned by one job: setup, scheduling, CAS refusal, and cancellation before execution
refund it; execution completion/failure/cancellation release it. Admission enables
the optional CAS runner by default, so engine and Agent setup happen before the
execution marker. make_claude_writer forwards the same gate. Quotas, approval
spending, rejection wording, and reservation storage remain caller policy.
Cancellation before a task's first step schedules retained cleanup on the host's
persistent event loop; await the retained tasks before closing that loop.
Writer aliases and reusable provider factories (1.8)¶
Both writers accept session_alias: str | None,
session_registry: SessionRegistry | None, and
accept_session_alias_override: bool = False. A generated session_alias tool
parameter is a durable name scoped to the engine cwd. The existing optional
session parameter continues to mean a native Codex thread or Claude session id.
Combining a native id and an alias in the same factory call raises ValueError.
The engines resolve and bind aliases through lazybridge.engines.sessions;
reconstructing the writer and registry after restart resumes the saved native id.
make_codex_writer_engine_factory(*, workspace_root, gate, model=None, effort=None,
session_alias=None, session_registry=None, writable_roots=None) returns a callable
accepting keyword cwd, model, effort, session, session_alias, and
writable_roots. make_claude_writer_engine_factory takes the same common
settings, with model="sonnet" and max_turns=60 instead of writable roots;
its callable also accepts a per-call max_turns. Every call builds a fresh engine
and keeps the existing writer permission configuration and unbounded request timeout.
Writers also forward optional engine_factory, extra_params, guard, and
job_runner to background delegation. For example, declare
extra_params={"cwd": ExtraParam(required=True)} and validate allowed repositories
in your own guard; the provider factory receives the chosen cwd. Custom factories
own their build-time settings; per-call arguments are forwarded unchanged. No
repository resolution, model rules, or quota policy is supplied by these adapters.
Plan rollback and structured batch outcomes (1.8)¶
make_plan_delegate(..., rollback_claim: Callable[[JobContext], Any] | None = None,
structured_outcomes: bool = False, stop_on_refusal: bool = False) adds an optional
attempt-restoring callback. It receives plan_id, task_index, job_id,
attempts_before (captured immediately before the claim), claim_owner (the
original owner), claimed, phase, error, and started. It runs once for each
valid item that never starts: admission refusal, later held-back items, claim
refusal, registration/scheduling failure, execution-CAS refusal, engine/Agent
setup failure, or cancellation before execution. Unclaimed items need no board
mutation; their original attempts are preserved. The callback can verify the
expected owner, job id, and attempt count before restoring a claimed item. It
must also refuse to release a claim if another worker is executing that same job.
This package does not implement the caller's board ownership or restoration policy.
Supplying rollback_claim enables the CAS runner and composes the claim hook with
an optional job_runner.rollback; the latter still receives failures after real
execution starts. Grants settle once even if rollback raises. Claim metadata is
also persisted in the initial job record. Omitting the claim hook keeps the
existing scheduling-failure unwind through board.mark_failed. A compatible board
for the new hook provides snapshot().tasks with attempts, in addition to
claim_task. The callback may be sync or async.
structured_outcomes=True returns a JSON-compatible dictionary from the tool:
{"items": [
{"item_number": 1, "task_index": 0, "status": "started",
"job_id": "full-job-id", "reason": None},
{"item_number": 2, "task_index": 1, "status": "refused",
"job_id": None, "reason": "caller admission reason"},
]}
Statuses are started, refused, and failed; every input item has one entry
in input order, including whole-call refusals. started means scheduled at tool
return, not completed or guaranteed to cross the execution CAS. Later worker
outcomes remain in JobRegistry. A setup failure records failed and its reason;
a held-back item records refused with the triggering item number. With
stop_on_refusal=True, refusal at item 3 leaves items 1–2 scheduled and calls the
not-started rollback for valid items 3–5. Without it, later items are still tried.
Existing string output remains the default. make_parallel_delegate also accepts
structured_outcomes=True, with task_index=None; its existing admission pre-pass
still holds back the whole batch on refusal.
Custom register callbacks can use context.begin_execution() to remember an
acquired CAS even if subsequent registration setup raises; for an external CAS,
set context.registered=True upon acquisition. JobRegistry.update accepts
expected_started: bool | None = None for cleanup that must not overwrite a
competing worker's execution marker. JobRunner.abort(context, error, phase="schedule")
unwinds an unsuccessful handoff using the same once-only cleanup as its phases.
Keep contexts attempt-local and never reuse one for a new job.
Local Phase B validation used Python C:/ProgramData/spyder-6/python.exe with
imports verified against the delegation worktree. The initial unit baseline was
2,915 passed, 17 skipped, and one existing Windows scratch-path failure in
test_event_exporter_protocol (/tmp/x.jsonl). That case passed in isolation
with a local pytest plugin redirecting just that literal path into .test-tmp.
The final full unit suite with the same redirection was 2,956 passed, 17 skipped,
zero failures: all baseline cases plus 40 new cases. The focused delegation suite
was 126 passed. Ruff lint, delegation Ruff format check, and delegation Mypy
passed. Tests used temporary Stores and simulated provider execution; live CLI,
quota, and notification integration were outside this local-only validation.