@vercel/agent-runs 0.0.0-stage → 0.0.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.
Files changed (129) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +77 -0
  3. package/README.md +251 -2
  4. package/dist/src/adapters/agent-transport.d.ts +9 -0
  5. package/dist/src/adapters/agent-transport.d.ts.map +1 -0
  6. package/dist/src/adapters/agent-transport.js +87 -0
  7. package/dist/src/adapters/agent-transport.js.map +1 -0
  8. package/dist/src/adapters/ai-sdk-payload.d.ts +16 -0
  9. package/dist/src/adapters/ai-sdk-payload.d.ts.map +1 -0
  10. package/dist/src/adapters/ai-sdk-payload.js +30 -0
  11. package/dist/src/adapters/ai-sdk-payload.js.map +1 -0
  12. package/dist/src/adapters/ai-sdk-telemetry.d.ts +15 -0
  13. package/dist/src/adapters/ai-sdk-telemetry.d.ts.map +1 -0
  14. package/dist/src/adapters/ai-sdk-telemetry.js +151 -0
  15. package/dist/src/adapters/ai-sdk-telemetry.js.map +1 -0
  16. package/dist/src/adapters/otel-error.d.ts +4 -0
  17. package/dist/src/adapters/otel-error.d.ts.map +1 -0
  18. package/dist/src/adapters/otel-error.js +32 -0
  19. package/dist/src/adapters/otel-error.js.map +1 -0
  20. package/dist/src/adapters/otel-ids.d.ts +20 -0
  21. package/dist/src/adapters/otel-ids.d.ts.map +1 -0
  22. package/dist/src/adapters/otel-ids.js +74 -0
  23. package/dist/src/adapters/otel-ids.js.map +1 -0
  24. package/dist/src/adapters/otel.d.ts +34 -0
  25. package/dist/src/adapters/otel.d.ts.map +1 -0
  26. package/dist/src/adapters/otel.js +147 -0
  27. package/dist/src/adapters/otel.js.map +1 -0
  28. package/dist/src/adapters/serialization.d.ts +37 -0
  29. package/dist/src/adapters/serialization.d.ts.map +1 -0
  30. package/dist/src/adapters/serialization.js +318 -0
  31. package/dist/src/adapters/serialization.js.map +1 -0
  32. package/dist/src/agent-run.d.ts +85 -0
  33. package/dist/src/agent-run.d.ts.map +1 -0
  34. package/dist/src/agent-run.js +213 -0
  35. package/dist/src/agent-run.js.map +1 -0
  36. package/dist/src/ai-sdk.d.ts +8 -0
  37. package/dist/src/ai-sdk.d.ts.map +1 -0
  38. package/dist/src/ai-sdk.js +8 -0
  39. package/dist/src/ai-sdk.js.map +1 -0
  40. package/dist/src/capture.d.ts +6 -0
  41. package/dist/src/capture.d.ts.map +1 -0
  42. package/dist/src/capture.js +9 -0
  43. package/dist/src/capture.js.map +1 -0
  44. package/dist/src/core/activation.d.ts +4 -0
  45. package/dist/src/core/activation.d.ts.map +1 -0
  46. package/dist/src/core/activation.js +31 -0
  47. package/dist/src/core/activation.js.map +1 -0
  48. package/dist/src/core/attributes.d.ts +15 -0
  49. package/dist/src/core/attributes.d.ts.map +1 -0
  50. package/dist/src/core/attributes.js +76 -0
  51. package/dist/src/core/attributes.js.map +1 -0
  52. package/dist/src/core/content-policy.d.ts +29 -0
  53. package/dist/src/core/content-policy.d.ts.map +1 -0
  54. package/dist/src/core/content-policy.js +94 -0
  55. package/dist/src/core/content-policy.js.map +1 -0
  56. package/dist/src/core/context.d.ts +4 -0
  57. package/dist/src/core/context.d.ts.map +1 -0
  58. package/dist/src/core/context.js +28 -0
  59. package/dist/src/core/context.js.map +1 -0
  60. package/dist/src/core/delegation.d.ts +14 -0
  61. package/dist/src/core/delegation.d.ts.map +1 -0
  62. package/dist/src/core/delegation.js +7 -0
  63. package/dist/src/core/delegation.js.map +1 -0
  64. package/dist/src/core/mcp.d.ts +25 -0
  65. package/dist/src/core/mcp.d.ts.map +1 -0
  66. package/dist/src/core/mcp.js +33 -0
  67. package/dist/src/core/mcp.js.map +1 -0
  68. package/dist/src/core/scopes.d.ts +60 -0
  69. package/dist/src/core/scopes.d.ts.map +1 -0
  70. package/dist/src/core/scopes.js +574 -0
  71. package/dist/src/core/scopes.js.map +1 -0
  72. package/dist/src/core/snapshot.d.ts +8 -0
  73. package/dist/src/core/snapshot.d.ts.map +1 -0
  74. package/dist/src/core/snapshot.js +207 -0
  75. package/dist/src/core/snapshot.js.map +1 -0
  76. package/dist/src/core/span-kinds.d.ts +13 -0
  77. package/dist/src/core/span-kinds.d.ts.map +1 -0
  78. package/dist/src/core/span-kinds.js +338 -0
  79. package/dist/src/core/span-kinds.js.map +1 -0
  80. package/dist/src/core/types.d.ts +298 -0
  81. package/dist/src/core/types.d.ts.map +1 -0
  82. package/dist/src/core/types.js +2 -0
  83. package/dist/src/core/types.js.map +1 -0
  84. package/dist/src/core/writer.d.ts +16 -0
  85. package/dist/src/core/writer.d.ts.map +1 -0
  86. package/dist/src/core/writer.js +77 -0
  87. package/dist/src/core/writer.js.map +1 -0
  88. package/dist/src/delegation.d.ts +3 -0
  89. package/dist/src/delegation.d.ts.map +1 -0
  90. package/dist/src/delegation.js +3 -0
  91. package/dist/src/delegation.js.map +1 -0
  92. package/dist/src/index.d.ts +7 -0
  93. package/dist/src/index.d.ts.map +1 -0
  94. package/dist/src/index.js +5 -0
  95. package/dist/src/index.js.map +1 -0
  96. package/dist/src/operations.d.ts +97 -0
  97. package/dist/src/operations.d.ts.map +1 -0
  98. package/dist/src/operations.js +185 -0
  99. package/dist/src/operations.js.map +1 -0
  100. package/dist/src/otel.d.ts +13 -0
  101. package/dist/src/otel.d.ts.map +1 -0
  102. package/dist/src/otel.js +13 -0
  103. package/dist/src/otel.js.map +1 -0
  104. package/package.json +75 -3
  105. package/src/adapters/agent-transport.ts +113 -0
  106. package/src/adapters/ai-sdk-payload.ts +44 -0
  107. package/src/adapters/ai-sdk-telemetry.ts +179 -0
  108. package/src/adapters/otel-error.ts +34 -0
  109. package/src/adapters/otel-ids.ts +82 -0
  110. package/src/adapters/otel.ts +232 -0
  111. package/src/adapters/serialization.ts +338 -0
  112. package/src/agent-run.ts +341 -0
  113. package/src/ai-sdk.ts +7 -0
  114. package/src/capture.ts +13 -0
  115. package/src/core/activation.ts +42 -0
  116. package/src/core/attributes.ts +85 -0
  117. package/src/core/content-policy.ts +116 -0
  118. package/src/core/context.ts +34 -0
  119. package/src/core/delegation.ts +20 -0
  120. package/src/core/mcp.ts +60 -0
  121. package/src/core/scopes.ts +761 -0
  122. package/src/core/snapshot.ts +245 -0
  123. package/src/core/span-kinds.ts +406 -0
  124. package/src/core/types.ts +304 -0
  125. package/src/core/writer.ts +135 -0
  126. package/src/delegation.ts +2 -0
  127. package/src/index.ts +44 -0
  128. package/src/operations.ts +342 -0
  129. package/src/otel.ts +18 -0
@@ -0,0 +1,34 @@
1
+ import { SpanStatusCode, type Span } from "@opentelemetry/api";
2
+ import { boundedTraceError } from "./serialization.js";
3
+ import { truncateTelemetryText } from "./serialization.js";
4
+ import { contentAttribute, textContentAttribute } from "./serialization.js";
5
+
6
+ /** Call with policy-projected errors; undefined retains failure status without content. */
7
+ export function recordAgentSpanError(span: Span, error: unknown, errorType?: string): void {
8
+ span.setAttribute(
9
+ "error.type",
10
+ truncateTelemetryText(errorType ?? (error instanceof Error ? error.name : "_OTHER"), 128),
11
+ );
12
+ // A thrown Error keeps its own name; a wrapped value takes the error type.
13
+ const wrapped = error !== undefined && !(error instanceof Error);
14
+ if (wrapped) {
15
+ const serialized = contentAttribute(error);
16
+ const message =
17
+ typeof error === "object" && error !== null ? Reflect.get(error, "message") : undefined;
18
+ const detail =
19
+ typeof error === "string"
20
+ ? textContentAttribute(error)
21
+ : typeof message === "string"
22
+ ? textContentAttribute(serialized === undefined ? message : `${message}\n${serialized}`)
23
+ : serialized;
24
+ if (detail !== undefined) error = new Error(detail);
25
+ }
26
+ if (error instanceof Error) {
27
+ const bounded = boundedTraceError(error);
28
+ if (wrapped && errorType !== undefined) bounded.name = truncateTelemetryText(errorType, 128);
29
+ span.recordException(bounded);
30
+ span.setStatus({ code: SpanStatusCode.ERROR, message: bounded.message });
31
+ } else {
32
+ span.setStatus({ code: SpanStatusCode.ERROR });
33
+ }
34
+ }
@@ -0,0 +1,82 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ /**
4
+ * ID generator installed by the host OTel provider for reserved-ID emission. A span
5
+ * whose lifetime crosses durable worker boundaries (`invoke_agent`) is emitted
6
+ * at its terminal; priming the next span id lets that span carry the
7
+ * pre-allocated id its descendants already parented to.
8
+ */
9
+ export class AgentSpanIdGenerator {
10
+ #primedSpanId: string | undefined;
11
+ #primedTraceId: string | undefined;
12
+
13
+ /** Reserves a span id for a span emitted later via {@link withSpanId}. */
14
+ allocateSpanId(): string {
15
+ return randomHexId(16);
16
+ }
17
+
18
+ /** Derives one span id for a replay-stable instrumentation event. */
19
+ deriveSpanId(key: string): string {
20
+ return deriveAgentSpanId(key);
21
+ }
22
+
23
+ deriveTraceId(key: string): string {
24
+ const traceId = createHash("sha256").update(key).digest("hex").slice(0, 32);
25
+ return /^0+$/u.test(traceId) ? "00000000000000000000000000000001" : traceId;
26
+ }
27
+
28
+ generateSpanId(): string {
29
+ const primed = this.#primedSpanId;
30
+ if (primed !== undefined) {
31
+ this.#primedSpanId = undefined;
32
+ return primed;
33
+ }
34
+ return randomHexId(16);
35
+ }
36
+
37
+ generateTraceId(): string {
38
+ const primed = this.#primedTraceId;
39
+ if (primed !== undefined) {
40
+ this.#primedTraceId = undefined;
41
+ return primed;
42
+ }
43
+ return randomHexId(32);
44
+ }
45
+
46
+ /** Runs `startSpan` so the next span started carries `spanId`. */
47
+ withSpanId<T>(spanId: string, startSpan: () => T): T {
48
+ this.#primedSpanId = spanId;
49
+ try {
50
+ return startSpan();
51
+ } finally {
52
+ this.#primedSpanId = undefined;
53
+ }
54
+ }
55
+
56
+ withTraceId<T>(traceId: string, startSpan: () => T): T {
57
+ this.#primedTraceId = traceId;
58
+ try {
59
+ return startSpan();
60
+ } finally {
61
+ this.#primedTraceId = undefined;
62
+ }
63
+ }
64
+ }
65
+
66
+ function deriveAgentSpanId(key: string): string {
67
+ const spanId = createHash("sha256").update(key).digest("hex").slice(0, 16);
68
+ return /^0+$/u.test(spanId) ? "0000000000000001" : spanId;
69
+ }
70
+
71
+ const HEX_DIGITS = "0123456789abcdef";
72
+
73
+ function randomHexId(length: number): string {
74
+ let id: string;
75
+ do {
76
+ id = "";
77
+ for (let index = 0; index < length; index += 1) {
78
+ id += HEX_DIGITS[Math.trunc(Math.random() * 16)];
79
+ }
80
+ } while (!/[1-9a-f]/u.test(id));
81
+ return id;
82
+ }
@@ -0,0 +1,232 @@
1
+ import {
2
+ ROOT_CONTEXT,
3
+ SpanKind,
4
+ SpanStatusCode,
5
+ context,
6
+ trace,
7
+ createTraceState,
8
+ createContextKey,
9
+ type SpanContext,
10
+ type Context as OtelContext,
11
+ type TracerProvider,
12
+ } from "@opentelemetry/api";
13
+ import { recordAgentSpanError } from "./otel-error.js";
14
+ import { AgentSpanIdGenerator } from "./otel-ids.js";
15
+ import type {
16
+ Attributes,
17
+ AgentTelemetry,
18
+ OutputMapping,
19
+ PreparedSpan,
20
+ SpanWriter,
21
+ TraceReference,
22
+ ExecutionContext,
23
+ ActiveOperation,
24
+ } from "../core/types.js";
25
+ import { activeOperation, intersectCapture } from "../core/activation.js";
26
+ import { withCapture } from "../capture.js";
27
+
28
+ type Context = OtelContext;
29
+ const ACTIVE_OPERATION = createContextKey("agent.tracing.active-operation");
30
+ const SUPPRESS_TRACING_KEY = createContextKey("OpenTelemetry SDK Context Key SUPPRESS_TRACING");
31
+
32
+ export function withErrorContent<T extends { setValue(key: symbol, value: unknown): T }>(
33
+ context: T,
34
+ allowed: boolean,
35
+ ): T {
36
+ return withCapture(context, { emit: true, recordInputs: allowed, recordOutputs: allowed });
37
+ }
38
+
39
+ function suppressTracing(context: Context): Context {
40
+ return context.setValue(SUPPRESS_TRACING_KEY, true);
41
+ }
42
+
43
+ export function activeTraceOperation(
44
+ host: Pick<Context, "getValue"> = context.active(),
45
+ ): ActiveOperation | undefined {
46
+ return typeof host.getValue === "function"
47
+ ? (host.getValue(ACTIVE_OPERATION) as ActiveOperation | undefined)
48
+ : undefined;
49
+ }
50
+
51
+ function parentContext(reference: TraceReference | undefined, host?: ExecutionContext): Context {
52
+ const base = (host as Context | undefined) ?? ROOT_CONTEXT;
53
+ return reference === undefined
54
+ ? base
55
+ : trace.setSpan(base, trace.wrapSpanContext(otelReference(reference)));
56
+ }
57
+
58
+ function otelReference(reference: TraceReference): SpanContext {
59
+ return {
60
+ ...reference,
61
+ traceState:
62
+ reference.tracestate === undefined ? undefined : createTraceState(reference.tracestate),
63
+ };
64
+ }
65
+
66
+ function portableReference(reference: SpanContext): TraceReference {
67
+ return {
68
+ traceId: reference.traceId,
69
+ spanId: reference.spanId,
70
+ traceFlags: reference.traceFlags,
71
+ isRemote: reference.isRemote,
72
+ tracestate: reference.traceState?.serialize(),
73
+ };
74
+ }
75
+
76
+ function mappedAttributes(
77
+ mapping: OutputMapping | undefined,
78
+ span: PreparedSpan,
79
+ attributes: Attributes,
80
+ ): Attributes {
81
+ return mapping?.attributes(span, attributes) ?? attributes;
82
+ }
83
+
84
+ /**
85
+ * Applied when the host passes no mapping. On Vercel it adds the session ID
86
+ * Agent Runs uses to find a run's spans.
87
+ */
88
+ const DEFAULT_MAPPING: OutputMapping = {
89
+ attributes(_span, attributes) {
90
+ const runId = attributes["agent.run.id"];
91
+ return onVercel() && typeof runId === "string" && attributes["vercel.session_id"] === undefined
92
+ ? { ...attributes, "vercel.session_id": runId }
93
+ : attributes;
94
+ },
95
+ link: (_span, link) => ({ "agent.link.type": link.relationship }),
96
+ };
97
+
98
+ function onVercel(): boolean {
99
+ const env = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process
100
+ ?.env;
101
+ return env?.VERCEL_ENV !== undefined;
102
+ }
103
+
104
+ export interface OtelTelemetryOptions {
105
+ /** Defaults to the global tracer provider. */
106
+ readonly provider?: TracerProvider;
107
+ /**
108
+ * Generator installed on `provider`. Required for durable tracing so a span
109
+ * started in a later process carries the ID its children already reference.
110
+ */
111
+ readonly idGenerator?: AgentSpanIdGenerator;
112
+ /** Defaults to recording every trace. */
113
+ readonly samplesTrace?: (
114
+ traceId: string,
115
+ operation: { name: string; attributes: Attributes },
116
+ ) => boolean;
117
+ /** Replaces the default mapping, which adds `vercel.session_id` on Vercel. */
118
+ readonly mapping?: OutputMapping;
119
+ readonly tracerName?: string;
120
+ /** Recorded as the spans' instrumentation scope version. */
121
+ readonly tracerVersion?: string;
122
+ /** Override the provider's lifecycle, for example to flush metric readers too. */
123
+ readonly forceFlush?: () => Promise<void>;
124
+ readonly shutdown?: () => Promise<void>;
125
+ }
126
+
127
+ /** OpenTelemetry implementation of {@link AgentTelemetry}. */
128
+ export function otelTelemetry(options: OtelTelemetryOptions = {}): AgentTelemetry {
129
+ const provider = options.provider ?? trace.getTracerProvider();
130
+ const tracer = provider.getTracer(options.tracerName ?? "agent.tracing", options.tracerVersion);
131
+ const mapping = options.mapping ?? DEFAULT_MAPPING;
132
+ const idGenerator = options.idGenerator;
133
+ function lifecycle(method: "forceFlush" | "shutdown"): () => Promise<void> {
134
+ return (
135
+ options[method] ??
136
+ (async () => {
137
+ const callback = (
138
+ provider as TracerProvider & { forceFlush?(): Promise<void>; shutdown?(): Promise<void> }
139
+ )[method];
140
+ if (callback === undefined)
141
+ throw new Error(`The tracer provider does not support ${method}.`);
142
+ await callback.call(provider);
143
+ })
144
+ );
145
+ }
146
+ function start(span: PreparedSpan, executionContext?: ExecutionContext): SpanWriter {
147
+ const recorded = tracer.startSpan(
148
+ span.name,
149
+ {
150
+ attributes: mappedAttributes(mapping, span, span.attributes),
151
+ kind: span.kind === undefined ? undefined : SpanKind[span.kind],
152
+ root: span.root,
153
+ startTime: span.startTimeMs,
154
+ links: span.links?.map((link) => ({
155
+ context: otelReference(link.context),
156
+ attributes: mapping?.link(span, link) ?? { "agent.link.type": link.relationship },
157
+ })),
158
+ },
159
+ parentContext(span.root ? undefined : span.parent, executionContext),
160
+ );
161
+ return {
162
+ reference: portableReference(recorded.spanContext()),
163
+ setAttribute(key, value) {
164
+ for (const [name, mapped] of Object.entries(
165
+ mappedAttributes(mapping, span, { [key]: value }),
166
+ )) {
167
+ if (mapped !== undefined) recorded.setAttribute(name, mapped);
168
+ }
169
+ },
170
+ addEvent: (name, attributes, timeMs) => {
171
+ recorded.addEvent(name, attributes, timeMs);
172
+ },
173
+ fail: (error, errorType) => recordAgentSpanError(recorded, error, errorType),
174
+ setStatus: (code) => {
175
+ recorded.setStatus({ code: SpanStatusCode[code] });
176
+ },
177
+ end: (timeMs) => {
178
+ recorded.end(timeMs);
179
+ },
180
+ };
181
+ }
182
+ return {
183
+ startSpan(span, { reference, context: host } = {}) {
184
+ if (reference === undefined || idGenerator === undefined) return start(span, host);
185
+ const writer = idGenerator.withTraceId(reference.traceId, () =>
186
+ idGenerator.withSpanId(reference.spanId, () => start(span, host)),
187
+ );
188
+ if (
189
+ writer.reference.spanId !== reference.spanId ||
190
+ writer.reference.traceId !== reference.traceId
191
+ ) {
192
+ throw new Error("The tracer provider must use the telemetry's ID generator.");
193
+ }
194
+ return writer;
195
+ },
196
+ active: () => activeTraceOperation(),
197
+ suppressed<T>(execute: () => T): T {
198
+ return context.with(withErrorContent(suppressTracing(ROOT_CONTEXT), false), execute);
199
+ },
200
+ run(operation, execute, executionContext) {
201
+ let capture = intersectCapture(operation.capture, activeTraceOperation()?.capture);
202
+ // Retain baggage and host context while replacing the semantic parent span.
203
+ let active = trace.setSpan(
204
+ (executionContext as Context | undefined) ?? context.active(),
205
+ trace.wrapSpanContext(otelReference(operation.reference)),
206
+ );
207
+ capture = intersectCapture(
208
+ capture,
209
+ (active.getValue(ACTIVE_OPERATION) as ActiveOperation | undefined)?.capture,
210
+ );
211
+ active = active.setValue(ACTIVE_OPERATION, activeOperation(operation, capture));
212
+ active = withCapture(active, capture);
213
+ if (!capture.emit || (operation.reference.traceFlags & 1) === 0)
214
+ active = suppressTracing(active);
215
+ return context.with(active, execute);
216
+ },
217
+ samples: (traceId, span) =>
218
+ options.samplesTrace?.(traceId, {
219
+ name: span.name,
220
+ attributes: mappedAttributes(mapping, span, span.attributes),
221
+ }) ?? true,
222
+ ids:
223
+ idGenerator === undefined
224
+ ? undefined
225
+ : {
226
+ traceId: (key) => idGenerator.deriveTraceId(key),
227
+ spanId: (key) => idGenerator.deriveSpanId(key),
228
+ },
229
+ forceFlush: lifecycle("forceFlush"),
230
+ shutdown: lifecycle("shutdown"),
231
+ };
232
+ }
@@ -0,0 +1,338 @@
1
+ /**
2
+ * Serializes model and tool payloads into span content attributes for the
3
+ * local trace viewer: prompt messages, the system prompt, responses, and
4
+ * tool arguments/results. Everything is capped so a giant payload cannot
5
+ * bloat a span segment past the reader's per-file limit, and message
6
+ * payloads stay valid JSON through every truncation path.
7
+ */
8
+
9
+ /** Content attributes are capped so a giant payload cannot bloat a span segment. */
10
+ export const CONTENT_ATTRIBUTE_LIMIT = 32 * 1024;
11
+ import type { ContentSerializer, InputMessagesContent } from "../core/types.js";
12
+
13
+ export const aiSdkContentSerializer: ContentSerializer = {
14
+ json: contentAttribute,
15
+ text: textContentAttribute,
16
+ inputMessages: (messages) =>
17
+ genAiInputMessagesAttribute(messages, (value) =>
18
+ typeof value === "string" ? value : undefined,
19
+ ),
20
+ instructions: genAiSystemInstructionsAttribute,
21
+ toolDefinitions: genAiToolDefinitionsAttribute,
22
+ outputMessages: genAiOutputMessagesAttribute,
23
+ toolResults: toolResultsContentAttribute,
24
+ };
25
+
26
+ const encoder = new TextEncoder();
27
+ const decoder = new TextDecoder();
28
+ const MARKER = "... [truncated]";
29
+
30
+ export function telemetryByteLength(text: string): number {
31
+ return encoder.encode(text).length;
32
+ }
33
+
34
+ export function truncateTelemetryText(text: string, maxBytes: number): string {
35
+ const prefix = text.slice(0, maxBytes + 1);
36
+ const bytes = encoder.encode(prefix);
37
+ if (prefix.length === text.length && bytes.length <= maxBytes) return text;
38
+ const marker = MARKER.slice(0, maxBytes);
39
+ let end = Math.max(0, maxBytes - marker.length);
40
+ while (end > 0 && (bytes[end]! & 0xc0) === 0x80) end -= 1;
41
+ return decoder.decode(bytes.subarray(0, end)) + marker;
42
+ }
43
+
44
+ export function boundedPrincipalId(value: unknown): string | undefined {
45
+ return typeof value === "string" &&
46
+ value.length > 0 &&
47
+ value.length <= 1024 &&
48
+ telemetryByteLength(value) <= 1024
49
+ ? value
50
+ : undefined;
51
+ }
52
+
53
+ export function boundedTraceError(value: Error): Error {
54
+ const error = new Error(truncateTelemetryText(value.message, 4096));
55
+ error.name = truncateTelemetryText(value.name, 128);
56
+ error.stack = value.stack === undefined ? undefined : truncateTelemetryText(value.stack, 8192);
57
+ return error;
58
+ }
59
+
60
+ /** Serializes one payload value as capped JSON. */
61
+ export function contentAttribute(value: unknown): string | undefined {
62
+ if (value === undefined) return undefined;
63
+ let json: string | undefined;
64
+ try {
65
+ json = JSON.stringify(value);
66
+ } catch {
67
+ return undefined;
68
+ }
69
+ if (json === undefined) return undefined;
70
+ return textContentAttribute(json);
71
+ }
72
+
73
+ /** Serializes model messages using the OpenTelemetry GenAI message schema. */
74
+ export function genAiInputMessagesAttribute(
75
+ messages: unknown,
76
+ messageKind?: (value: unknown) => string | undefined,
77
+ ): InputMessagesContent | undefined {
78
+ if (!Array.isArray(messages)) return undefined;
79
+ const formatted = messages.flatMap((message) => {
80
+ if (!isRecord(message) || message.role === "system" || typeof message.role !== "string") {
81
+ return [];
82
+ }
83
+ const parts = semanticParts(message.content);
84
+ if (parts.length === 0) return [];
85
+ const kind = message.role === "user" ? messageKind?.(message.kind) : undefined;
86
+ return [
87
+ kind === undefined ? { parts, role: message.role } : { kind, parts, role: message.role },
88
+ ];
89
+ });
90
+ for (let start = 0; start < formatted.length; start += 1) {
91
+ const json = semanticJsonAttribute(formatted.slice(start));
92
+ if (json !== undefined) return { json, omitted: start };
93
+ }
94
+ const newest = truncateSingleSemanticMessage(formatted);
95
+ if (newest !== undefined) return { json: newest, omitted: formatted.length - 1 };
96
+ return { json: "[]", omitted: formatted.length };
97
+ }
98
+
99
+ /** Serializes the system prompt using the OpenTelemetry GenAI instruction schema. */
100
+ export function genAiSystemInstructionsAttribute(instructions: unknown): string | undefined {
101
+ const text = systemPromptText(instructions);
102
+ if (text === undefined) return undefined;
103
+ return fitSemanticText(text, (content) => [{ content, type: "text" }]);
104
+ }
105
+
106
+ /** Serializes one model response using the OpenTelemetry GenAI message schema. */
107
+ export function genAiOutputMessagesAttribute(
108
+ content: readonly unknown[],
109
+ finishReason: string,
110
+ ): string | undefined {
111
+ const parts = semanticParts(content);
112
+ const reason = finishReason === "tool-calls" ? "tool_call" : finishReason;
113
+ const message = (messageParts: unknown) => [
114
+ { finish_reason: reason, parts: messageParts, role: "assistant" },
115
+ ];
116
+ // An oversized response keeps a truncated text form, as an oversized prompt does.
117
+ return (
118
+ semanticJsonAttribute(message(parts)) ??
119
+ fitSemanticText(
120
+ semanticMessageText(parts),
121
+ (text) => message([{ content: text, type: "text" }]),
122
+ true,
123
+ )
124
+ );
125
+ }
126
+
127
+ /** Serializes memory records using the OpenTelemetry GenAI memory-records schema. */
128
+ export function genAiMemoryRecordsAttribute(records: readonly unknown[]): string | undefined {
129
+ return semanticJsonAttribute(records);
130
+ }
131
+
132
+ /**
133
+ * Serializes provider-executed tool results, keeping the attribute valid
134
+ * JSON under the cap: when the full payload is too big, each entry's input
135
+ * and output collapse to capped text at progressively smaller budgets
136
+ * instead of cutting the JSON mid-string.
137
+ */
138
+ export function toolResultsContentAttribute(
139
+ results: readonly Record<string, unknown>[],
140
+ ): string | undefined {
141
+ if (results.length === 0) return undefined;
142
+ const full = stringifyContent(results);
143
+ if (full !== undefined && full.length <= CONTENT_ATTRIBUTE_LIMIT) return full;
144
+ for (let cap = CONTENT_ATTRIBUTE_LIMIT; cap >= 0; cap = cap >= 256 ? Math.floor(cap / 2) : -1) {
145
+ const json = stringifyContent(results.map((entry) => cappedToolResult(entry, cap)));
146
+ if (json !== undefined && json.length <= CONTENT_ATTRIBUTE_LIMIT) return json;
147
+ }
148
+ return undefined;
149
+ }
150
+
151
+ function cappedToolResult(entry: Record<string, unknown>, cap: number): Record<string, unknown> {
152
+ const out: Record<string, unknown> = { toolName: entry.toolName };
153
+ for (const key of ["input", "output", "error"]) {
154
+ if (!(key in entry)) continue;
155
+ const value = entry[key];
156
+ const text = typeof value === "string" ? value : (stringifyContent(value) ?? "");
157
+ out[key] = text.length <= cap ? text : `${text.slice(0, cap)}… [truncated]`;
158
+ }
159
+ return out;
160
+ }
161
+
162
+ /** Serializes tool definitions, dropping trailing entries to keep the JSON whole. */
163
+ export function genAiToolDefinitionsAttribute(tools: unknown): string | undefined {
164
+ if (!Array.isArray(tools)) return undefined;
165
+ const definitions = tools.flatMap((tool): Record<string, unknown>[] => {
166
+ if (!isRecord(tool) || typeof tool.name !== "string") return [];
167
+ const definition: Record<string, unknown> = { name: tool.name };
168
+ if (typeof tool.description === "string") definition.description = tool.description;
169
+ if (tool.inputSchema !== undefined) definition.parameters = tool.inputSchema;
170
+ return [definition];
171
+ });
172
+ if (definitions.length === 0) return undefined;
173
+
174
+ const serialized: string[] = [];
175
+ let length = 2;
176
+ for (const definition of definitions) {
177
+ const json = stringifyContent(definition);
178
+ if (json === undefined) break;
179
+ const nextLength = length + (serialized.length === 0 ? 0 : 1) + json.length;
180
+ if (nextLength > CONTENT_ATTRIBUTE_LIMIT) break;
181
+ serialized.push(json);
182
+ length = nextLength;
183
+ }
184
+ return `[${serialized.join(",")}]`;
185
+ }
186
+
187
+ /** Normalizes the AI SDK's `instructions` prompt to plain text. */
188
+ function systemPromptText(instructions: unknown): string | undefined {
189
+ if (typeof instructions === "string") return instructions.length === 0 ? undefined : instructions;
190
+ if (!isRecord(instructions) && !Array.isArray(instructions)) return undefined;
191
+ const messages = Array.isArray(instructions) ? instructions : [instructions];
192
+ const texts: string[] = [];
193
+ for (const message of messages) {
194
+ if (!isRecord(message)) continue;
195
+ if (typeof message.content === "string") texts.push(message.content);
196
+ else if (Array.isArray(message.content))
197
+ for (const part of message.content)
198
+ if (isRecord(part) && part.type === "text" && typeof part.text === "string")
199
+ texts.push(part.text);
200
+ }
201
+ const joined = texts.join("\n\n").trim();
202
+ return joined.length === 0 ? undefined : joined;
203
+ }
204
+
205
+ /** Caps plain text, marking the cut. */
206
+ export function textContentAttribute(text: string): string | undefined {
207
+ if (text.length === 0) return undefined;
208
+ return text.length <= CONTENT_ATTRIBUTE_LIMIT
209
+ ? text
210
+ : `${text.slice(0, CONTENT_ATTRIBUTE_LIMIT)}… [truncated]`;
211
+ }
212
+
213
+ function stringifyContent(value: unknown): string | undefined {
214
+ try {
215
+ return JSON.stringify(value);
216
+ } catch {
217
+ return undefined;
218
+ }
219
+ }
220
+
221
+ function semanticJsonAttribute(value: unknown): string | undefined {
222
+ const json = stringifyContent(value);
223
+ return json !== undefined && json.length <= CONTENT_ATTRIBUTE_LIMIT ? json : undefined;
224
+ }
225
+
226
+ function truncateSingleSemanticMessage(
227
+ messages: readonly Record<string, unknown>[],
228
+ ): string | undefined {
229
+ const message = messages.at(-1);
230
+ if (message === undefined || typeof message.role !== "string") return undefined;
231
+ const text = semanticMessageText(message.parts);
232
+ const base =
233
+ typeof message.kind === "string"
234
+ ? { kind: message.kind, role: message.role }
235
+ : { role: message.role };
236
+ return fitSemanticText(
237
+ text,
238
+ (content) => [{ ...base, parts: [{ content, type: "text" }] }],
239
+ true,
240
+ );
241
+ }
242
+
243
+ function fitSemanticText(
244
+ text: string,
245
+ wrap: (content: string) => unknown,
246
+ alwaysMark = false,
247
+ ): string | undefined {
248
+ if (!alwaysMark && text.length <= CONTENT_ATTRIBUTE_LIMIT) {
249
+ const full = semanticJsonAttribute(wrap(text));
250
+ if (full !== undefined) return full;
251
+ }
252
+ for (let length = Math.min(text.length, CONTENT_ATTRIBUTE_LIMIT); length > 0; length -= 256) {
253
+ const json = semanticJsonAttribute(wrap(`${text.slice(0, length)}… [truncated]`));
254
+ if (json !== undefined) return json;
255
+ }
256
+ return semanticJsonAttribute(wrap("… [truncated]"));
257
+ }
258
+
259
+ function semanticMessageText(parts: unknown): string {
260
+ if (!Array.isArray(parts)) return "";
261
+ let text = "";
262
+ for (const part of parts) {
263
+ if (!isRecord(part) || part.type !== "text" || typeof part.content !== "string") continue;
264
+ const separator = text.length === 0 ? "" : "\n";
265
+ const remaining = CONTENT_ATTRIBUTE_LIMIT + 1 - text.length - separator.length;
266
+ if (remaining <= 0) break;
267
+ text += separator + part.content.slice(0, remaining);
268
+ }
269
+ return text;
270
+ }
271
+
272
+ function semanticParts(content: unknown): Record<string, unknown>[] {
273
+ if (typeof content === "string") return [{ content, type: "text" }];
274
+ if (!Array.isArray(content)) return [];
275
+ const parts: Record<string, unknown>[] = [];
276
+ for (const part of content) {
277
+ const formatted = semanticPart(part);
278
+ if (formatted !== undefined) parts.push(formatted);
279
+ }
280
+ return parts;
281
+ }
282
+
283
+ function semanticPart(value: unknown): Record<string, unknown> | undefined {
284
+ if (!isRecord(value) || typeof value.type !== "string") return undefined;
285
+ switch (value.type) {
286
+ case "text":
287
+ case "reasoning": {
288
+ const content = typeof value.text === "string" ? value.text : value.content;
289
+ return typeof content === "string" ? { content, type: value.type } : undefined;
290
+ }
291
+ case "tool-call":
292
+ return {
293
+ arguments: value.input,
294
+ id: stringValue(value.toolCallId) ?? stringValue(value.callId) ?? null,
295
+ name: value.toolName,
296
+ type: "tool_call",
297
+ };
298
+ case "tool-result":
299
+ return {
300
+ id: stringValue(value.toolCallId) ?? stringValue(value.callId) ?? null,
301
+ response: toolResponse(value.output),
302
+ type: "tool_call_response",
303
+ };
304
+ case "tool-error":
305
+ return {
306
+ id: stringValue(value.toolCallId) ?? stringValue(value.callId) ?? null,
307
+ response: value.error instanceof Error ? value.error.message : value.error,
308
+ type: "tool_call_response",
309
+ };
310
+ default:
311
+ return { type: value.type };
312
+ }
313
+ }
314
+
315
+ function toolResponse(output: unknown): unknown {
316
+ if (!isRecord(output)) return output;
317
+ if (
318
+ (output.type === "text" ||
319
+ output.type === "error-text" ||
320
+ output.type === "json" ||
321
+ output.type === "error-json") &&
322
+ "value" in output
323
+ ) {
324
+ return output.value;
325
+ }
326
+ if (output.type === "execution-denied") {
327
+ return { denied: true, reason: output.reason };
328
+ }
329
+ return output;
330
+ }
331
+
332
+ function stringValue(value: unknown): string | undefined {
333
+ return typeof value === "string" ? value : undefined;
334
+ }
335
+
336
+ function isRecord(value: unknown): value is Record<string, unknown> {
337
+ return typeof value === "object" && value !== null && !Array.isArray(value);
338
+ }