Skip to content

keelgate.loop

Stability of each symbol (stable or provisional) is listed in the API contract. Narrative and semantics are in the integration contract.

Durable, budgeted agent loop with explicit stop conditions and resume.

AcceptAllVerifier

Accepts everything. For tests and loops where nothing needs checking.

ActionOutcome

Bases: BaseModel

What happened to one planned action: status, error code and fixed-wording detail, and the tool output as untrusted JSON text.

ActionStatus

Bases: StrEnum

Where a planned action stands: pending, done, denied, error, awaiting approval, unknown (may or may not have happened) or abandoned.

CallableVerifier

Wraps a plain function (sync or async) returning a Verdict, a decision or a bool.

CheckpointStore

Bases: Protocol

Where loop state is saved after every step.

save is atomic and refuses a sequence number that is not newer, so a stale writer cannot overwrite a newer checkpoint; load returns the latest state.

load

load(tenant_id: str, run_id: str) -> LoopState | None

The latest state, or None. A run in another tenant is simply absent.

save

save(state: LoopState) -> None

Persist state atomically. Raises :class:StaleCheckpointError if it is not newer.

ContextSource

Bases: Protocol

Supplies extra context for a plan step, such as retrieved memory.

Items must be dated (published_at) and untrusted; the builder enforces both.

Divergence dataclass

One difference between a recorded run and its replay: where, what was expected and what was seen.

GroundedAnswerVerifier

A deterministic verifier: is the final answer supported by what the tools returned?

Two rules, both cheap and both explainable:

  • Grounded figures. Every number in the answer (other than a small whole number such as "3") must appear in the goal or in a tool observation. A fabricated or transposed price has nowhere to come from.
  • Honest claims. If write_tools is given and the answer says something was bought, sold, placed or executed (and does not say it did not), at least one of those tools must have actually completed.

It cannot judge meaning, so it is a floor under a model-based verifier, not a replacement. It never authorises anything: only the gateway does that.

HistoryCheckpointStore

Bases: CheckpointStore, Protocol

A store that keeps every checkpoint, oldest first. Replay needs this.

InMemoryCheckpointStore

Process-local. Fine for tests; it does not survive the process, so it cannot resume.

InProcessRunner

Runs the loop in the calling process.

LLMPlanner

Plans by function calling: tool calls are actions, plain text is a final answer.

LLMVerifier

Asks a model for a JSON verdict. Anything it cannot parse is a rejection.

Loop

The durable agent loop: plan, act, observe, verify, then revise or stop.

Checkpoints every step, honours the stop conditions, and resumes without re-planning or repeating a completed WRITE.

reconcile async

reconcile(
    tenant_id: str,
    run_id: str,
    action_id: str,
    *,
    executed: bool,
    by: str,
    note: str = "",
) -> LoopState

A human settles an action whose outcome was unknown.

executed=True records it as done; executed=False abandons it. Either way the action is never run again. If it must be attempted afresh, plan a new action with a new idempotency key.

resume async

resume(
    tenant_id: str,
    run_id: str,
    *,
    stop: StopConditions | None = None,
) -> LoopResult

Continue a stopped or interrupted run from its latest checkpoint.

Pass a new stop to continue after a budget stop with a larger budget.

run async

run(
    *,
    goal: str,
    tenant_id: str,
    agent_id: str,
    as_of: datetime,
    run_id: str | None = None,
    trace_id: str | None = None,
    stop: StopConditions | None = None,
) -> LoopResult

Start a new run. Refuses an id that already has a checkpoint.

run_or_resume async

run_or_resume(
    *,
    goal: str,
    tenant_id: str,
    agent_id: str,
    as_of: datetime,
    run_id: str,
    trace_id: str | None = None,
    stop: StopConditions | None = None,
) -> LoopResult

Idempotent start: resume the run if it exists, otherwise begin it.

LoopOutcome dataclass

The serialisable result of a run.

LoopResult dataclass

What a Loop run or resume returns.

The final state, why it stopped, whether it reached the goal and whether it can be resumed.

LoopRunner

Bases: Protocol

Starts a run, or resumes it if a checkpoint exists. Safe to call again after a crash.

LoopSpec dataclass

Everything needed to start or resume one run, as plain data.

LoopState

Bases: BaseModel

Everything a run needs to resume: the goal, the phase, the planned actions and their outcomes, usage, and the trace identity.

This is what is checkpointed.

LoopType

Bases: StrEnum

The kind of run: a TASK, a read-only VERIFICATION, or a MONITOR tick.

MonitorLoop

Runs a bounded loop on a schedule, finishing a half-done tick after a restart.

Each tick is its own run, named <monitor_id>.tick-<n>, so ticks are checkpointed and resumable independently. The next tick number comes from what is already stored: a restarted monitor carries on rather than starting over.

loop_factory is called as loop_factory(loop_type=..., allowed_side_effects=...) and must pass both through to the :class:Loop it builds.

run_ticks async

run_ticks(count: int) -> MonitorSummary

Run count ticks, sleeping schedule.interval between them.

tick async

tick() -> LoopResult

Run, or finish, exactly one tick.

MonitorSummary dataclass

The outcome of a bounded monitoring session: how many ticks ran and each tick's result.

NotReplayableError

Bases: Exception

The recording lacks a recorded result for some action, or the run cannot be found.

OutcomeConfirmer

Bases: Protocol

Settles an action whose outcome is unknown by asking the downstream system of record.

Return True if the action took effect, False if it definitely did not, and None when the answer is not certain (the loop then stops for a human, as before).

Phase

Bases: StrEnum

Where a run is in its cycle: plan, act, observe, verify, or done.

Plan dataclass

What the planner proposed: actions to run, or a final answer, never both.

PlanRequest dataclass

What the planner is given for one planning call.

The run, the iteration, the built context messages and the tools it may propose.

PlannedAction

Bases: BaseModel

One tool call the planner chose, written down before it runs so a resume re-submits the same arguments.

Planner

Bases: Protocol

Proposes the next step: tool calls to run, or a final answer.

A planner is untrusted; its proposals go through the gateway like any other.

ProposedAction dataclass

One tool call the planner proposes: a tool name, untrusted arguments and a rationale.

A proposal only; the gateway decides.

RecordedAction dataclass

One tool call as recorded in a run: name, arguments, final status and outcome.

RecordedStep dataclass

One iteration of a recorded run: the actions proposed, the final answer if any and the verdict.

Recording dataclass

A run rebuilt from its checkpoint history, ready to replay.

Find one by trace id with Recording.from_store.

from_store classmethod

from_store(
    store: CheckpointStore,
    tenant_id: str,
    *,
    trace_id: str | None = None,
    run_id: str | None = None,
) -> Recording

Rebuild a run by trace id (or run id) from a store that keeps history.

ReplayReport dataclass

The result of a replay: the original and replayed recordings and every divergence.

identical is true when there is none.

RunExistsError

Bases: Exception

run was asked to start a run id that already has a checkpoint.

RunNotFoundError

Bases: Exception

resume found no checkpoint for that tenant and run id.

Schedule dataclass

A fixed interval between monitoring ticks.

SqliteCheckpointStore

Durable, dependency-free. Keeps every checkpoint, so a run's history can be inspected.

StaleCheckpointError

Bases: Exception

A newer checkpoint already exists: another writer got there first.

StopConditions dataclass

Limits for one run.

max_iterations counts plan steps. max_tokens and max_dollars count what the model has already used, so a call that crosses the line is accepted, but nothing further is paid for or acted on. timeout is active time: time spent stopped, for example waiting for a human, does not count. max_verifier_rejections counts every non-ACCEPT verdict. goal is checked after each observation.

A dollar budget needs a priced model; with an unpriced one the loop stops with COST_UNKNOWN instead of spending unmetered.

StopReason

Bases: StrEnum

Why a run stopped: the goal was reached, a limit was hit (iterations, tokens, dollars, time), the verifier rejected too often, an outcome is unknown, an approval is pending, or an error.

Verdict dataclass

A verifier's judgement of a proposed final answer.

Accept, revise or reject, with reasons and flags. It steers the loop but can never authorise an action.

VerdictDecision

Bases: StrEnum

A verifier's answer: ACCEPT, REVISE or REJECT.

VerificationLoop

Bases: Loop

A loop that can only read. Asking for more than READ is an error, not a warning.

Verifier

Bases: Protocol

Judges a proposed final answer.

Its verdict steers the loop but can never authorise an action. A verifier that cannot be understood counts as a rejection.

VerifyRequest dataclass

What the verifier is given.

The goal, the proposed answer, the actions taken this step and the (untrusted) observations.

default_checkpointer

default_checkpointer(
    path: str | Path = ":memory:",
) -> CheckpointStore

A durable store: LangGraph's SQLite saver if installed, else the standard-library one.

The choice depends on what is installed, so pass an explicit store anywhere the two must agree. Both hold the same JSON and a run can be moved between them.

diff

diff(
    original: Recording, replayed: Recording
) -> list[Divergence]

Compare two recordings step by step and return every divergence (empty when they match).

find_run

find_run(
    store: CheckpointStore, tenant_id: str, trace_id: str
) -> str | None

The run id whose trace id is trace_id within one tenant (never across tenants).

replay async

replay(recording: Recording) -> ReplayReport

Replay a recording through a fresh loop and report any divergence.