Skip to content

Hello, world

The smallest complete RunAgent pipeline: one AgentEnvelope created in-process, keyed by entity_key, processed by an agent that makes a single model call and completes. This is the fast path — the activation runs to completion inside one process() call, with no suspension, no side-effect intents, and no state carried between elements.

The model is a scripted [FakeLLM], so the pipeline runs offline with no API keys: one match_any() rule answers every request with the greeting. Swapping make_provider for a factory returning a real provider client is the only change a production pipeline needs.

Run it

uv run python -m examples.hello_world

It prints the single terminal output and exits:

b'Hello from the beam-agents runtime!'

The whole program

The code below is included verbatim from examples/hello_world.py — the same file tests/examples/test_hello_world.py executes in CI, asserting exactly what this page claims.

examples/hello_world.py
"""The smallest complete `RunAgent` pipeline: one event, one model call, one output.

One `AgentEnvelope` enters, keyed by `entity_key`; the agent awaits a single
model call (served by a scripted, offline `FakeLLM`) and completes. Everything
here is the fast path — no suspension, no side effects, no state carried
between elements.

Run it offline, with no credentials and no docker:

    uv run python -m examples.hello_world

Every function a pipeline references is module-level so the DirectRunner can
pickle it by reference.
"""

from __future__ import annotations

import apache_beam as beam

from beam_agents import AgentConfig, RunAgent, RunAgentOutputs
from beam_agents._protos import AgentEnvelope
from beam_agents.core.agent import Complete
from beam_agents.core.context import ActivationContext
from beam_agents.model.client import LlmRequest
from beam_agents.model.fake import FakeLLM, match_any, respond_with

# The scripted response, and therefore the pipeline's one documented output.
GREETING = b"Hello from the beam-agents runtime!"


def make_provider() -> FakeLLM:
    """A deterministic stand-in for a real provider: every request gets `GREETING`.

    Swapping this factory for one that returns a real `LLMClient` is the only
    change a production pipeline needs.
    """
    return FakeLLM([(match_any(), respond_with(GREETING))])


async def greeter(ctx: ActivationContext) -> Complete:
    """One model call, then complete: the whole fast path.

    `ctx.call_model` is cache-first — a retried bundle replays the cached
    response instead of paying for a second provider call.
    """
    response = await ctx.call_model(
        LlmRequest(
            model_id="fake-model",
            messages=[ctx.single_event.decode()],
            tools_schema=None,
            sampling_params=None,
        )
    )
    return Complete(output=response.response)


def build(pipeline: beam.Pipeline) -> RunAgentOutputs:
    """Wire the minimal pipeline: one envelope, keyed upstream, into `RunAgent`."""
    envelope = AgentEnvelope(entity_key=b"user-1", event_time_ms=1_000, external_event=b"hello")
    keyed = (
        pipeline
        | "OneEvent" >> beam.Create([envelope])
        # RunAgent takes a pre-keyed PCollection[KV[bytes, AgentEnvelope]]; the
        # caller keys by entity_key, exactly as the documented dataflow shape does.
        | "KeyByEntity"
        >> beam.WithKeys(lambda env: env.entity_key).with_output_types(tuple[bytes, AgentEnvelope])
    )
    return keyed | RunAgent(greeter, config=AgentConfig(provider_factory=make_provider))


def main() -> None:
    """Run the example pipeline, printing each decision to stdout."""
    with beam.Pipeline() as pipeline:
        outputs = build(pipeline)
        outputs.output | "Print" >> beam.Map(print)


if __name__ == "__main__":
    main()

What the output means

RunAgent exposes four named outputs. Here only the main one carries anything:

  • .output — exactly one element: the scripted response the agent returned via Complete(output=...).
  • .intents — empty: the agent staged no side effects.
  • .errors — empty: the activation committed successfully.
  • .traces — the activation's trace events (one LLM_CALL among them); see trace delivery.

The one subtlety worth taking away: ctx.call_model is cache-first. The response is staged in the keyed replay cache and committed atomically with the bundle, so if the runner retries this bundle the activation replays from cache with zero additional provider calls — the property the retry-determinism gate pins for every pipeline, demonstrated here at its smallest.