Instrumenting a Flue agent

@runtypelabs/flue-otel is OpenTelemetry instrumentation from Runtype for Flue agents. Install it with Flue’s instrument() function, configure an OTLP exporter for Runtype, and view each instrumented run in the dashboard. Runtype displays the model, token usage, cost, loop iterations, tool calls, and stop reason.

Your agent continues to run with your model credentials. Runtype provides the dashboard where you inspect the run.

Choose a path

Flue runs on several hosts, and the shortest route to a run in the dashboard depends on where your agent runs and what you need from it.

Your agentUse
Deployed to Cloudflare WorkersNo code. Export Workers traces to Runtype: Deploy on Cloudflare Workers.
On Node@runtypelabs/flue-otel, this guide. It sends the transcript, system prompt and tool content by default (each kind can be switched off), so runs can be captured as eval cases, and it reports an exact iteration count, a run-level stop reason, and summed usage.
On Node, stock instrumentation only@flue/opentelemetry. It exports the conversation by default, so runs carry a transcript and can be captured as eval cases. Runtype derives loop structure from Flue’s own attributes; the exact iteration count and run-level stop reason are unavailable.
Something Runtype should callRegister the agent as an external agent with a runtype-stream endpoint: Let Runtype call your Flue agent. Combine with either telemetry path.

For the full picture of which Runtype features apply to an agent that runs elsewhere, read Bring your own agent.

When to use this instead of a generic exporter

Reporting telemetry from an external agent covers the general path. A generic exporter sends the GenAI semantic-convention attributes that your spans contain.

This package adds two values that a generic export cannot derive after the spans leave the process:

  • An exact loop iteration count. Flue events do not include an absolute ordinal. If you rank turn IDs after export, you count only the turns in the export batch. A five-turn run whose final batch has two turns appears to have two iterations. The instrumentation counts each agent turn before export.
  • A run-level stop reason and summed token usage. The instrumentation sums token counts from model turns and adds the total to the run’s invoke_agent span. The run span retains that total when model spans flush in separate batches.

The package reports a delegated sub-agent as a tool call on the parent run. One trace represents one execution, so delegation does not create a second execution.

From 0.5 the package sends the transcript, system prompt and tool arguments and results by default, so runs carry a transcript and can be captured as eval cases. Every content kind has its own off switch, and content: false sends shape and cost only; see Content and What it does not send.

Install and start

Use Node 22 or later and a Flue runtime that exposes instrument(). Install the package and the OpenTelemetry API with this command:

Install
$npm install @runtypelabs/flue-otel @opentelemetry/api

Register the instrumentation with Flue using this code:

Register the instrumentation
1import { instrument } from '@flue/runtime'
2import { createRuntypeFlueInstrumentation } from '@runtypelabs/flue-otel'
3
4const stopInstrumenting = instrument(createRuntypeFlueInstrumentation())

The package supports Flue >=1.0.0-beta.9 and 2.x through one entry point.

For OpenTelemetry, the package depends on @opentelemetry/api and does not include an SDK, provider, exporter, sampler, or resource. Your application configures those components, and the instrumentation writes spans through the provider that you register. The instrumentation does not flush spans.

If your application already configures OpenTelemetry, register the instrumentation and add the resource attributes that identify the Runtype agent. Use runtypeFlueResourceAttributes({ agentId }) as described in Attribution.

Create a telemetry key with the Telemetry Ingest permission group. For the key creation steps, see Create a telemetry API key.

Set up an OpenTelemetry pipeline

If your application does not have an OpenTelemetry pipeline, install @opentelemetry/sdk-trace-node, @opentelemetry/exporter-trace-otlp-http, and @opentelemetry/resources. Configure the provider, exporter, and instrumentation with this example:

Full pipeline
1import { NodeTracerProvider, BatchSpanProcessor } from '@opentelemetry/sdk-trace-node'
2import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
3import { resourceFromAttributes } from '@opentelemetry/resources'
4import { instrument } from '@flue/runtime'
5import {
6 createRuntypeFlueInstrumentation,
7 runtypeFlueResourceAttributes,
8} from '@runtypelabs/flue-otel'
9
10const provider = new NodeTracerProvider({
11 resource: resourceFromAttributes({
12 'service.name': 'my-agent',
13 ...runtypeFlueResourceAttributes({ agentId: process.env.RUNTYPE_AGENT_ID }),
14 }),
15 spanProcessors: [
16 new BatchSpanProcessor(
17 new OTLPTraceExporter({
18 url: 'https://api.runtype.com/v1/otel/v1/traces',
19 headers: { Authorization: `Bearer ${process.env.RUNTYPE_API_KEY}` },
20 })
21 ),
22 ],
23})
24provider.register()
25
26instrument(createRuntypeFlueInstrumentation())
27
28process.on('beforeExit', () => void provider.shutdown())

Set RUNTYPE_AGENT_ID to the ID of the Runtype agent that receives the run. Set RUNTYPE_API_KEY to the telemetry key.

The following settings keep spans in one trace and preserve the closing run span:

Register the provider. provider.register() installs the OpenTelemetry context manager. Without it, the API uses a no-op context manager, so each span becomes a trace root and Runtype stores separate traces instead of one run.

Flush before the process exits. A BatchSpanProcessor exports child spans before parent spans. If the process exits without forceFlush() or shutdown(), the closing invoke_agent span can be lost. A run whose invoke_agent span never arrives remains in flight. In a serverless handler, await provider.forceFlush() before returning.

Flue’s instrument() returns a disposer. Call it when you stop instrumentation. It ends open spans as interrupted. It does not flush spans because your application owns the exporter.

Attribution: which Runtype agent owns the run

Runtype resolves the agent for each trace in this order:

  1. The runtype.agent.id resource attribute. The runtypeFlueResourceAttributes({ agentId }) helper sets it.
  2. The x-runtype-agent-id request header that your exporter sends.
  3. The runtype.agent.id attribute on the run’s invoke_agent span. The agents option sets it.

Use the resource attribute or request header when one process runs one Runtype agent.

Run several agents in one process

When one process runs several agents, map each Flue agent name to a Runtype agent ID with the agents option:

Per agent attribution
1createRuntypeFlueInstrumentation({
2 agents: {
3 triage: 'YOUR_TRIAGE_AGENT_ID',
4 billing: 'YOUR_BILLING_AGENT_ID',
5 },
6})

Replace YOUR_TRIAGE_AGENT_ID and YOUR_BILLING_AGENT_ID with the corresponding Runtype agent IDs.

The package does not attribute a delegated sub-agent separately. The sub-agent’s work stays in the delegating agent’s trace, and one trace represents one execution.

Keep one agent invocation per trace. Spans inherit the active OpenTelemetry context. If an enclosing span remains active when two agent invocations run, both invocations land in the same trace. An HTTP server span from auto-instrumentation commonly causes this.

Runtype either merges the invocations into one execution, which drops the second run’s usage and stop reason, or rejects the trace as ambiguous_agent_attribution when the invocations claim different IDs through agents and no resource attribute or request header resolves the trace. The rejected trace produces no run.

To dispatch several agent invocations in one request or job, start each invocation in its own trace. A separate process per agent lets you set the resource attribute instead. Flue’s dispatch() does not propagate trace context, so a direct dispatch() call creates its own trace.

What it emits

The instrumentation emits the following span types and attributes:

SpanWhenAttributes
invoke_agentOne per agent invocationThe request and response model, summed token usage, runtype.stop_reason, runtype.tools.reported, the highest runtype.iteration, runtype.execution.id, runtype.adapter.name and runtype.adapter.version, and runtype.agent.id when the agents option maps the agent.
chatOne per model turnThe provider, request and response model, finish reason, per-turn usage, runtype.turn.id, runtype.turn.index, runtype.iteration, and runtype.provider.finish_reason / runtype.gateway.log_id when the provider records them (Workers AI attaches both; the gateway log id is a pointer to that request in the AI Gateway dashboard).
execute_toolOne per model-requested tool callgen_ai.tool.name, gen_ai.tool.call.id, the loop position, and runtype.tool.type when the package identifies the tool class.
flue.task, flue.compaction, flue.operation shellDelegation, compaction, and host shell callsCorrelation IDs only.

Every span also carries the flue.* correlation attributes that the standard @flue/opentelemetry instrumentation emits. These attributes support dashboards that group spans by Flue correlation keys.

The following details explain two intentional omissions:

runtype.tool.type for ordinary tools. Flue’s origin field identifies who initiated a call, not its implementation class. The package emits this attribute only for a sub-agent delegation and maps a 1.x datastore tool to data_connection.

An unknown stop reason. If a run ends during a tool call, the package reports unknown. It cannot identify whether a turn cap, tool cap, or host abort stopped the run.

Compose with other instrumentations

Flue’s instrument() supports multiple subscribers. This package uses a separate instrumentation key, so it does not replace @flue/opentelemetry.

Send one Flue instrumentation to each Runtype endpoint. If this package and the standard @flue/opentelemetry instrumentation export to the same endpoint, Runtype receives two invoke_agent spans for one run and counts the token usage and cost twice. You can send them to different backends.

If you already export Flue traces to Runtype with the standard @flue/opentelemetry instrumentation, replace it with this package for that endpoint.

Export tool content

Each execute_tool span carries the tool’s arguments and result, shown on its card in the trace view. To drop either, switch it off:

Tool content switches
1instrument(
2 createRuntypeFlueInstrumentation({
3 content: {
4 toolArguments: true,
5 toolResults: false,
6 // Optional. Per value, marker included. Defaults to 64 KiB.
7 maxChars: 65_536,
8 // Optional. Runs on the raw value before encoding; narrow on kind first.
9 redact: (value, context) =>
10 context.kind === 'arguments' && context.toolName === 'delegate' ? undefined : value,
11 },
12 })
13)

Each switch is independent and defaults to on. The package writes gen_ai.tool.call.arguments when the span opens and gen_ai.tool.call.result immediately before it ends. Runtype reads those two attributes onto the tool card’s input and output. Arguments are always sent as a JSON object, because that is the shape Runtype projects for them: a non-object value is carried as { "value": ... }. A string result is sent as-is; any other result is sent as JSON.

A result longer than maxChars is cut and ends with …[truncated N chars], so a cut value never looks complete. Arguments over maxChars are replaced by { "truncated": "..." } holding the head of the JSON and the same marker, which keeps them a valid object. maxChars is clamped to 256 KiB because Runtype drops a single content attribute above that ceiling whole rather than cutting it. One span may carry at most 384 KiB of content across all of its attributes, and one export request at most 2 MiB. Content makes spans large: a batch exporter that sends 512 spans per request can exceed the 8 MiB request limit, which rejects the whole batch. Lower maxExportBatchSize (16 to 32 for chatty agents, 64 for short runs) with content on.

Content

The conversation itself travels on the invoke_agent span, which is the span Runtype reads a run’s transcript and final output from. Three switches govern it, all on by default:

Message content switches
1instrument(
2 createRuntypeFlueInstrumentation({
3 content: {
4 inputMessages: true, // gen_ai.input.messages
5 outputMessages: true, // gen_ai.output.messages
6 systemInstructions: false, // gen_ai.system_instructions
7 },
8 })
9)
10
11// Shape and cost only, no content of any kind:
12instrument(createRuntypeFlueInstrumentation({ content: false }))
  • inputMessages is the conversation as the run’s last model turn saw it, read from Flue’s normalized turn_request.request.input.messages, so a user message dispatched mid-run is included. It lands on the run as its transcript and is the seed eval capture freezes a case from.
  • outputMessages is the assistant text of the last agent turn that produced any, read from turn.response.output. Runtype projects it onto the run’s final output.
  • systemInstructions is turn_request.request.input.systemPrompt. It is usually the largest value a run carries and rarely differs between runs, so it is the switch most deployments turn off.

Messages are text parts only: tool-result rows, tool-call, thinking and image parts, any system row in the history, and Flue’s own signal messages are dropped. Over maxChars, the oldest messages are dropped whole first so the newest survive. AgentMessage is still never read; the Llm* payloads are identical on Flue 1.x and 2.x.

The package never sends a failed tool’s result, because it is commonly the error payload, and never sends content for the host’s own session.shell() call. A delegation to a sub-agent is a tool call whose arguments carry the prompt handed to that sub-agent. To keep that prompt out of the export, return undefined from redact for the delegation tool. Returning undefined from redact drops the attribute for that one call.

What it does not send

Whatever you set, the package never sends error messages or stack traces, a failed tool’s result, the host’s own session.shell() call, thinking or image parts, or Flue’s internal signal messages. With content: false it sends identifiers, structure, metrics, model IDs, token counts, durations, tool names, correlation IDs, error types, and exception class names for failed spans — and then:

  • Runs have no transcript. You can inspect timing, cost, iteration counts, and the tool call sequence, but not the exchanged text.
  • Eval capture is unavailable. Capturing an external run into an eval suite requires the transcript.

The standard @flue/opentelemetry instrumentation exports content by default. Review its exporter configuration before sending traces to a third-party backend.

Deploy on Cloudflare Workers

A Flue 2 agent deployed with @flue/vite and @cloudflare/vite-plugin runs as a Durable Object, and Workers Observability traces it automatically: an invoke_agent span per run, a chat span per model turn, and an execute_tool span per tool call, in the same GenAI semantic conventions this package emits, with conversation content included by default. You do not install this package or an OpenTelemetry SDK. Export the Workers traces to Runtype instead:

  1. In the Cloudflare dashboard, open Workers Observability and add a destination of type Traces with the endpoint https://api.runtype.com/v1/otel/v1/traces and two custom headers: Authorization: Bearer rt_YOUR_TELEMETRY_KEY and x-runtype-agent-id: agent_YOUR_AGENT_ID. Exporting Workers traces to an OTLP destination requires a Workers Paid plan.

  2. Reference the destination in wrangler.jsonc:

    wrangler.jsonc
    1{
    2 "observability": {
    3 "traces": {
    4 "enabled": true,
    5 "destinations": ["runtype"],
    6 "head_sampling_rate": 1,
    7 },
    8 },
    9}
  3. Run npx vite build && npx wrangler deploy and start a conversation. The run appears under the agent’s Runs with a transcript, so it can be captured as an eval case.

Workers traces carry content by default, the same as this package. To remove the content, register instrument(createCloudflareTracing({ content: false })) from @flue/runtime/cloudflare at module scope in app.ts; the runs then lose eval capture as well. Each agent conversation runs in its own isolate, so a subscriber registered in app.ts sees only that isolate’s activity; that is the behavior you want for one-run-per-trace attribution.

Let Runtype call your Flue agent

Telemetry is one direction. To test the agent from the dashboard, put a Persona chat widget or a surface in front of it, or schedule it, register it as an external agent whose endpoint streams Runtype’s unified event vocabulary:

POST /v1/agents
1{
2 "name": "Support agent (Flue)",
3 "agentType": "external",
4 "externalConfig": {
5 "endpoint": "https://my-worker.example.workers.dev/dispatch",
6 "protocol": "runtype-stream",
7 "framework": "flue",
8 "auth": { "type": "bearer", "credentials": "{{secret:MY_AGENT_TOKEN}}" }
9 }
10}

The endpoint receives POST {endpoint} with { "messages": [...], "context": { "conversationId": "..." } } and answers with a text/event-stream of unified frames (execution_start first, one terminal execution_complete or execution_error last; the ExecutionStreamEvent union in the API reference). In a Flue 2 app the route runs the turn with init(agent, { id: conversationId }) and handle.dispatch(message), then maps each chunk from handle.read(receipt, { onEvent }) onto the matching frame. Eval suites do not run against a runtype-stream endpoint; they run against an A2A endpoint. See Bring your own agent for the feature matrix.

Next steps

Continue with these guides: