Network
Where Agent Telemetry traces what happens inside one agent, network telemetry traces what happens between them — the hub's envelope dispatch, channel lifecycles, agent registration, and tasks. It is emitted by the hub itself, as OpenTelemetry spans on the same TracerProvider your agents use, so the two stitch together into one picture.
Like agent telemetry it is opt-in: nothing is traced unless you hand the hub a TracerProvider.
Tip
Network telemetry composes with Agent Telemetry. When an agent's turn is triggered by a network envelope, its invoke_agent span links back to that envelope's span — so a trace can follow a message from dispatch, into the receiving agent's LLM calls and tools, and back out.
Installation#
Quick Start#
Two opt-ins wire up the full picture: pass a tracer_provider to Hub.open (the hub owns envelope spans), and register a HubTelemetryListener (it owns channel, agent, and task spans). Give each agent a TelemetryMiddleware so its turn spans join the same provider.
What Gets Traced#
The hub and listener emit five span types. Each carries an ag2.span.type attribute, mirroring the agent-side convention.
ag2.span.type | Span name | Emitted by | Lifetime |
|---|---|---|---|
envelope | network.envelope {event_type} | Hub | One per dispatched envelope. Brackets WAL append + dispatch; root of its own trace (SpanKind.PRODUCER). |
channel | network.channel {type} | listener | Long-lived: opened on created, ended on closed / expired. Channel-scoped signal events attach here. |
agent_lifetime | agent.lifetime {name} | listener | Long-lived: opened on registered, ended on unregistered. |
agent_event | agent.resume_set / agent.skill_set / agent.rule_set | listener | Single-shot identity-change span, nested under the agent's agent.lifetime. |
task | network.task {capability} | listener | Single-shot, created at a task's terminal event with its start time backdated to when the task began. |
Agents instrumented with TelemetryMiddleware add their own agent / llm / tool / human_input spans (see Agent Telemetry), linked to the triggering envelope.
Note
network.task spans cover capability-tagged tasks — the same agent.task(..., capability="X") mechanism described in Task Observation. The span records the task's capability, outcome, and latency, which is what feeds task-level evaluation views.
Checkpoint events#
Task checkpoints are written straight to the hub's store, so they bypass the envelope path and are not their own spans. Instead, HubBackedCheckpointStore pins them as span-events on whatever span is active when a checkpoint is saved or loaded — typically the owning agent's turn span:
| Event | Attributes | Meaning |
|---|---|---|
checkpoint.write | ag2.checkpoint.task_id, ag2.checkpoint.bytes | Owner saved a resume snapshot. |
checkpoint.read | ag2.checkpoint.task_id, ag2.checkpoint.hit | Snapshot loaded; hit=false when none existed. On a resume read, task_id is the prior task being resumed from — the link back to the original run. |
These are no-ops when no tracer is configured, so checkpointing carries no tracing cost unless you opt in.
Bounded Traces, Joined by Links#
Rather than forcing everything onto one enormous hub-wide trace, each entity is its own bounded trace — an envelope, a channel, an agent's lifetime, and a task each root a separate trace. This matches how OTLP collectors and trace viewers expect to ingest data (a long-lived channel that stays open for hours does not hold one trace open indefinitely).
The causal relationships between these traces are expressed two ways:
-
SpanLinks connect related spans across traces:Link From → To Meaning in_channelenvelope → channel the envelope was dispatched in this channel triggered_byenvelope → caller's span the envelope was posted by this work in_channeltask → channel the task ran in this channel -
Resource attributes (
service.instance.id,ag2.hub.id) stamp every span the hub emits, so all of a hub's traces are joinable in your backend without a shared parent span.
Connecting agent turns to envelopes#
You do not wire this up yourself — adding TelemetryMiddleware to your agents is enough. Under the hood, the hub injects the network.envelope span's W3C traceparent into the envelope before it is written to the WAL (so the write-ahead log is the source of truth), and the receiving agent's middleware extracts it to parent the invoke_agent span. The result: a continuous trace from dispatch into the recipient's work.
What a Single Exchange Produces#
To make the model concrete, here is the exact output of one consulting exchange — a 1-question / 1-reply channel where alice asks bob, both agents instrumented with TelemetryMiddleware. It yields five bounded traces (indentation = parent → child within one trace):
network.channel consulting # the channel: open → auto-close
# ← linked from each envelope below
network.envelope ag2.msg.text (alice → bob) # dispatch of alice's question
└─ invoke_agent bob # bob's turn, triggered by the question
└─ chat # bob's LLM call
# → in_channel link to network.channel
network.envelope ag2.msg.text (bob → alice) # dispatch of bob's reply
# → in_channel link to network.channel
agent.lifetime alice # alice: registered → unregistered
agent.lifetime bob # bob: registered → unregistered
What to read off this:
- Each entity is its own trace. The channel, each envelope, and each agent's lifetime are separate bounded traces — not one giant tree — joined by SpanLinks and the shared
ag2.hub.idresource attribute. - The recipient's work nests under the triggering envelope.
invoke_agent bob(and itschat, plus anyexecute_tool) are children of the question envelope, because the traceparent rides that envelope into bob's turn. One trace follows the message from dispatch into the recipient's LLM call. - The reply is its own envelope trace (bob → alice) — the hub starts a fresh
network.enveloperoot for every dispatched message. - Lifetime spans appear only once the agent is unregistered — they stay open from
registereduntilshutdown()(see Getting a complete trace set).
It scales predictably: N messages on a channel → N network.envelope traces; each agent turn adds an invoke_agent (with its chat / execute_tool children) under the envelope that triggered it; one network.channel per channel; one agent.lifetime per registered agent.
Two Sinks: Collector and Disk#
Every span the hub and listener emit goes to two places:
- The OTLP collector — via the
TracerProvideryou configured, exactly like agent telemetry. Any OTLP-compatible backend works. - Disk — each ended span is also mirrored as one JSON object per line to
/telemetry/spans.jsonlinside the hub'sKnowledgeStore. This is hub-private storage (agents cannot read it), it survives restarts, and it can be queried offline without a running collector.
The disk records use the same hex trace/span IDs as the OTLP export, so a span on disk and the same span in your backend cross-reference one another.
Span Attributes#
Beyond the resource attributes, spans carry ag2.network.* and ag2.agent.* attributes you can filter and aggregate on:
| Attribute | Span types | Description |
|---|---|---|
ag2.network.channel_id | envelope, channel, task | Channel the span belongs to |
ag2.network.event_type | envelope | Envelope event type (e.g. ag2.msg.text) |
ag2.network.sender_id | envelope | Sender agent ID |
ag2.network.envelope_id | envelope | Envelope ID |
ag2.network.audience | envelope | Recipient agent IDs the envelope was addressed to |
ag2.network.causation_id | envelope | ID of the envelope that caused this one (reply chains) |
ag2.network.dispatch_failures | envelope | Count of failed deliveries — set only when non-zero; also flips span status to ERROR |
ag2.network.manifest_type | channel | Channel type (e.g. workflow, discussion) |
ag2.network.creator_id | channel | Agent that opened the channel |
ag2.network.capability | task | The task's declared capability |
ag2.network.outcome | task | Terminal outcome (e.g. completed, expired, cancelled) |
ag2.network.task_id | task | Task ID |
ag2.network.owner_id | task | Agent that owns the task |
ag2.agent.id | agent_lifetime, agent_event | Agent ID |
ag2.agent.resume_source | agent_event | tenant (set by an operator) or observed (earned via a task) |
ag2.agent.capability | agent_event | Capability observed (on an observed resume update) |
ag2.agent.outcome | agent_event | Task outcome that drove the observation |
ag2.agent.skill_removed | agent_event | true when a skill_set event removed (rather than set) the skill |
Channel spans also record diagnostic span events — dispatch_failed (carrying ag2.network.recipient_id + ag2.error.type / ag2.error.message), turn_failed, envelope_rejected, and expectation violations — and agent.lifetime spans record inbox_pressure (ag2.inbox.pending / ag2.inbox.cap), so failures and back-pressure surface in the trace without their own spans.
The network.envelope span reports ERROR when delivery fails: per-recipient delivery failures set ag2.network.dispatch_failures, and an exception that escapes dispatch (for example, a custom HubArbiter error) is recorded on the span as an exception event.
Owner code that checkpoints a task adds checkpoint.write / checkpoint.read span events on the active span — see Checkpoint events above.
Custom Span Attributes#
HubTelemetryListener accepts a span_attributes dict, stamped onto every channel / agent / task span it emits — the same pattern as the agent middleware, useful for tenant or environment labels.
Tip
For hub-wide identity that should appear on all spans (including the hub-owned network.envelope spans), prefer resource attributes on the TracerProvider (service.instance.id, ag2.hub.id) over span_attributes. Resource attributes stamp every span automatically.
Getting a Complete Trace Set#
Long-lived spans only export when they end. agent.lifetime spans end when an agent is unregistered, and network.channel spans end when a channel closes.
HubClient.shutdown()unregisters the client's agents — ending theiragent.lifetimespans.HubClient.close()only drops the connection; registrations (and their lifetime spans) stay open.
So for a clean, complete trace set at the end of a run, call shutdown() (not just close()) on your hub clients, then provider.shutdown() to flush the exporter.
Configuration#
HubTelemetryListener accepts:
| Parameter | Type | Default | Description |
|---|---|---|---|
store | KnowledgeStore | required | The hub's store; span records are appended to /telemetry/spans.jsonl |
tracer_provider | TracerProvider \| None | Global provider | OpenTelemetry TracerProvider; share it with the hub and agents |
span_attributes | dict[str, str] \| None | None | Extra key-value pairs stamped onto every channel / agent / task span |
To enable hub-owned network.envelope spans, pass the same tracer_provider to Hub.open(store, tracer_provider=...).
Backend Integration#
Network telemetry emits standard OpenTelemetry spans, so any OTLP-compatible backend works — Grafana Tempo, Jaeger, Datadog, Honeycomb, Langfuse, and others. Swap the ConsoleSpanExporter for an OTLPSpanExporter pointed at your backend or a local OpenTelemetry Collector; the hub and agents share the one provider, so all their spans flow to the same place.