@runtypelabs/flue-otel 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Runtype Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,218 @@
1
+ # @runtypelabs/flue-otel
2
+
3
+ OpenTelemetry instrumentation for [Flue](https://flueframework.com) agents that
4
+ reports runs to [Runtype](https://runtype.com) at full fidelity.
5
+
6
+ Install it with Flue's `instrument()`, point an OTLP exporter at Runtype, and
7
+ every agent run appears in your Runtype dashboard as a first-class execution —
8
+ with the model, token counts, cost, loop iterations, tool calls and stop reason
9
+ that a native Runtype run has.
10
+
11
+ ```bash
12
+ npm install @runtypelabs/flue-otel @opentelemetry/api
13
+ ```
14
+
15
+ ```ts
16
+ import { instrument } from '@flue/runtime'
17
+ import { createRuntypeFlueInstrumentation } from '@runtypelabs/flue-otel'
18
+
19
+ const stopInstrumenting = instrument(createRuntypeFlueInstrumentation())
20
+ ```
21
+
22
+ Works on **both** Flue lines — `>=1.0.0-beta.9` and 2.x — from one entry point.
23
+
24
+ ## Already exporting Flue traces to Runtype? Read this first
25
+
26
+ Two things change, and one of them is a regression, so decide before you install:
27
+
28
+ - **Replace your stock instrumentation, do not add to it.** Flue's
29
+ `instrument()` composes, and this package carries its own key, so calling both
30
+ is possible and silently wrong: Runtype receives two `invoke_agent` spans for
31
+ one run and the **token counts and cost double**. Point exactly one Flue
32
+ instrumentation at a given Runtype endpoint.
33
+ - **You will lose the transcript.** Stock `@flue/opentelemetry` exports content
34
+ by default, so your runs currently carry prompts and completions. This release
35
+ exports none (below), which also means **eval capture from these runs stops
36
+ working**. In exchange the runs gain loop structure, an accurate iteration
37
+ count, a stop reason, and priced usage. If the transcript is what you rely on,
38
+ stay on stock until content ships.
39
+
40
+ ## It emits no content
41
+
42
+ This release puts **no prompts, completions, tool arguments, tool results, error
43
+ messages or stack traces on the wire.** Only identifiers, structure and metrics.
44
+
45
+ That is deliberate and it is the default we intend to keep earning: a package
46
+ that lands inside your process should not start shipping your users' text
47
+ somewhere because you installed it. Content export is a later, explicitly
48
+ opt-in increment. Until it exists, there is nothing to configure and nothing to
49
+ audit — the spans carry model ids, token counts, durations, tool _names_, and
50
+ correlation ids.
51
+
52
+ Two consequences worth knowing:
53
+
54
+ - Your Runtype executions will show timing, cost, iteration counts and the tool
55
+ call sequence, but **no transcript**. Eval capture from an external run needs
56
+ content and is not available yet.
57
+ - If you also run stock `@flue/opentelemetry`, note that it treats calling
58
+ `instrument()` as consent to export content by default. That is a different
59
+ choice, not a bug — but it is worth checking before you point it at a
60
+ third-party backend.
61
+
62
+ ## It brings no OpenTelemetry SDK
63
+
64
+ This package depends on `@opentelemetry/api` and nothing else. It creates no
65
+ provider, no exporter, no sampler, no resource, and it never flushes. Your
66
+ application owns all of that, and this instrumentation writes through whatever
67
+ you have registered.
68
+
69
+ If you already run OpenTelemetry, you are done after the `instrument()` call
70
+ above — add the resource attributes below so Runtype knows which agent the runs
71
+ belong to.
72
+
73
+ ### If you have no OpenTelemetry setup yet
74
+
75
+ ```ts
76
+ import { NodeTracerProvider, BatchSpanProcessor } from '@opentelemetry/sdk-trace-node'
77
+ import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
78
+ import { resourceFromAttributes } from '@opentelemetry/resources'
79
+ import { instrument } from '@flue/runtime'
80
+ import {
81
+ createRuntypeFlueInstrumentation,
82
+ runtypeFlueResourceAttributes,
83
+ } from '@runtypelabs/flue-otel'
84
+
85
+ const provider = new NodeTracerProvider({
86
+ resource: resourceFromAttributes({
87
+ 'service.name': 'my-agent',
88
+ // Which Runtype agent these runs file under, plus adapter provenance.
89
+ ...runtypeFlueResourceAttributes({ agentId: process.env.RUNTYPE_AGENT_ID }),
90
+ }),
91
+ spanProcessors: [
92
+ new BatchSpanProcessor(
93
+ new OTLPTraceExporter({
94
+ url: 'https://api.runtype.com/v1/otel/v1/traces',
95
+ headers: { Authorization: `Bearer ${process.env.RUNTYPE_API_KEY}` },
96
+ })
97
+ ),
98
+ ],
99
+ })
100
+ // `register()` also installs the context manager, WITHOUT WHICH nothing nests:
101
+ // the OTel API's default is a no-op that makes every span a trace root.
102
+ provider.register()
103
+
104
+ instrument(createRuntypeFlueInstrumentation())
105
+
106
+ // Flush before the process exits — see "Shutdown" below. This is not optional.
107
+ process.on('beforeExit', () => void provider.shutdown())
108
+ ```
109
+
110
+ You will also need `@opentelemetry/sdk-trace-node`,
111
+ `@opentelemetry/exporter-trace-otlp-http` and `@opentelemetry/resources`.
112
+
113
+ ## Shutdown is yours, and it matters
114
+
115
+ A `BatchSpanProcessor` flushes children before their parents. A process that
116
+ exits without `forceFlush()` or `shutdown()` therefore tends to lose the
117
+ **closing `invoke_agent` span specifically** — and a run whose envelope never
118
+ arrives is recorded as still in flight, permanently.
119
+
120
+ Wire the shutdown. In a serverless handler, `await provider.forceFlush()` before
121
+ returning.
122
+
123
+ Flue's `instrument()` returns a disposer. Calling it ends any span still open as
124
+ `interrupted`, which is what you want on an unclean exit — but it does not
125
+ flush, because the exporter is not ours to flush.
126
+
127
+ ## Attribution: which Runtype agent owns the run
128
+
129
+ Runtype resolves the owning agent in this order:
130
+
131
+ 1. the `runtype.agent.id` **resource** attribute (what
132
+ `runtypeFlueResourceAttributes({ agentId })` sets) — preferred;
133
+ 2. the `x-runtype-agent-id` request header on the exporter;
134
+ 3. the `runtype.agent.id` attribute on the run's `invoke_agent` span.
135
+
136
+ The first two describe a whole process, so they are the right answer when it
137
+ runs one Runtype agent. **One process running several Runtype agents** cannot
138
+ express that in a shared resource, which is what the third placement is for:
139
+
140
+ ```ts
141
+ createRuntypeFlueInstrumentation({
142
+ agents: {
143
+ triage: 'agent_01jabc...', // keyed by the Flue agent's name
144
+ billing: 'agent_01jxyz...',
145
+ },
146
+ })
147
+ ```
148
+
149
+ A delegated sub-agent is deliberately **not** attributed separately. Its work
150
+ runs inside the delegating agent's trace, and one trace is one execution.
151
+
152
+ > **One agent invocation per trace.** Runtype's model is that one trace is one
153
+ > execution, and spans inherit whatever OTel context is active. So if an
154
+ > enclosing span is active while two agent invocations run — an HTTP server span
155
+ > from auto-instrumentation is the usual way this happens — both land in the
156
+ > same trace, and Runtype either merges them into one execution (dropping the
157
+ > second run's usage and stop reason) or, when they claim different
158
+ > `runtype.agent.id` values through the `agents` map above with no resource
159
+ > attribute and no header to fall back on, rejects the trace as
160
+ > `ambiguous_agent_attribution` and writes nothing.
161
+ >
162
+ > If you dispatch several agent invocations inside one request or job, start
163
+ > each one in its own trace, or attribute at the resource level with one process
164
+ > per agent. Flue's own `dispatch(...)` does not propagate trace context, so a
165
+ > plain dispatch is already its own trace.
166
+
167
+ ## Composing with other instrumentations
168
+
169
+ Flue's `instrument()` composes — an error reporter and a tracer subscribe side
170
+ by side — and this package carries its own key, so installing it never replaces
171
+ `@flue/opentelemetry`.
172
+
173
+ > **Point exactly one instrumentation at Runtype.** If this package and a stock
174
+ > `@flue/opentelemetry` both export to the same Runtype endpoint, Runtype sees
175
+ > two `invoke_agent` spans for one run and the **token counts double**. Running
176
+ > both is fine when they export to different backends.
177
+
178
+ ## What it emits
179
+
180
+ | Span | When | Carries |
181
+ | -------------------------------------------------------------- | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
182
+ | `invoke_agent <agent>` | one per agent invocation | the run's model, summed token usage, `runtype.stop_reason`, `runtype.tools.reported`, the highest loop iteration, `runtype.execution.id`, and `runtype.agent.id` when `agents` names the agent |
183
+ | `chat <model>` | one per model turn | provider, request/response model, finish reason, per-turn usage, `runtype.turn.id` / `.turn.index` / `.iteration` |
184
+ | `execute_tool <tool>` | one per tool call | `gen_ai.tool.name`, `gen_ai.tool.call.id`, the loop position it belongs to, and `runtype.tool.type` when the tool's class is actually known |
185
+ | `flue.task <agent>`, `flue.compaction`, `flue.operation shell` | delegation, compaction, host shell | correlation ids only — these are framework structure, not agent invocations |
186
+
187
+ Every span also carries the `flue.*` correlation attributes stock
188
+ `@flue/opentelemetry` emits, so dashboards you have already built keep working.
189
+
190
+ ### Two places it deliberately says nothing
191
+
192
+ - **`runtype.tool.type` for an ordinary tool.** Flue's `origin` field classifies
193
+ who _initiated_ a call (`model` for every model-requested tool, whoever wrote
194
+ it), not what the tool _is_. Mapping it would fill the column with a value
195
+ that means nothing on that axis. Only a genuine class fact is emitted: a
196
+ sub-agent delegation, and a 1.x `datastore` tool.
197
+ - **A stop reason it cannot determine.** A run that ended mid-tool-call reports
198
+ `unknown` rather than guessing between a turn cap, a tool cap and a host
199
+ abort.
200
+
201
+ An absent attribute costs one column. A wrong one renders as a measurement.
202
+
203
+ ## Deriving from Flue's stable surface only
204
+
205
+ Everything here comes from the observations Flue publishes as stable — event
206
+ type names, envelope and correlation fields, and the normalized `turn_request` /
207
+ `turn` / `tool_*` / `task` / `operation` / `compaction` / `submission_settled`
208
+ payloads.
209
+
210
+ Nothing reads `AgentMessage`, which Flue documents as explicitly unstable and
211
+ which rides `message_start`, `message_end`, `turn_messages` and `agent_end`.
212
+ That exclusion is why one entry point serves both the 1.x and 2.x lines: the
213
+ observation plane was essentially frozen across Flue's major rewrite, and the
214
+ handful of fields that did change are probed rather than version-compared.
215
+
216
+ ## License
217
+
218
+ MIT