Skip to content
beam-agents
GitHub

Correctness invariants

The seven rules the implementation is held to, what each one buys, and where each is tested.

StableImplemented, specified, and covered by tests in the repository.

These seven rules are treated as blocking defects when violated, not as design preferences. Each one is stated here with what it buys you and where it is tested, so you can check the claim rather than take it.

1. Atomic commit

State mutations and emitted outputs commit atomically with the Beam bundle. Every effect an agent produces — memory writes, cache inserts, intents, traces, outputs — is staged in the activation context and applied only on success. A failed or timed-out activation mutates nothing.

How an activation commitsFive kinds of effect — memory writes, replay-cache inserts, intents, traces and outputs — are staged inside the activation context rather than written as the agent produces them. All five meet one all-or-nothing gate. If the activation returns, the runtime writes keyed state in a fixed order — memory, LLM cache, continuation, pending intents, then seq plus one — and emits on .output, .intents and .traces, including the activation's full set of trace spans. If it raises or times out, every staged effect is discarded: no keyed state is written, seq does not advance, and nothing reaches .output or .intents. The only two records that leave are one ActivationError on .errors and one ERROR event on .traces, both synthesized by the runtime from the key, the sequence number and the failure reason.STAGED IN THE ACTIVATION CONTEXTmemory writesreplay-cache insertsintentstracesoutputscommit gateALL OR NOTHINGIF IT RETURNSIF IT RAISES OR TIMES OUTcommit to keyed stateMEMORY LLM_CACHE CONTINUATION PENDING SEQ+1.output.intents.tracesA FULL SPAN SETdiscard everythingNO STATE WRITTEN — SEQ DOES NOT ADVANCE.errorsONE ActivationError.tracesONE ERROR EVENTNOTHING REACHES .output OR .intents
Nothing is written as the agent produces it. Either everything the activation did lands together, or none of it does and the failure is visible only as the two records on the bottom row — never as a partial write.

You can watch this from outside. In the four-outputs example one key raises after writing memory; that write never lands, and the activation's staged traces are discarded with it, leaving a single ERROR trace where a successful activation emits a full span set:

async def route_by_event(ctx: ActivationContext) -> Complete:
    """Take a different path per event so one pipeline exercises each output."""
    if ctx.event == b"BROKEN":
        # Routed to `.errors` as `activation_error`. Nothing this activation
        # staged — including this memory write — reaches durable state.
        ctx.memory.set("scratch", b"never-persisted")
        raise RuntimeError("downstream schema changed")

    if ctx.event == b"NOTIFY":
        ctx.act("slack.post", '{"channel": "#ops"}', ttl_ms=INTENT_TTL_MS)
        return Complete(output=b"notified")

    response = await ctx.call_model(
        LlmRequest(
            model_id="fake-1",
            messages=[ctx.event.decode()],
            tools_schema=None,
            sampling_params=None,
        )
    )
    return Complete(output=response.response)
website/examples/four_outputs.py (region: agent) — executed by the repository’s offline test tier.

Tested by tests/core/test_dofn_commit.py.

2. Deterministic intent IDs

intent_id = uuid5(NAMESPACE, f"{entity_key.hex()}|{seq}|{step_index}")

A pure function of the activation's position. Never a clock, never a counter, never randomness. A replayed bundle that walks the same path produces byte-identical intents, and the effector deduplicates on intent_id.

This is the entire effectively-once argument. Not an at-least-once delivery guarantee plus hope — a deterministic identity that makes duplicate suppression a lookup.

Where an intent id comes fromAn intent id is derived from three values that describe where the activation is, not when it ran: the entity key, seq (how many activations have committed for that key), and step_index (the position of this step inside the activation). They are formatted into the name entity_key.hex() pipe seq pipe step_index and hashed with uuid5 against a fixed namespace — a pure function with no clock, no counter and no randomness. A retried bundle for the same key, seq and step re-derives rather than remembers, so it produces byte-identical intents. The intent leaves on .intents through the outbox topic to the effector, which claims on the intent id and gets exactly one of three answers: Claimed, meaning it owns the execution and runs it once; InFlight, meaning another worker holds a live lease and it must wait; or Done, meaning a terminal record already exists, so it republishes the stored result and does not execute.PURE INPUTS — WHERE THE ACTIVATION IS, NOT WHENentity_key.hex()THE BEAM KEYseqCOMMITTED ACTIVATIONSstep_indexSTEP IN THIS ACTIVATION"{entity_key.hex()}|{seq}|{step_index}"THE UUID5 NAMEuuid5(NAMESPACE, name)NO CLOCK · NO COUNTER · NO RNGRE-DERIVED, NOT RECALLEDa retried bundleSAME KEY, SEQ, STEPintent_idBYTE-IDENTICAL ON A REPLAY.intentsOUTBOX TOPICeffectorDEDUP BY INTENT_IDClaimedEXECUTES ONCEInFlightWAITS FOR THE OWNERDoneREPUBLISHES, NO EXECUTION
Determinism is what makes duplicate suppression a lookup. Because the id is a function of position rather than time, the second delivery of the same work asks the same question and gets Done.

The id is computable ahead of time, which is how the re-injection example can name the intent it is about to receive a result for:

def _tool_result(key: bytes, intent_id: str, payload: bytes, t_ms: int):
    """One effector result, shaped as it arrives from the results topic."""
    env = AgentEnvelope(entity_key=key, event_time_ms=t_ms)
    env.tool_result.intent_id = intent_id
    env.tool_result.entity_key = key
    env.tool_result.payload = payload
    env.tool_result.status = ToolResult.OK
    return TimestampedValue(env, t_ms / 1000)
website/examples/intents_and_resume.py (region: result) — executed by the repository’s offline test tier.

Tested by tests/semantics/test_retry_determinism.py and tests/semantics/test_effectively_once_e2e.py.

3. Replay cache

Every model call is keyed by sha256(model_id, canonical_json(messages), tools_schema, sampling_params, key, seq) and cached in keyed state — LRU, 64 entries maximum, 6-hour TTL, 100 KiB blob cap.

The property that matters: a bundle retry incurs zero additional provider calls on the cached path. Without it, a retried bundle would re-bill every model call in the activation and — worse — could take a different path if the model answered differently the second time.

What a bundle retry costs on the cached pathA model call from inside the activation is turned into a cache key first: the sha256 of one canonical JSON document holding the model id, the messages, the tools schema, the sampling parameters, the entity key and seq. That key is looked up in the replay cache, which lives in keyed state and is bounded to 64 entries, a 6-hour TTL and a 100 KiB blob. A miss calls the provider and stages the response, which is committed to keyed state with the rest of the activation. A hit returns the stored response and makes no provider call. A retried bundle issues the same request for the same key and seq, so it recomputes the same cache key. Where that entry was already committed — by an earlier activation at the same seq, which is the cached path — the lookup hits and the provider is not called again.ctx.call_model(...)INSIDE THE ACTIVATIONsha256(canonical json)MODEL MESSAGES TOOLS PARAMS KEY SEQSAME REQUESTthe bundle retriesSAME KEY, SAME SEQreplay cacheIN KEYED STATELRU · 64 ENTRIES6h TTL · 100 KiB CAPHITcached responseNO PROVIDER CALLWHERE A RETRY LANDS —IF THE ENTRY ALREADY COMMITTEDMISScall the providerTHEN STAGE THE RESULTSTAGED — COMMITTED WITH THE BUNDLE
The retry re-enters at the key computation, not at the provider: same six components, same sha256. Where the entry was already committed, that second pass is a lookup rather than a call.

Tested by tests/model/test_replay_cache_hits.py and the retry-determinism gate, which asserts zero extra FakeLLM calls under forced bundle retries.

4. Per-key serialization

Beam stateful DoFns process one element at a time per key. Memory is race-free by construction — not by locking, and not by careful ordering in the agent code. Cross-key parallelism comes from the runner.

The consequence for you: never introduce cross-key shared mutable state. The only sanctioned exceptions are documented worker-local singletons (circuit breakers, the vLLM sidecar via beam.utils.shared.Shared).

5. Side effects only via intents

Calling a side_effect=True tool directly raises. ctx.act(...) is the only effect path. External writes never execute inside the pipeline.

try:
    freeze_account(customer_id="cust-9")
except SideEffectToolError as exc:
    refusal = str(exc)
else:
    raise AssertionError("a side-effect tool must refuse a direct call")
website/examples/read_only_tools.py (region: refusal) — executed by the repository’s offline test tier.

There is one documented exception: idempotent upserts to the long-term MemoryStore, keyed by (key, seq).

Tested by tests/tools/test_side_effect_guard.py.

6. Timeouts fail closed at both layers

A pending approval that never arrives has to end, and it has to end without letting a late answer cause an effect afterwards. Two independent guards:

  • Layer 1, in-pipeline. The HITL_TIMER fires and the policy's route runs — Deny, Drop, or Escalate.
  • Layer 2, in the effector. An intent past its expires_at_ms is refused rather than executed. A non-positive expiry reads as expired, never as unbounded: the safe reading of "no expiry recorded" is "do not execute".

Late results that find no live continuation are dropped to .errors as orphaned_result.

def deny_on_timeout(fallback: FallbackContext) -> Route:
    """Route an unanswered approval to a deterministic denial.

    Pure and synchronous. It reads only the `FallbackContext` it is handed —
    which carries the suspended `seq`, the persisted snapshot, the elapsed
    deadline, the timer's fire time, and the intent ids nothing answered — so a
    retried timer bundle reaches the same decision.
    """
    return Deny(output=b"denied:no-approval:" + str(fallback.seq).encode())
website/examples/human_in_the_loop.py (region: policy) — executed by the repository’s offline test tier.

The routing function must be pure, synchronous, and picklable. That is a correctness requirement: a timer callback re-executes when its bundle is retried, and a fallback that read a clock or called the model would make the retry diverge from the original. Every value the policy could need is carried on the FallbackContext.

Tested by tests/semantics/test_hitl_fail_closed.py.

7. State is protobuf, never pickle

All keyed state is protobuf with deterministic encoding. Pickle would make pipeline --update compatibility impossible to reason about and would tie the wire format to a Python version.

The upgrade rule follows from it: additive proto changes only. A breaking change requires a state_schema_version bump, lazy migration, and a golden-blob compatibility test.

Tested by tests/core/test_schema_compat.py and tests/core/test_coders.py.

How these are gated

The invariants are not checked by review alone. The repository runs a semantics test tier specifically for them — retry determinism under a chaos wrapper that forces bundle retries, effectively-once end to end against real Kafka/Redis/Flink with SIGKILLed effector workers and a killed TaskManager, and state compatibility against golden blobs. That tier gates every release and is never skipped or marked flaky.

See testing and CI for how the tiers are split and what runs where.

Watch them hold

Each invariant above is illustrated with a region of a real program. Those programs run offline on the DirectRunner, so the quickest way to check a claim is to run the one that demonstrates it:

What backs this page

Symbol
beam_agents.RunAgent
Source
src/beam_agents/core/dofn.py
Source
src/beam_agents/core/agent.py
Source
src/beam_agents/model/replay_cache.py
Source
src/beam_agents/hitl.py
Specification
openspec/specs/llm-replay-cache/spec.md
Specification
openspec/specs/wire-schemas/spec.md
Test
tests/semantics/test_retry_determinism.py
Test
tests/semantics/test_effectively_once_e2e.py
Test
tests/semantics/test_hitl_fail_closed.py
Test
tests/core/test_dofn_commit.py
Test
tests/core/test_schema_compat.py
Example
four_outputs.py