@runtypelabs/flue-otel 0.3.0 → 0.5.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/README.md +137 -32
- package/dist/index.cjs +172 -46
- package/dist/index.d.cts +81 -299
- package/dist/index.d.ts +81 -299
- package/dist/index.mjs +172 -46
- package/package.json +6 -5
package/README.md
CHANGED
|
@@ -10,9 +10,11 @@ token usage, cost, loop iterations, tool calls and stop reason.
|
|
|
10
10
|
- Works with Flue `>=1.0.0-beta.9` and 2.x from one entry point.
|
|
11
11
|
- Depends on `@opentelemetry/api` only. It brings no SDK, provider, exporter or
|
|
12
12
|
sampler; it writes through whatever your application has registered.
|
|
13
|
-
- Emits
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
- Emits everything Runtype can show by default: identifiers, structure,
|
|
14
|
+
metrics, the transcript, the system prompt, and tool arguments and results.
|
|
15
|
+
Every content kind has its own off switch, and `content: false` sends shape
|
|
16
|
+
and cost only; see [Content](#content). Error messages and stack traces never
|
|
17
|
+
reach the wire.
|
|
16
18
|
|
|
17
19
|
Full guide: [Instrumenting a Flue agent](https://docs.runtype.com/developer-guides/guides/flue-instrumentation).
|
|
18
20
|
|
|
@@ -85,11 +87,11 @@ process.on('beforeExit', () => void provider.shutdown())
|
|
|
85
87
|
|
|
86
88
|
`createRuntypeFlueInstrumentation(options?)` accepts:
|
|
87
89
|
|
|
88
|
-
| Option | Type
|
|
89
|
-
| --------- |
|
|
90
|
-
| `agents` | `Record<string, string>`
|
|
91
|
-
| `tracer` | `Tracer`
|
|
92
|
-
| `content` | `FlueContentOptions`
|
|
90
|
+
| Option | Type | Purpose |
|
|
91
|
+
| --------- | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
92
|
+
| `agents` | `Record<string, string>` | Runtype agent id per Flue agent name, e.g. `{ triage: 'agent_01j...' }`. Stamped as `runtype.agent.id` on that agent's `invoke_agent` span. See [Attribution](#attribution). |
|
|
93
|
+
| `tracer` | `Tracer` | Where spans are written. Defaults to `trace.getTracer('@runtypelabs/flue-otel')` on the global provider. Pass one to route Runtype spans through a separate provider. |
|
|
94
|
+
| `content` | `FlueContentOptions \| false` | Transcript, system prompt, tool arguments and results. All on by default; switch kinds off individually, or pass `false` for none. See [Content](#content). |
|
|
93
95
|
|
|
94
96
|
The returned object implements Flue's full `FlueInstrumentation` contract:
|
|
95
97
|
`observe` (builds spans from Flue's observation stream), `interceptor` (makes
|
|
@@ -106,9 +108,12 @@ subscriber. Its key is exported as `RUNTYPE_FLUE_INSTRUMENTATION_KEY`.
|
|
|
106
108
|
|
|
107
109
|
- `runtypeFlueResourceAttributes({ agentId? })` builds the `runtype.*` resource
|
|
108
110
|
attributes (`schema.version`, `adapter.name`, `adapter.version`, and
|
|
109
|
-
`agent.id` when given). Spread it into your SDK `Resource`.
|
|
111
|
+
`agent.id` when given). Spread it into your SDK `Resource`. The adapter pair
|
|
112
|
+
is also stamped on each run's `invoke_agent` span, so Runtype can tell which
|
|
113
|
+
version produced a trace even when the resource is not wired; the resource
|
|
114
|
+
placement still wins where both are present.
|
|
110
115
|
- `GEN_AI` and `RUNTYPE`: the attribute-name constants this package emits by
|
|
111
|
-
default; `GEN_AI_CONTENT`: the
|
|
116
|
+
default; `GEN_AI_CONTENT`: the five content attributes it emits unless switched off.
|
|
112
117
|
- `DEFAULT_CONTENT_MAX_CHARS` and `INGEST_CONTENT_ATTRIBUTE_CEILING`: the
|
|
113
118
|
content size defaults, see [Tool content](#tool-content).
|
|
114
119
|
- `RUNTYPE_STOP_REASONS`, `RUNTYPE_TOOL_TYPES`, `RUNTYPE_SCHEMA_VERSION`,
|
|
@@ -185,7 +190,7 @@ backends.
|
|
|
185
190
|
| -------------------------------------------------------------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
186
191
|
| `invoke_agent <agent>` | once per agent invocation | `gen_ai.agent.name`, `gen_ai.conversation.id`, the run's request/response model, summed token usage across every turn (`gen_ai.usage.*`), `runtype.stop_reason`, `runtype.tools.reported`, the highest loop iteration (`runtype.iteration`), `runtype.execution.id`, and `runtype.agent.id` when `agents` names the agent |
|
|
187
192
|
| `chat <model>` | once per model turn | `gen_ai.provider.name`, request/response model, response id, finish reason, per-turn usage, request parameters (`max_tokens`, `temperature`, reasoning level, server address), `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 exact request in your AI Gateway dashboard) |
|
|
188
|
-
| `execute_tool <tool>` | once per model-requested tool call | `gen_ai.tool.name`, `gen_ai.tool.call.id`, the loop position it belongs to, `gen_ai.tool.type` (`function`) for every tool except a sub-agent delegation, and `runtype.tool.type` when the tool's class is known, and `gen_ai.tool.call.arguments` / `gen_ai.tool.call.result`
|
|
193
|
+
| `execute_tool <tool>` | once per model-requested tool call | `gen_ai.tool.name`, `gen_ai.tool.call.id`, the loop position it belongs to, `gen_ai.tool.type` (`function`) for every tool except a sub-agent delegation, and `runtype.tool.type` when the tool's class is known, and `gen_ai.tool.call.arguments` / `gen_ai.tool.call.result` unless [`content`](#tool-content) switches them off |
|
|
189
194
|
| `flue.task <agent>`, `flue.compaction`, `flue.operation shell` | delegation, compaction, host shell call | correlation ids only; framework structure, not agent invocations |
|
|
190
195
|
|
|
191
196
|
Every span also carries Flue's own `flue.*` correlation attributes
|
|
@@ -226,25 +231,58 @@ Rules the projection follows:
|
|
|
226
231
|
|
|
227
232
|
An absent attribute costs one column. A wrong one renders as a measurement.
|
|
228
233
|
|
|
229
|
-
##
|
|
234
|
+
## Content
|
|
230
235
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
arguments and results
|
|
236
|
+
Everything under `content` is **on by default** and independent: with no
|
|
237
|
+
option at all a run lands in Runtype with its transcript, system prompt, and
|
|
238
|
+
every tool's arguments and results — which is what lets Runtype show the
|
|
239
|
+
conversation and capture it as a replayable eval case. Two families:
|
|
240
|
+
[tool content](#tool-content) on `execute_tool` spans, and
|
|
241
|
+
[messages](#messages) on the `invoke_agent` envelope. Set a switch to `false`
|
|
242
|
+
to drop that one kind, or pass `content: false` to send shape and cost only.
|
|
234
243
|
|
|
235
244
|
```ts
|
|
236
245
|
createRuntypeFlueInstrumentation({
|
|
237
246
|
content: {
|
|
238
|
-
toolArguments: true,
|
|
239
|
-
toolResults: true,
|
|
247
|
+
toolArguments: true,
|
|
248
|
+
toolResults: true,
|
|
249
|
+
inputMessages: true,
|
|
250
|
+
outputMessages: true,
|
|
251
|
+
systemInstructions: false, // the one most deployments turn off: large, and rarely per-run
|
|
252
|
+
maxChars: 65_536,
|
|
253
|
+
redact: (value, context) =>
|
|
254
|
+
context.kind === 'arguments' && context.toolName === 'delegate' ? undefined : value,
|
|
255
|
+
},
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
// Shape and cost only, no content of any kind:
|
|
259
|
+
createRuntypeFlueInstrumentation({ content: false })
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Content makes spans large. Runtype admits at most 384 KiB of content per span
|
|
263
|
+
and 2 MiB per export request, and rejects a request over 8 MiB whole; past the
|
|
264
|
+
2 MiB mark the remaining runs in that request land without their transcript.
|
|
265
|
+
With content on, set your `BatchSpanProcessor`'s `maxExportBatchSize` so one
|
|
266
|
+
request stays under those bounds — 16 to 32 spans for chatty agents, 64 for
|
|
267
|
+
short runs — and watch `contentDropCount` on ingested runs.
|
|
268
|
+
|
|
269
|
+
### Tool content
|
|
270
|
+
|
|
271
|
+
An `execute_tool` span carries the call's arguments and result, so Runtype's
|
|
272
|
+
trace view shows them on the tool card. To drop either:
|
|
273
|
+
|
|
274
|
+
```ts
|
|
275
|
+
createRuntypeFlueInstrumentation({
|
|
276
|
+
content: {
|
|
277
|
+
toolArguments: false, // gen_ai.tool.call.arguments, set when the span opens
|
|
278
|
+
toolResults: false, // gen_ai.tool.call.result, set just before the span ends
|
|
240
279
|
maxChars: 65_536, // per value, marker included; this is the default
|
|
241
|
-
redact: (value, { kind, toolName }) => value, // optional;
|
|
280
|
+
redact: (value, { kind, toolName }) => value, // optional; narrow on kind, then toolName
|
|
242
281
|
},
|
|
243
282
|
})
|
|
244
283
|
```
|
|
245
284
|
|
|
246
|
-
|
|
247
|
-
values come from Flue's stable `tool_start.args` and `tool.result` payloads
|
|
285
|
+
The values come from Flue's stable `tool_start.args` and `tool.result` payloads
|
|
248
286
|
(2.x's `effectiveResult`, what the model was actually shown, when present).
|
|
249
287
|
|
|
250
288
|
The attribute names are the GenAI semconv `gen_ai.tool.call.arguments` and
|
|
@@ -275,23 +313,90 @@ Ceilings worth knowing, because they decide what Runtype keeps:
|
|
|
275
313
|
the whole batch (envelope and usage included, not only the content). With
|
|
276
314
|
both switches on, set `maxExportBatchSize` to something like 64.
|
|
277
315
|
|
|
278
|
-
What
|
|
316
|
+
What tool content never carries, whatever you set:
|
|
279
317
|
|
|
280
318
|
- **A failed tool's result.** On both Flue lines it is commonly the error
|
|
281
319
|
payload, and error messages stay off the wire. The span still closes with its
|
|
282
320
|
error type and exception class name.
|
|
283
321
|
- **The host's own shell call** (`session.shell()`), which is not a tool row.
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
322
|
+
|
|
323
|
+
One edge to know: a delegation to a sub-agent is a tool call, and its
|
|
324
|
+
**arguments carry the prompt handed to that sub-agent**. If that matters for
|
|
325
|
+
your deployment, drop it with `redact` (return `undefined` when `toolName` is
|
|
326
|
+
the delegation tool).
|
|
327
|
+
|
|
328
|
+
### Messages
|
|
329
|
+
|
|
330
|
+
Three switches govern the conversation itself on the `invoke_agent` span, which
|
|
331
|
+
is the span Runtype reads a run's transcript and final output from. All three
|
|
332
|
+
are on unless switched off:
|
|
333
|
+
|
|
334
|
+
```ts
|
|
335
|
+
createRuntypeFlueInstrumentation({
|
|
336
|
+
content: {
|
|
337
|
+
inputMessages: true, // gen_ai.input.messages
|
|
338
|
+
outputMessages: true, // gen_ai.output.messages
|
|
339
|
+
systemInstructions: false, // gen_ai.system_instructions
|
|
340
|
+
},
|
|
341
|
+
})
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
- **`inputMessages`** is the conversation as the run's _last_ model turn saw
|
|
345
|
+
it, read from Flue's normalized `turn_request.request.input.messages`: every
|
|
346
|
+
agent turn replaces the capture, so a user message dispatched mid-run (a
|
|
347
|
+
signal delivery, `useDispatchMessage()`) is included, and so is the run's own
|
|
348
|
+
intermediate assistant text. One copy per run, on the envelope. It lands on
|
|
349
|
+
the run as its transcript and is the seed eval capture freezes a case from.
|
|
350
|
+
- **`outputMessages`** is the assistant text of the _last_ agent turn that
|
|
351
|
+
produced any, read from `turn.response.output`. A final step that only called
|
|
352
|
+
tools does not clear an earlier answer, and a failed turn's output is never
|
|
353
|
+
read. Runtype projects it onto the run's final output.
|
|
354
|
+
- **`systemInstructions`** is `turn_request.request.input.systemPrompt`,
|
|
355
|
+
sent as one text part (`[{ type: 'text', content }]`) so a prompt that happens
|
|
356
|
+
to be valid JSON is never re-read as data. It is usually the largest value a
|
|
357
|
+
run carries and rarely differs between runs, so it is the switch most
|
|
358
|
+
deployments turn off; Runtype prepends it to the transcript as a `system` row.
|
|
359
|
+
|
|
360
|
+
Messages are encoded as the GenAI semconv structured-message shape,
|
|
361
|
+
`[{ role, parts: [{ type: 'text', content }] }]`, and they are **text parts
|
|
362
|
+
only**: a `toolResult` message, a tool-call part, a thinking part and an image
|
|
363
|
+
part are dropped, as is any `system` row Flue put in the history (that is what
|
|
364
|
+
`systemInstructions` is for) and any Flue **signal** — Flue delivers its own
|
|
365
|
+
steering (`<signal type="…">…</signal>`, `useAgentFinish` continuations, stream
|
|
366
|
+
recovery) to the model as a user message wrapping one XML element, and those
|
|
367
|
+
are not something the user said. Tool content has its own switches above; the
|
|
368
|
+
rest is never exported. `AgentMessage`, which Flue marks unstable, is still
|
|
369
|
+
never read — the `Llm*` request and response payloads are byte-identical on
|
|
370
|
+
1.x and 2.x.
|
|
371
|
+
|
|
372
|
+
Over `maxChars`, a message list drops its **oldest** messages whole first, so
|
|
373
|
+
the newest messages survive; a lone message that still does not fit has its
|
|
374
|
+
text cut with the truncation marker, and the JSON stays valid either way. The
|
|
375
|
+
system prompt is cut inside its text part. Because all three share one span,
|
|
376
|
+
each is additionally capped at a third of ingest's per-span ceiling
|
|
377
|
+
(128 KiB), whatever `maxChars` says; at the default ceiling they fit
|
|
378
|
+
comfortably.
|
|
379
|
+
|
|
380
|
+
### Redaction
|
|
381
|
+
|
|
382
|
+
`redact` runs on every content value before encoding and truncation. It
|
|
383
|
+
receives the value and a context:
|
|
384
|
+
|
|
385
|
+
- `{ kind: 'arguments' | 'result', toolName }` with the raw Flue payload;
|
|
386
|
+
- `{ kind: 'input_messages' | 'output_messages' }` with the projected message
|
|
387
|
+
array (`[{ role, parts: [{ type: 'text', content }] }]`; `{ role, content }`
|
|
388
|
+
with a string or Flue-style `{ type: 'text', text }` parts is accepted back);
|
|
389
|
+
- `{ kind: 'system_instructions' }` with the prompt string; return a string or
|
|
390
|
+
text parts (`[{ type: 'text', content | text }]`) — anything else emits
|
|
391
|
+
nothing, because ingest would discard it silently.
|
|
392
|
+
|
|
393
|
+
`toolName` is present on the tool kinds and `undefined` on the others, so a
|
|
394
|
+
hook written for 0.4 — `({ kind, toolName }) => …` — keeps compiling; narrow on
|
|
395
|
+
`kind` before relying on it.
|
|
396
|
+
|
|
397
|
+
Return the value to keep it, a replacement to substitute it, or `undefined` to
|
|
398
|
+
drop that one attribute. If it throws, the attribute is dropped and the span is
|
|
399
|
+
otherwise unaffected.
|
|
295
400
|
|
|
296
401
|
## Flue compatibility
|
|
297
402
|
|
package/dist/index.cjs
CHANGED
|
@@ -78,35 +78,156 @@ function mapSettlementOutcome(outcome) {
|
|
|
78
78
|
return void 0;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
// src/messages.ts
|
|
82
|
+
var ROLES = /* @__PURE__ */ new Set(["system", "user", "assistant"]);
|
|
83
|
+
var FLUE_ROLES = /* @__PURE__ */ new Set(["user", "assistant"]);
|
|
84
|
+
var SIGNAL_MESSAGE = /^\s*<([A-Za-z][\w-]*)\b[^>]*>[\s\S]*<\/\1>\s*$/;
|
|
85
|
+
function projectFlueMessages(value) {
|
|
86
|
+
if (!Array.isArray(value)) return [];
|
|
87
|
+
const out = [];
|
|
88
|
+
for (const entry of value) {
|
|
89
|
+
const message = projectFlueMessage(entry);
|
|
90
|
+
if (message) out.push(message);
|
|
91
|
+
}
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
function projectFlueMessage(value) {
|
|
95
|
+
if (!isRecord(value)) return void 0;
|
|
96
|
+
const role = value.role;
|
|
97
|
+
if (typeof role !== "string" || !FLUE_ROLES.has(role)) return void 0;
|
|
98
|
+
const text = flueMessageText(value.content);
|
|
99
|
+
if (text === "") return void 0;
|
|
100
|
+
if (role === "user" && SIGNAL_MESSAGE.test(text)) return void 0;
|
|
101
|
+
return { role, parts: [{ type: "text", content: text }] };
|
|
102
|
+
}
|
|
103
|
+
function flueMessageText(content) {
|
|
104
|
+
if (typeof content === "string") return content;
|
|
105
|
+
if (!Array.isArray(content)) return "";
|
|
106
|
+
const texts = [];
|
|
107
|
+
for (const part of content) {
|
|
108
|
+
if (!isRecord(part) || part.type !== "text") continue;
|
|
109
|
+
if (typeof part.text === "string" && part.text.length > 0) texts.push(part.text);
|
|
110
|
+
}
|
|
111
|
+
return texts.join("\n");
|
|
112
|
+
}
|
|
113
|
+
function normalizeContentMessages(value) {
|
|
114
|
+
if (!Array.isArray(value)) return [];
|
|
115
|
+
const out = [];
|
|
116
|
+
for (const entry of value) {
|
|
117
|
+
if (!isRecord(entry)) continue;
|
|
118
|
+
const role = entry.role;
|
|
119
|
+
if (typeof role !== "string" || !ROLES.has(role)) continue;
|
|
120
|
+
const text = contentMessageText(entry);
|
|
121
|
+
if (text === "") continue;
|
|
122
|
+
out.push({ role, parts: [{ type: "text", content: text }] });
|
|
123
|
+
}
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
function contentTextOf(value) {
|
|
127
|
+
if (typeof value === "string") return value;
|
|
128
|
+
return Array.isArray(value) ? contentMessageText({ parts: value }) : "";
|
|
129
|
+
}
|
|
130
|
+
function contentMessageText(message) {
|
|
131
|
+
if (typeof message.content === "string") return message.content;
|
|
132
|
+
const parts = Array.isArray(message.parts) ? message.parts : Array.isArray(message.content) ? message.content : [];
|
|
133
|
+
const texts = [];
|
|
134
|
+
for (const part of parts) {
|
|
135
|
+
if (!isRecord(part) || part.type !== "text") continue;
|
|
136
|
+
const text = typeof part.content === "string" && part.content.length > 0 ? part.content : typeof part.text === "string" && part.text.length > 0 ? part.text : "";
|
|
137
|
+
if (text !== "") texts.push(text);
|
|
138
|
+
}
|
|
139
|
+
return texts.join("\n");
|
|
140
|
+
}
|
|
141
|
+
function isRecord(value) {
|
|
142
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
143
|
+
}
|
|
144
|
+
|
|
81
145
|
// src/content.ts
|
|
82
146
|
var DEFAULT_CONTENT_MAX_CHARS = 65536;
|
|
83
147
|
var INGEST_CONTENT_ATTRIBUTE_CEILING = 262144;
|
|
84
|
-
|
|
148
|
+
var INGEST_CONTENT_SPAN_CEILING = 393216;
|
|
149
|
+
function resolveContentPolicy(setting) {
|
|
150
|
+
const options = setting === false ? void 0 : setting;
|
|
151
|
+
const on = (value) => setting !== false && value !== false;
|
|
85
152
|
const requested = options?.maxChars;
|
|
86
153
|
const maxChars = typeof requested === "number" && Number.isFinite(requested) && requested > 0 ? Math.min(Math.floor(requested), INGEST_CONTENT_ATTRIBUTE_CEILING) : DEFAULT_CONTENT_MAX_CHARS;
|
|
87
154
|
return {
|
|
88
|
-
toolArguments: options?.toolArguments
|
|
89
|
-
toolResults: options?.toolResults
|
|
155
|
+
toolArguments: on(options?.toolArguments),
|
|
156
|
+
toolResults: on(options?.toolResults),
|
|
157
|
+
inputMessages: on(options?.inputMessages),
|
|
158
|
+
outputMessages: on(options?.outputMessages),
|
|
159
|
+
systemInstructions: on(options?.systemInstructions),
|
|
90
160
|
maxChars,
|
|
161
|
+
envelopeMaxChars: Math.min(maxChars, Math.floor(INGEST_CONTENT_SPAN_CEILING / 3)),
|
|
91
162
|
redact: options?.redact
|
|
92
163
|
};
|
|
93
164
|
}
|
|
94
165
|
function encodeContentValue(value, policy, context3) {
|
|
95
166
|
if (value === void 0) return void 0;
|
|
96
|
-
|
|
97
|
-
if (
|
|
98
|
-
try {
|
|
99
|
-
redacted = policy.redact(value, context3);
|
|
100
|
-
} catch {
|
|
101
|
-
return void 0;
|
|
102
|
-
}
|
|
103
|
-
if (redacted === void 0) return void 0;
|
|
104
|
-
}
|
|
167
|
+
const redacted = applyRedact(value, policy, context3);
|
|
168
|
+
if (redacted === void 0) return void 0;
|
|
105
169
|
if (context3.kind === "arguments") return encodeArguments(redacted, policy.maxChars);
|
|
106
170
|
const encoded = stringify(redacted);
|
|
107
171
|
if (encoded === void 0) return void 0;
|
|
108
172
|
return truncate(encoded, policy.maxChars);
|
|
109
173
|
}
|
|
174
|
+
function encodeMessagesValue(messages, policy, kind) {
|
|
175
|
+
if (messages.length === 0) return void 0;
|
|
176
|
+
const redacted = applyRedact(messages, policy, { kind });
|
|
177
|
+
if (redacted === void 0) return void 0;
|
|
178
|
+
const all = normalizeContentMessages(redacted);
|
|
179
|
+
if (all.length === 0) return void 0;
|
|
180
|
+
const maxChars = policy.envelopeMaxChars;
|
|
181
|
+
let start = all.length;
|
|
182
|
+
let total = 2;
|
|
183
|
+
while (start > 0) {
|
|
184
|
+
const length = JSON.stringify(all[start - 1]).length + (start < all.length ? 1 : 0);
|
|
185
|
+
if (total + length > maxChars) break;
|
|
186
|
+
total += length;
|
|
187
|
+
start -= 1;
|
|
188
|
+
}
|
|
189
|
+
if (start === 0) return JSON.stringify(all);
|
|
190
|
+
if (start < all.length) return JSON.stringify(all.slice(start));
|
|
191
|
+
return encodeTruncatedMessage(all[all.length - 1], maxChars);
|
|
192
|
+
}
|
|
193
|
+
function encodeSystemInstructionsValue(value, policy) {
|
|
194
|
+
if (value.length === 0) return void 0;
|
|
195
|
+
const redacted = applyRedact(value, policy, { kind: "system_instructions" });
|
|
196
|
+
if (redacted === void 0) return void 0;
|
|
197
|
+
const text = contentTextOf(redacted);
|
|
198
|
+
if (text.length === 0) return void 0;
|
|
199
|
+
return encodeTextParts(text, policy.envelopeMaxChars);
|
|
200
|
+
}
|
|
201
|
+
function encodeTruncatedMessage(message, maxChars) {
|
|
202
|
+
const text = message.parts.map((part) => part.content).join("\n");
|
|
203
|
+
return fitJson(
|
|
204
|
+
(cut) => JSON.stringify([{ role: message.role, parts: [{ type: "text", content: cut }] }]),
|
|
205
|
+
text,
|
|
206
|
+
maxChars
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
function encodeTextParts(text, maxChars) {
|
|
210
|
+
return fitJson((cut) => JSON.stringify([{ type: "text", content: cut }]), text, maxChars);
|
|
211
|
+
}
|
|
212
|
+
function fitJson(wrap, text, maxChars) {
|
|
213
|
+
let encoded = wrap(text);
|
|
214
|
+
if (encoded.length <= maxChars) return encoded;
|
|
215
|
+
let budget = maxChars - wrap("").length;
|
|
216
|
+
for (let round = 0; round < 8 && budget > 0; round += 1) {
|
|
217
|
+
encoded = wrap(truncate(text, budget));
|
|
218
|
+
if (encoded.length <= maxChars) return encoded;
|
|
219
|
+
budget -= encoded.length - maxChars;
|
|
220
|
+
}
|
|
221
|
+
return void 0;
|
|
222
|
+
}
|
|
223
|
+
function applyRedact(value, policy, context3) {
|
|
224
|
+
if (!policy.redact) return value;
|
|
225
|
+
try {
|
|
226
|
+
return policy.redact(value, context3);
|
|
227
|
+
} catch {
|
|
228
|
+
return void 0;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
110
231
|
function encodeArguments(value, maxChars) {
|
|
111
232
|
const shaped = isPlainObject(value) ? value : { value };
|
|
112
233
|
const encoded = stringify(shaped);
|
|
@@ -154,7 +275,7 @@ function isHighSurrogate(code) {
|
|
|
154
275
|
}
|
|
155
276
|
|
|
156
277
|
// package.json
|
|
157
|
-
var version = "0.
|
|
278
|
+
var version = "0.5.0";
|
|
158
279
|
|
|
159
280
|
// src/semconv.ts
|
|
160
281
|
var GEN_AI = {
|
|
@@ -183,6 +304,9 @@ var GEN_AI = {
|
|
|
183
304
|
serverPort: "server.port"
|
|
184
305
|
};
|
|
185
306
|
var GEN_AI_CONTENT = {
|
|
307
|
+
inputMessages: "gen_ai.input.messages",
|
|
308
|
+
outputMessages: "gen_ai.output.messages",
|
|
309
|
+
systemInstructions: "gen_ai.system_instructions",
|
|
186
310
|
toolCallArguments: "gen_ai.tool.call.arguments",
|
|
187
311
|
toolCallResult: "gen_ai.tool.call.result"
|
|
188
312
|
};
|
|
@@ -336,11 +460,13 @@ function createFlueProjection(options = {}) {
|
|
|
336
460
|
...isEnvelope ? { [GEN_AI.operationName]: GEN_AI_OPERATION_INVOKE_AGENT } : {},
|
|
337
461
|
...isEnvelope && event.agentName ? { [GEN_AI.agentName]: event.agentName } : {},
|
|
338
462
|
...isEnvelope && event.conversationId ? { [GEN_AI.conversationId]: event.conversationId } : {},
|
|
339
|
-
// The execution id LIFTS the tier and is claim-checked at ingest like
|
|
340
|
-
// any inbound id; it does not key the row, which stays derived from
|
|
341
|
-
// the trace id.
|
|
342
463
|
...isEnvelope && event.submissionId ? { [RUNTYPE.executionId]: event.submissionId } : {},
|
|
343
|
-
...isEnvelope && agentId ? { [RUNTYPE.agentId]: agentId } : {}
|
|
464
|
+
...isEnvelope && agentId ? { [RUNTYPE.agentId]: agentId } : {},
|
|
465
|
+
// WHY(README.md): the resource placement is the customer's to wire and is routinely skipped.
|
|
466
|
+
...isEnvelope ? {
|
|
467
|
+
[RUNTYPE.adapterName]: ADAPTER_NAME,
|
|
468
|
+
[RUNTYPE.adapterVersion]: ADAPTER_VERSION
|
|
469
|
+
} : {}
|
|
344
470
|
}
|
|
345
471
|
}
|
|
346
472
|
];
|
|
@@ -397,9 +523,7 @@ function createFlueProjection(options = {}) {
|
|
|
397
523
|
{
|
|
398
524
|
kind: "open",
|
|
399
525
|
ref,
|
|
400
|
-
//
|
|
401
|
-
// agent-invocation span in the trace is a coin flip over which one
|
|
402
|
-
// becomes the run's envelope.
|
|
526
|
+
// WHY(packages/flue-otel/src/projection.ts): Use a task span, not invoke_agent; one trace is persisted as one execution.
|
|
403
527
|
name: agent ? `flue.task ${agent}` : "flue.task",
|
|
404
528
|
spanKind: "internal",
|
|
405
529
|
...parentRef ? { parentRef } : {},
|
|
@@ -439,8 +563,6 @@ function createFlueProjection(options = {}) {
|
|
|
439
563
|
ref,
|
|
440
564
|
name: "flue.compaction",
|
|
441
565
|
spanKind: "internal",
|
|
442
|
-
// `session.compact()` is callable by the host outside any turn, and a
|
|
443
|
-
// `compact` operation is not intercepted, so this can be parentless.
|
|
444
566
|
requiresParent: true,
|
|
445
567
|
...parentRef ? { parentRef } : {},
|
|
446
568
|
...event.timestamp ? { startTime: event.timestamp } : {},
|
|
@@ -505,6 +627,7 @@ function createFlueProjection(options = {}) {
|
|
|
505
627
|
if (!envelope.modelPinned && purpose === "agent")
|
|
506
628
|
envelope.requestModel = request.requestedModel;
|
|
507
629
|
if (Array.isArray(request.input?.tools)) envelope.toolsReported = true;
|
|
630
|
+
if (position?.atEnvelopeScope) captureInput(envelope, request);
|
|
508
631
|
}
|
|
509
632
|
return [
|
|
510
633
|
{
|
|
@@ -550,20 +673,16 @@ function createFlueProjection(options = {}) {
|
|
|
550
673
|
if (purpose === "agent" && response.finishReason) {
|
|
551
674
|
envelope.lastFinishReason = response.finishReason;
|
|
552
675
|
}
|
|
676
|
+
if (content.outputMessages && purpose === "agent" && !event.taskId && !isError) {
|
|
677
|
+
const output = projectFlueMessage(response.output);
|
|
678
|
+
if (output?.role === "assistant") envelope.outputMessage = output;
|
|
679
|
+
}
|
|
553
680
|
}
|
|
554
681
|
const attributes = {
|
|
555
682
|
...response.responseModel ? { [GEN_AI.responseModel]: response.responseModel } : {},
|
|
556
683
|
...response.responseId ? { [GEN_AI.responseId]: response.responseId } : {},
|
|
557
684
|
...response.finishReason ? { [GEN_AI.finishReasons]: [response.finishReason] } : {},
|
|
558
685
|
...usageAttributes(response.usage),
|
|
559
|
-
// Provider-diagnostic pointers, emitted only when Flue actually provides
|
|
560
|
-
// them. Both are optional and provider-dependent (Workers AI attaches
|
|
561
|
-
// both today); an absent value costs one column, a synthesized one would
|
|
562
|
-
// render as a measurement. `gatewayLogId` is a pointer to the content
|
|
563
|
-
// without shipping the content — a customer can click through to that
|
|
564
|
-
// exact request in their own AI Gateway dashboard. `providerFinishReason`
|
|
565
|
-
// is the provider's exact finish value before normalization, which our
|
|
566
|
-
// `GEN_AI.finishReasons` above deliberately hides.
|
|
567
686
|
...response.providerFinishReason ? { [RUNTYPE.providerFinishReason]: response.providerFinishReason } : {},
|
|
568
687
|
...response.gatewayLogId ? { [RUNTYPE.gatewayLogId]: response.gatewayLogId } : {}
|
|
569
688
|
};
|
|
@@ -594,8 +713,6 @@ function createFlueProjection(options = {}) {
|
|
|
594
713
|
ref,
|
|
595
714
|
name: "flue.operation shell",
|
|
596
715
|
spanKind: "internal",
|
|
597
|
-
// `session.shell()` is callable by the host outside any turn, and a
|
|
598
|
-
// `shell` operation is not intercepted, so this can be parentless.
|
|
599
716
|
requiresParent: true,
|
|
600
717
|
...parentRef ? { parentRef } : {},
|
|
601
718
|
...event.timestamp ? { startTime: event.timestamp } : {},
|
|
@@ -627,10 +744,6 @@ function createFlueProjection(options = {}) {
|
|
|
627
744
|
[GEN_AI.toolName]: toolName,
|
|
628
745
|
[GEN_AI.toolCallId]: toolCallId,
|
|
629
746
|
...args !== void 0 ? { [GEN_AI_CONTENT.toolCallArguments]: args } : {},
|
|
630
|
-
// The semconv `gen_ai.tool.type` domain is the transport-level one
|
|
631
|
-
// (`function` / `extension` / `datastore`); a sub-agent delegation is
|
|
632
|
-
// not a plain function call, so it is left unclaimed there while
|
|
633
|
-
// `runtype.tool.type` says what it actually is.
|
|
634
747
|
...isDelegation ? {} : { [GEN_AI.toolType]: "function" },
|
|
635
748
|
...toolType ? { [RUNTYPE.toolType]: toolType } : {},
|
|
636
749
|
...event.origin ? { [FLUE.toolOrigin]: event.origin } : {},
|
|
@@ -689,10 +802,28 @@ function createFlueProjection(options = {}) {
|
|
|
689
802
|
} : {},
|
|
690
803
|
...stopReason ? { [RUNTYPE.stopReason]: stopReason } : {},
|
|
691
804
|
...envelope.toolsReported ? { [RUNTYPE.toolsReported]: true } : {},
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
805
|
+
...envelope.maxIteration !== null ? { [RUNTYPE.iteration]: envelope.maxIteration } : {},
|
|
806
|
+
...envelopeContentAttributes(envelope)
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
function captureInput(envelope, request) {
|
|
810
|
+
const input = request.input;
|
|
811
|
+
if (content.inputMessages) {
|
|
812
|
+
const messages = projectFlueMessages(input?.messages);
|
|
813
|
+
if (messages.length > 0) envelope.inputMessages = messages;
|
|
814
|
+
}
|
|
815
|
+
if (content.systemInstructions && typeof input?.systemPrompt === "string") {
|
|
816
|
+
envelope.systemInstructions = input.systemPrompt;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
function envelopeContentAttributes(envelope) {
|
|
820
|
+
const input = envelope.inputMessages ? encodeMessagesValue(envelope.inputMessages, content, "input_messages") : void 0;
|
|
821
|
+
const system = envelope.systemInstructions ? encodeSystemInstructionsValue(envelope.systemInstructions, content) : void 0;
|
|
822
|
+
const output = envelope.outputMessage ? encodeMessagesValue([envelope.outputMessage], content, "output_messages") : void 0;
|
|
823
|
+
return {
|
|
824
|
+
...input !== void 0 ? { [GEN_AI_CONTENT.inputMessages]: input } : {},
|
|
825
|
+
...system !== void 0 ? { [GEN_AI_CONTENT.systemInstructions]: system } : {},
|
|
826
|
+
...output !== void 0 ? { [GEN_AI_CONTENT.outputMessages]: output } : {}
|
|
696
827
|
};
|
|
697
828
|
}
|
|
698
829
|
function resolveEnvelopeRef(event) {
|
|
@@ -812,9 +943,7 @@ function positionAttributes(position) {
|
|
|
812
943
|
return {
|
|
813
944
|
...position.turnId ? { [RUNTYPE.turnId]: position.turnId } : {},
|
|
814
945
|
[RUNTYPE.turnIndex]: position.index,
|
|
815
|
-
//
|
|
816
|
-
// turns would otherwise inflate the run's iteration count, which ingest
|
|
817
|
-
// computes as the highest position across every span in the trace.
|
|
946
|
+
// WHY(packages/flue-otel/src/projection.ts): Do not stamp delegated turns with root iteration positions; they would inflate the run iteration count.
|
|
818
947
|
...position.atEnvelopeScope ? { [RUNTYPE.iteration]: position.index } : {}
|
|
819
948
|
};
|
|
820
949
|
}
|
|
@@ -840,9 +969,6 @@ function closeIntent(ref, event, isError, errorInfo) {
|
|
|
840
969
|
...isError ? {
|
|
841
970
|
error: {
|
|
842
971
|
type: errorInfo?.type ?? "_OTHER",
|
|
843
|
-
// The class NAME only. `errorInfo.message` and `.stack` are content
|
|
844
|
-
// and this release emits none: a provider message routinely quotes
|
|
845
|
-
// the prompt back, and a stack ships filesystem layout.
|
|
846
972
|
...errorInfo?.name ? { exceptionType: errorInfo.name } : {}
|
|
847
973
|
}
|
|
848
974
|
} : {}
|