Runtime metrics¶
RunAgent publishes Beam user metrics under the namespace beam_agents.runtime.
They surface wherever your runner surfaces user metrics: the Dataflow job page,
the Flink metrics reporter, or PipelineResult.metrics() on the DirectRunner.
from apache_beam.metrics.metric import MetricResults, MetricsFilter
result = pipeline.run()
result.wait_until_finish()
query = result.metrics().query(MetricsFilter().with_namespace("beam_agents.runtime"))
{m.key.metric.name: m.result for m in query[MetricResults.COUNTERS]}
Nothing needs enabling: metrics are published unconditionally, with no
configuration knob. (AgentConfig.tool_registry supplies the tools
ctx.run_tool executes — it configures tool execution, not metrics.) Working
memory's own beam_agents.memory/soft_cap_warnings counter is unchanged and
keeps its separate namespace.
Counters¶
| Counter | Incremented |
|---|---|
activations |
Once per activation that reached the commit path — a start or a resume. Same event as the SEQ increment. |
llm_calls |
Once per model call that reached the provider. A replay-cache hit is not a call. |
tool_calls |
Once per read-only tool executed inline via ctx.run_tool(...), on either activation surface. The tools come from AgentConfig.tool_registry. |
intents_emitted |
Once per ToolIntent put on .intents, including one minted by a HITL escalation. |
agent_errors |
Once per .errors record that is not an orphaned result (activation_timeout, activation_error, budget_exceeded, hitl_timeout, ttl_wiped_suspension, ttl_wiped_batch, batch_buffer_overflow). |
suspensions |
Once per committed activation whose outcome was Suspend. |
orphaned_results |
Once per .errors record with reason orphaned_result. |
longterm_upserts |
Once per long-term memory row flushed through the MemoryStore in a committed activation's commit tail (docs/memory.md). A failed activation flushes nothing and a failed flush fails the activation, so this only counts durable writes on the committed path. |
events_buffered |
Once per event appended to a key's adaptive-batching buffer (docs/batching.md). Zero under the default BatchPolicy.NONE. |
batch_flushes_size |
Once per committed flush that the max_batch_size threshold triggered. |
batch_flushes_timer |
Once per committed flush that the max_wait_ms FLUSH_TIMER triggered. |
Three identities hold by construction, and are worth alerting on if they break:
intents_emittedequals the element count on.intents.agent_errors + orphaned_resultsequals the element count on.errors.batch_flushes_size + batch_flushes_timerequals thebatch_sizesample count, and each committed flush counts as exactly oneactivation.
Distributions¶
Beam distributions are integer-only and report sum, count, min, max — there are no percentiles. (Beam's histogram cell is marked internal-use-only, so a portable percentile is not available; percentile work belongs to the trace sink or the benchmark suite.)
| Distribution | One sample per |
|---|---|
activation_ms |
Agent run, including failures and timeouts. Sample count is therefore activations + failed activations. A resume refused at admission never runs the agent and is not sampled. |
overhead_ms |
Committed activation: its wall time minus its model-call and inline-tool time, clamped at zero. This is the release-gate figure (the budget excludes LLM/tool time). Sample count equals activations; a failed activation's tally does not escape, so failures contribute activation_ms only. |
llm_ms |
Provider-reached model call. Sample count equals llm_calls. |
tokens |
Committed activation whose provider usage was actually decoded. Activations that decoded no usage contribute no sample, so the count means "activations with known usage". |
prompt_tokens |
The same activations, summed input tokens. Same sampling rule as tokens, so the three counts move together. |
completion_tokens |
The same activations, summed output tokens. Input and output are priced differently by every provider, so the split — not the total — is what a price sheet multiplies. |
memory_bytes |
Committed activation: the working-memory size that was committed. |
iterations |
Committed activation: the agent steps it consumed. A resume reports only its own steps. |
batch_size |
Committed batch flush: how many events it activated over. The mean is the batching ratio — how many events one activation (and one set of model calls) covered. No samples under BatchPolicy.NONE. |
The cost distributions vs. the token budget¶
tokens/prompt_tokens/completion_tokens are billed: they are fed only by
provider-reached calls, exactly as the model facade has always accumulated
usage, so a replayed activation whose calls all came out of the replay cache
records nothing at all.
AgentConfig.max_tokens_per_activation (docs/errors.md) meters
something different on purpose. It charges every response the agent consumes,
replay-cache hits included, because it is a decision rather than a
measurement: the budget check is a branch taken upstream of every intent the
activation mints, and provider-reached-ness is exactly the property a bundle
retry does not preserve. Charging billed tokens would make a retried walk take a
different branch than the original and break the byte-identical-intents
guarantee.
So the two are allowed to disagree, and on a replayed walk they do: the budget charges N, these distributions record nothing. That disagreement is the replay cache working, not a bug in either.
activation_ms vs. overhead_ms¶
activation_ms is total wall time and includes provider and tool latency —
useful for end-to-end latency questions, wrong for the release gate. The
release-gating budget (p50 < 15 ms, p99 < 60 ms per activation) excludes LLM and
tool time; overhead_ms publishes exactly that subtraction, so it is the
distribution to alert on for the budget. It is clamped at zero: an agent that
awaits calls concurrently can make summed call time exceed wall time. Beam
distributions carry no percentiles (see above), so the p99 check itself is
rendered by the benchmark suite (docs/benchmarks.md — its
overhead_*ms tiers record the same subtraction per activation and its gate
enforces the p50/p99 budget); overhead_ms's sum/count/max give the
dashboard-level early warning.
What these numbers are, and are not¶
Attempted, not committed. Most runners report attempted metric values, so
a bundle that fails and is retried re-applies its increments even though its
state and outputs roll back. These counters are telemetry, not an accounting
ledger: nothing in the runtime reads one back, and the authoritative record of
what happened is .traces, .intents, and .errors. Effectively-once
execution is the effector's intent_id dedup, not a counter.
No labels. Beam user metrics carry no dimensions, so there is no per-tool,
per-model, or per-error-reason breakdown here. Those dimensions are on every
TraceEvent (gen_ai.request.model, beam_agents.cache_hit, …) and on every
ActivationError (reason), which is where dimensioned analysis belongs.
Cache-hit ratio is a trace question. llm_calls deliberately counts only
provider-reached calls, which is what makes "a replayed bundle adds zero
provider calls" visible on a dashboard. The hit/miss split lives on the
LLM_CALL traces' beam_agents.cache_hit attribute.
A DirectRunner caveat for local runs¶
_AgentDoFn declares a REAL_TIME timer (the HITL deadline), which rules out
Beam's FnApiRunner, so every RunAgent pipeline runs on the classic
DirectRunner. Its metrics implementation reports one bundle's updates and drops
the rest, so a local multi-bundle run (several TestStream groups, or a
timer-fired bundle) under-reports. This is a runner artifact — a plain
beam.ParDo counter shows the same thing — not a property of the metrics
themselves. Dataflow and Flink aggregate normally.