@glassflow-ai/rius 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 GlassFlow
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,328 @@
1
+ # @glassflow-ai/rius
2
+
3
+ OpenTelemetry-native tracing for AI agents and LLM applications.
4
+
5
+ Rius wraps the OpenTelemetry SDK with agent-shaped primitives: spans for
6
+ tool calls and chains, generations for LLM calls, and a function wrapper
7
+ that turns a plain `async` function into an instrumented one. Traces are
8
+ sent as OTLP over HTTP, so any OTLP-compatible backend can receive them.
9
+
10
+ > Status: alpha and unpublished (0.1.0). APIs may change before the first release.
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ npm install @glassflow-ai/rius @opentelemetry/api
16
+ ```
17
+
18
+ `@opentelemetry/api` is a peer dependency and is never bundled. Install
19
+ it alongside `@glassflow-ai/rius` at a version satisfying `^1.9.0`.
20
+
21
+ ## Quickstart
22
+
23
+ ```ts
24
+ import {
25
+ init,
26
+ observe,
27
+ startAsCurrentSpan,
28
+ startAsCurrentGeneration,
29
+ SpanKind,
30
+ } from "@glassflow-ai/rius";
31
+
32
+ const client = init({
33
+ apiKey: process.env.RIUS_API_KEY, // or set RIUS_API_KEY
34
+ });
35
+
36
+ // Optional: wait for auto-instrumentations to finish attaching before the
37
+ // first span is created.
38
+ await client.ready;
39
+
40
+ // 1. Function wrapper — trace a whole call. Use a named function expression
41
+ // (or pass { name }): an arrow function has no inferrable name and its
42
+ // span would fall back to "anonymous".
43
+ const handle = observe(async function handle(query: string) {
44
+ return await callModel(query);
45
+ });
46
+
47
+ // 2. Scoped span — trace a block
48
+ await startAsCurrentSpan(
49
+ "retrieve",
50
+ { kind: SpanKind.RETRIEVER, input: query },
51
+ async (obs) => {
52
+ const docs = await retrieveDocuments(query);
53
+ obs.setOutput(docs);
54
+ return docs;
55
+ },
56
+ );
57
+
58
+ // 3. LLM generations — gen_ai-native
59
+ await startAsCurrentGeneration(
60
+ "chat",
61
+ { model: "gpt-4o", input: messages },
62
+ async (gen) => {
63
+ const reply = await callModel(messages);
64
+ gen.setOutput(reply);
65
+ gen.setUsage({ inputTokens: 42, outputTokens: 17 });
66
+ return reply;
67
+ },
68
+ );
69
+ ```
70
+
71
+ Each surface has more detail below: `observe` for the function wrapper,
72
+ `startSpan`/`startAsCurrentSpan` for manual and scoped spans, and
73
+ `startGeneration`/`startAsCurrentGeneration` for LLM calls.
74
+
75
+ ### `observe`: wrap a function
76
+
77
+ `observe` wraps a function so every call becomes a span. It always
78
+ returns a **promise**, even when the wrapped function is synchronous.
79
+
80
+ Use a named function expression (or pass `{ name }`) so the span gets a
81
+ real name. An arrow function has no inferrable name, and its span falls
82
+ back to `"anonymous"`:
83
+
84
+ ```ts
85
+ import { observe } from "@glassflow-ai/rius";
86
+
87
+ const handleQuery = observe(async function handleQuery(query: string) {
88
+ return await callModel(query);
89
+ });
90
+
91
+ // Equivalent, with an explicit name:
92
+ const handleQuery2 = observe(
93
+ async (query: string) => callModel(query),
94
+ { name: "handleQuery" },
95
+ );
96
+
97
+ await handleQuery("hello");
98
+ ```
99
+
100
+ ### `startSpan` / `startAsCurrentSpan`: manual spans
101
+
102
+ For finer control than `observe`, create spans directly:
103
+
104
+ ```ts
105
+ import { startAsCurrentSpan } from "@glassflow-ai/rius";
106
+
107
+ await startAsCurrentSpan("fetch-documents", { input: query }, async (span) => {
108
+ const docs = await fetchDocuments(query);
109
+ span.setOutput(docs);
110
+ return docs;
111
+ });
112
+ ```
113
+
114
+ `startSpan` returns a handle you end yourself (or dispose with `using`);
115
+ `startAsCurrentSpan` runs a callback with the span active, ending it
116
+ automatically and recording any thrown exception. The options argument is
117
+ optional in both, so `startAsCurrentSpan("step", async (span) => { ... })`
118
+ works when you have nothing to configure.
119
+
120
+ On the manual path, `recordException()` does what the scoped form does for
121
+ you: it records the error and sets ERROR status.
122
+
123
+ ```ts
124
+ const span = startSpan("fetch-documents");
125
+ try {
126
+ span.setOutput(await fetchDocuments(query));
127
+ } catch (error) {
128
+ span.recordException(error);
129
+ throw error;
130
+ } finally {
131
+ span.end();
132
+ }
133
+ ```
134
+
135
+ ### `startGeneration` / `startAsCurrentGeneration`: LLM calls
136
+
137
+ A `Generation` is a span specialised for LLM calls, with setters for the
138
+ gen_ai message and usage attributes:
139
+
140
+ ```ts
141
+ import { startAsCurrentGeneration } from "@glassflow-ai/rius";
142
+
143
+ await startAsCurrentGeneration(
144
+ "chat-completion",
145
+ {
146
+ model: "gpt-4o",
147
+ provider: "openai",
148
+ input: messages,
149
+ modelParameters: { temperature: 0.2, max_tokens: 512 },
150
+ },
151
+ async (generation) => {
152
+ const response = await client.chat.completions.create({ model: "gpt-4o", messages });
153
+ generation.setOutput(response.choices[0]?.message);
154
+ generation.setUsage({
155
+ inputTokens: response.usage?.prompt_tokens,
156
+ outputTokens: response.usage?.completion_tokens,
157
+ });
158
+ generation.setFinishReasons(response.choices[0]?.finish_reason ?? "stop");
159
+ return response;
160
+ },
161
+ );
162
+ ```
163
+
164
+ Each `modelParameters` entry is recorded as `gen_ai.request.<key>`, so use the
165
+ provider's own parameter names. `setFinishReasons` accepts one reason or a
166
+ list.
167
+
168
+ ## Auto-instrumentation
169
+
170
+ The SDK bundles five integrations. Each one is an optional peer
171
+ dependency: install the packages you need and `init()` enables whatever
172
+ it finds, so a plain `npm install @glassflow-ai/rius` patches nothing.
173
+
174
+ ```bash
175
+ npm install @arizeai/openinference-instrumentation-openai @arizeai/openinference-instrumentation-anthropic @arizeai/openinference-instrumentation-langchain @arizeai/openinference-vercel @modelcontextprotocol/sdk
176
+ ```
177
+
178
+ Install only the ones you use:
179
+
180
+ - **`openai`** wraps the OpenAI SDK, via `@arizeai/openinference-instrumentation-openai`, turning provider calls into spans.
181
+ - **`anthropic`** wraps the Anthropic SDK, via `@arizeai/openinference-instrumentation-anthropic`, turning Messages calls into LLM spans with model, messages and token counts.
182
+ - **`langchain`** traces LangChain.js chains, models, tools and retrievers, via `@arizeai/openinference-instrumentation-langchain`. It patches the callback manager in `@langchain/core`, which every LangChain.js application already has, and the SDK resolves that module for you so nothing has to be passed in at `init()`.
183
+ - **`vercel-ai`** attaches to spans the Vercel AI SDK's own OpenTelemetry integration produces, via `@arizeai/openinference-vercel`, adding OpenInference attributes to them. This package requires Node 22 or newer.
184
+ - **`mcp`** patches `@modelcontextprotocol/sdk`'s `Client.callTool` so every MCP tool call becomes a TOOL span, carrying the tool name, arguments, result, latency, and error status.
185
+
186
+ There is no selection option: `init()` always attempts every bundled
187
+ integration, so which ones actually attach is determined entirely by
188
+ which optional peers are installed. `client.ready` resolves with the
189
+ names that attached (for example `["openai", "mcp"]`) and never rejects,
190
+ even if every peer is missing or one of them is broken. A package that
191
+ is not installed stays quiet; a package that is installed but fails to
192
+ load logs a warning naming the integration and the underlying error, and
193
+ the SDK continues without it.
194
+
195
+ The `openai` and `anthropic` integrations work in both module systems:
196
+ CommonJS applications that `require` the provider SDK and pure-ESM
197
+ applications that `import` it are both patched, regardless of whether
198
+ the provider was loaded before or after `init()`. One combination is
199
+ not covered: the provider SDKs ship separate CommonJS and ESM builds,
200
+ and the instrumentation packages can patch only one of them per process
201
+ ([Arize-ai/openinference#3557](https://github.com/Arize-ai/openinference/issues/3557)),
202
+ so exactly one build gets patched: the CommonJS build when anything in
203
+ the process `require`d the provider before `init()`, the ESM build
204
+ otherwise. Two combinations therefore stay uncovered until the
205
+ upstream fix lands: a CommonJS application whose *only* `require` of
206
+ the provider happens lazily after `init()` (requiring it anywhere
207
+ before `init()` avoids this), and — the mirror image — an ESM
208
+ application where some CommonJS dependency `require`d the provider
209
+ before `init()`, which wins the choice away from the ESM build the
210
+ application itself is calling. The `langchain`, `vercel-ai` and `mcp`
211
+ integrations attach differently and have no such edge.
212
+
213
+ Content captured by these integrations, prompts, tool arguments and
214
+ results, and so on, is covered by the same `mask` and `captureContent`
215
+ controls as our own spans, since sanitisation happens centrally at
216
+ export rather than per integration.
217
+
218
+ `disabled: true` now means no third-party patching happens at all: no
219
+ optional package is even imported, and `client.ready` resolves to an
220
+ empty array.
221
+
222
+ ## Configuration
223
+
224
+ Options passed to `init()` take priority, then `RIUS_*` environment
225
+ variables, then the defaults below.
226
+
227
+ | Option | Environment variable | Default |
228
+ | ---------------- | ------------------------ | --------------------------------------------- |
229
+ | `endpoint` | `RIUS_ENDPOINT` | `https://ingest.eu.console.rius-glassflow.com` |
230
+ | `apiKey` | `RIUS_API_KEY` | none |
231
+ | `serviceName` | `RIUS_SERVICE_NAME` | `unknown_service` |
232
+ | `disabled` | `RIUS_DISABLED` | `false` |
233
+ | `sampleRate` | `RIUS_SAMPLE_RATE` | `1.0` |
234
+ | `captureContent` | `RIUS_CAPTURE_CONTENT` | `true` |
235
+ | `mask` | (options only) | none |
236
+
237
+ Traces are posted to `<endpoint>/v1/traces`.
238
+
239
+ `captureContent` controls whether input/output content (prompts,
240
+ completions, tool arguments) is attached to spans. It defaults to
241
+ `true`. Set it to `false`, or supply a `mask` function, if your spans
242
+ must not carry raw content. Both apply to span attributes and to the
243
+ attributes of span events and links. With `captureContent: false` the
244
+ message and stacktrace of a recorded exception are stripped as well,
245
+ since provider errors routinely echo the request back; the exception
246
+ event and its `exception.type` are kept, so failures stay visible.
247
+ `mask` does not extend that far: with `captureContent: true`, a `mask`
248
+ function scrubs content attributes on spans, events and links, but
249
+ exception messages, stacktraces and the status message pass through
250
+ unmasked, so a raw provider error can still reach your backend. Disable
251
+ `captureContent` if you need those scrubbed too.
252
+
253
+ `disabled` turns the SDK off completely: nothing is exported, and no
254
+ optional integration is loaded, so no third-party module is patched in your
255
+ process. `client.ready` resolves to an empty list. Spans can still be
256
+ created and are simply dropped.
257
+
258
+ ## Reliability
259
+
260
+ Export is designed to never block or crash your application:
261
+
262
+ - **Async batched export.** Spans are queued in-process and exported in
263
+ batches from a background timer via OpenTelemetry's `BatchSpanProcessor`.
264
+ Span creation stays fast even when the backend is slow or unreachable.
265
+ - **Retries.** The OTLP/HTTP transport retries transient and retryable
266
+ failures with exponential backoff and jitter, up to 5 attempts, bounded
267
+ by the export timeout.
268
+ - **Graceful degradation.** If the backend stays unreachable, spans are
269
+ dropped and the failure is logged, it is never raised into application
270
+ code. Only the first export failure in a process is logged, naming
271
+ `RIUS_API_KEY`/`RIUS_ENDPOINT` as the likely cause; later failures stay
272
+ silent so a persistent outage does not spam your logs. `client.flush()`
273
+ returns `false` when the most recent export failed, so you can check
274
+ delivery explicitly instead of relying on logs alone.
275
+ - **Mask failures fail safe.** If a `mask` callback throws, the affected
276
+ attribute value is replaced with the literal `"[mask error]"` rather
277
+ than dropping the batch or letting the exception escape.
278
+ - **No automatic flush on exit.** The SDK does not register a process-exit
279
+ hook. Call `client.flush()` to force a pending export, or
280
+ `client.shutdown()` to drain the queue and tear the client down, before
281
+ your process exits:
282
+
283
+ ```ts
284
+ await client.flush();
285
+ await client.shutdown();
286
+ ```
287
+
288
+ Batching is tunable via the standard OpenTelemetry env vars:
289
+ `OTEL_BSP_MAX_QUEUE_SIZE` (default 2048; spans beyond this are dropped),
290
+ `OTEL_BSP_SCHEDULE_DELAY` (default 5000 ms), `OTEL_BSP_MAX_EXPORT_BATCH_SIZE`
291
+ (default 512), and `OTEL_BSP_EXPORT_TIMEOUT` (default 30000 ms).
292
+
293
+ ## Development
294
+
295
+ ```bash
296
+ npm install
297
+ npm run lint
298
+ npm run typecheck
299
+ npm test
300
+ npm run build
301
+ ```
302
+
303
+ ## Releasing
304
+
305
+ Releases are automated with [release-please](https://github.com/googleapis/release-please)
306
+ and published to npm.
307
+
308
+ 1. Merge changes to `main` using [Conventional Commits](https://www.conventionalcommits.org/)
309
+ (`feat:` → minor, `fix:` → patch, `feat!:`/`BREAKING CHANGE` → major).
310
+ 2. release-please keeps a **Release PR** open that bumps the package
311
+ version and updates the changelog. Merge it when you want to cut a
312
+ release.
313
+ 3. Merging the Release PR tags `vX.Y.Z`, creates a GitHub Release, and
314
+ publishes to npm automatically using npm trusted publishing, no token
315
+ required.
316
+
317
+ Non-conventional commits are ignored for versioning.
318
+
319
+ The package has not been published yet, so the very first release cannot
320
+ use trusted publishing: npm requires a package to already exist before
321
+ it can be linked to a trusted publisher. That first publish needs either
322
+ a one-time manual `npm publish` or a temporary `NPM_TOKEN` secret; once
323
+ the package exists on npm, trusted publishing takes over and the token
324
+ can be removed.
325
+
326
+ ## License
327
+
328
+ MIT