Skip to content
beam-agents
GitHub

Compared with running a framework yourself

What the runtime provides that an agent framework plus a job scheduler does not — and where the honest answer is "you may not need this".

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

The realistic alternative to this project is not another runtime. It is a LangGraph app in a container, consuming from a queue, with state in Redis. That works, and for a lot of workloads it keeps working. This page is about the specific failures it has, and when they matter enough to justify a pipeline.

Two arrangements of the same workTwo stacked panels. Beside the runtime: a queue that redelivers on failure feeds a worker process holding the agent and its framework, and the worker forks into a state store that commits separately and an external API where the effect runs. A rectangle labelled retry boundary encloses all three, so a redelivery re-runs the worker, the state write and the API call. Inside the runtime: events feed the RunAgent transform, which stages every effect and forks into keyed state that commits with the bundle and an intents output. A rectangle labelled retry boundary encloses the transform and its keyed state only. The intents output leaves that rectangle to an outbox topic that deduplicates by intent id, and then to an effector — a separate service, outside the boundary, where the effect runs. Its result re-enters on the same key.Beside the runtimeA REDELIVERY RE-RUNS EVERYTHING INSIDE THE BOUNDARYqueueREDELIVERS ON FAILURERETRY BOUNDARYworker processAGENT AND FRAMEWORKstate storeSEPARATE COMMITexternal APIEFFECT RUNS HEREInside the runtimeNOTHING INSIDE THE BOUNDARY APPLIES UNTIL THE BUNDLE COMMITSeventsKEYED BY ENTITYRETRY BOUNDARYRunAgentSTAGES EVERY EFFECTkeyed stateCOMMITS WITH THE BUNDLE.intentsoutbox topicDEDUP BY INTENT_IDeffectorEFFECT RUNS HERERE-ENTERS ON THIS KEY
Same work, same shape, one difference: in the upper panel the retry boundary contains the external effect, and in the lower one the effect is the single thing left outside it.

The four problems this exists to solve

1. A retried unit of work re-executes its effects

A worker crashes after calling the payments API but before acknowledging the message. The message is redelivered. The refund goes out twice.

The usual mitigations are an idempotency key you generate and store, or an inbox table. Both work; both are yours to build and to get right.

Here, the identity is structural rather than generated: the intent id is uuid5(namespace, key|seq|step_index), a pure function of the activation's position. A replayed bundle walking the same path mints the same id, and the effector deduplicates on it.

async def refund(ctx: ActivationContext) -> Complete | Suspend:
    """Request a refund, then report what the effector did.

    Two activations, one logical unit of work. `ctx.act` stages the intent and
    returns its deterministic id; `Suspend` persists the continuation and arms
    the fail-closed timeout. Nothing has been written to the outside world when
    this function returns — the intent is a request, not an effect.
    """
    if not ctx.is_resume:
        ctx.act("payments.refund", '{"amount": 4200}', ttl_ms=INTENT_TTL_MS)
        return Suspend(snapshot=b"awaiting-refund", adapter="example", timeout_ms=30_000)

    # On resume the same activation continues: same key, same seq, and the
    # snapshot it persisted is available as ctx.snapshot.
    assert ctx.resume_result is not None
    return Complete(output=b"refunded:" + ctx.resume_result.payload)
website/examples/intents_and_resume.py (region: agent) — executed by the repository’s offline test tier.

The guarantee is bounded and stated as such: duplicates are confined to the crash window between a tool's effect and its durable completion record, and true exactly-once still requires the tool to be idempotent on intent_id.

2. A retried unit of work re-bills its model calls

Fewer people think about this one. If the crash above happened after three model calls, the retry pays for three more — and if the model answers differently the second time, the retry takes a different path, which can undo the reasoning that led to the effect you already performed.

The replay cache keys every call on the request content plus (key, seq) and stores it in keyed state. A bundle retry costs zero additional provider calls on the cached path, and takes the same path.

3. Partial state after a failure

The framework wrote to memory, then failed. Now the agent's state reflects half of a decision that never completed.

Every effect an activation produces is staged and applied only on success. A failed activation mutates nothing — no memory write, no intent, not even the sequence counter. You can see it from outside: a failed activation's staged traces are discarded with everything else, leaving a single ERROR event where a committed one emits a full span set.

4. Concurrency on the same entity

Two events for the same account arrive at once and two workers process them concurrently against shared state. The usual fix is a distributed lock.

Beam serializes elements per key. There is no lock because there is no concurrency to guard — parallelism comes from having many keys, not many workers per key.

When you do not need this

Directly and without hedging:

  • A human is waiting. This is designed for system-triggered agents, not sub-second interactive chat. If someone is watching a text box, use something else.
  • Your effects are already idempotent. If every write is an upsert keyed on something stable, problem 1 is already solved and problem 2 is a cost question rather than a correctness one.
  • Your volume is small. A pipeline is real operational weight — a runner, a message bus, an effector service. Below some throughput, a container and a queue is the right engineering call.
  • You need it in production now. This project is unreleased. That is a serious argument against it, and it should be weighed as one.

What you give up

  • Python only, for v0.x.
  • A runtime dependency on Beam, including its Python SDK's constraints — no MapState, no portable async DoFn, stateful DoFns require KV input.
  • The effector is a separate service you deploy and operate. Effects happening outside the pipeline is what makes the guarantee possible, and also what makes it another thing to run.
  • A young API. Writing an agent currently requires importing from modules the project documents as private. See the API reference.

What backs this page

Symbol
beam_agents.RunAgent
Source
src/beam_agents/core/dofn.py
Source
src/beam_agents/model/replay_cache.py
Specification
openspec/specs/llm-replay-cache/spec.md
Test
tests/semantics/test_retry_determinism.py
Test
tests/core/test_dofn_commit.py
Example
intents_and_resume.py

Cited sources

  1. LangGraph is an agent-authoring framework maintained by LangChain, used here only as a stand-in for "the framework you already run".

    https://github.com/langchain-ai/langgraph — retrieved

  2. Apache Flink Agents is a separate comparable project, covered on its own page.

    https://github.com/apache/flink-agents — retrieved