Guarded runtime — compose, inject acceptance, run, record¶
The pieces exist separately: compose(spec) builds the engine call, an
acceptance policy (typically a guard pipeline) composes the checks, a decision
log records the decision and a kill switch is the operator stop.
run_guarded wires them into one call so a product does not reimplement the
order.
from agentdescent import run_guarded
guarded = run_guarded(
spec,
acceptance=pipeline, # any AcceptancePolicy, e.g. a GuardPipeline
decision_log=log, # any object with .record(**kwargs)
kill_switch=switch, # any object with .engaged()/.reason()
repo_path="runs/current/ledger")
print(guarded.result.final_reward, guarded.decision.category)
print(guarded.notes)
- The kill switch is checked before composing. This is a pre-flight check: an engaged switch raises rather than silently starting a session, but a switch engaged mid-run is not polled by this wrapper.
- The supplied acceptance is injected into the composed
Policies. When the spec has no acceptance policy, the guard is chained before the shippedDefaultAcceptancerather than replacing it. When the spec has its own policy, that policy is the second link. Guards can only make acceptance stricter, never remove the statistical gate. bind/configureand the statefulcheckpoint_state/restore_state/on_screen_rejecthooks are forwarded through the chain.- A
decision_logreceives onekind="run"record with final reward, outcomes, rounds, error, acceptance names, calls/tokens/wallclock.acceptedmeans a candidate was actually committed; a run that made no commits is not recorded as an acceptance. A failing log does not hide the completed run: the note says the log failed andGuardedRun.resultis still returned. - An algorithm
aggregator_factoryowns its merge path; the injected acceptance is then not consulted, andGuardedRun.notessays so.
The runtime is deliberately duck-typed: it does not import a specific guard, log or switch, so those can evolve independently and a third-party implementation is first-class.