@vercel/agent-runs 0.0.0-stage → 0.0.1

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,341 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { otelTelemetry } from "./adapters/otel.js";
3
+ import { aiSdkContentSerializer } from "./adapters/serialization.js";
4
+ import { createScopes, type LiveOperation } from "./core/scopes.js";
5
+ import { currentAgentHandoff } from "./core/delegation.js";
6
+ import { intersectCapture } from "./core/activation.js";
7
+ import { traceSnapshot } from "./core/snapshot.js";
8
+ import { operationHandle, wrappedOperation, type TurnOperation } from "./operations.js";
9
+ import type {
10
+ AgentTelemetry,
11
+ Attributes,
12
+ CaptureDecision,
13
+ ContentSerializer,
14
+ ExecutionContext,
15
+ FrameworkIdentity,
16
+ RunIdentity,
17
+ ScopeRecord,
18
+ TraceCheckpointer,
19
+ TraceErrorHandler,
20
+ TraceLink,
21
+ TraceReference,
22
+ } from "./core/types.js";
23
+
24
+ export interface AgentRunOptions {
25
+ /** Span output and context propagation. Defaults to the global OpenTelemetry provider. */
26
+ readonly telemetry?: AgentTelemetry;
27
+ /**
28
+ * Makes turns durable. Each in-flight turn is saved under a library-chosen
29
+ * key and removed when the turn and its open tool calls complete. Calling
30
+ * `turn()` again with the same identity, in this or a later process,
31
+ * continues the saved turn.
32
+ */
33
+ readonly checkpointer?: TraceCheckpointer;
34
+ /** Defaults to the AI SDK content serializer. */
35
+ readonly serializer?: ContentSerializer;
36
+ readonly onError?: TraceErrorHandler;
37
+ }
38
+
39
+ export interface TurnInput {
40
+ readonly identity: RunIdentity;
41
+ /** Names the invocation span. Omit for an anonymous agent. */
42
+ readonly agentName?: string;
43
+ readonly sequence: number;
44
+ readonly framework?: FrameworkIdentity;
45
+ readonly capture?: CaptureDecision;
46
+ readonly attributes?: Attributes;
47
+ /**
48
+ * Adopts a trace and span ID reserved before the turn started, such as one
49
+ * already returned to a caller. Requires telemetry with stable `ids`.
50
+ */
51
+ readonly reference?: TraceReference;
52
+ /**
53
+ * Nests the turn under a caller's span in the caller's trace. A local
54
+ * handoff sets this for the first turn; remote callees start their own trace.
55
+ */
56
+ readonly parent?: TraceReference;
57
+ readonly links?: readonly TraceLink[];
58
+ readonly startTimeMs?: number;
59
+ /** Delegation lineage recorded by the host, when it is not carried by a handoff. */
60
+ readonly lineage?: {
61
+ readonly parentRunId?: string;
62
+ readonly parentCallId?: string;
63
+ readonly agentName?: string;
64
+ };
65
+ readonly channel?: { readonly kind?: string; readonly origin?: string };
66
+ /** Host context for spans this process starts, such as OpenTelemetry baggage. */
67
+ readonly context?: ExecutionContext;
68
+ }
69
+
70
+ export interface ResumeInput {
71
+ readonly identity: RunIdentity;
72
+ readonly context?: ExecutionContext;
73
+ }
74
+
75
+ interface MemoryInput<T> {
76
+ identity: RunIdentity;
77
+ storeId: string;
78
+ slot: string;
79
+ phase: string;
80
+ capture?: CaptureDecision;
81
+ /** Defaults to the active operation. */
82
+ parent?: TraceReference;
83
+ context?: ExecutionContext;
84
+ describe?: (value: T) => {
85
+ recordCount?: number;
86
+ records?: readonly { id?: string; content: string }[];
87
+ };
88
+ }
89
+
90
+ export interface AgentRun {
91
+ turn: import("./operations.js").WrappedOperation<TurnInput, TurnOperation>;
92
+ /** Returns a saved durable turn, without starting one. */
93
+ resume(input: ResumeInput): Promise<TurnOperation | undefined>;
94
+ memory: AgentMemoryTracing;
95
+ forceFlush(): Promise<void>;
96
+ shutdown(): Promise<void>;
97
+ }
98
+ export interface AgentMemoryTracing {
99
+ search<T>(data: MemoryInput<T>, execute: () => T | PromiseLike<T>): Promise<T>;
100
+ write<T>(data: MemoryInput<T>, execute: () => T | PromiseLike<T>): Promise<T>;
101
+ }
102
+
103
+ const METADATA_ONLY: CaptureDecision = { emit: true, recordInputs: false, recordOutputs: false };
104
+ const LIVE_TURNS = 1000;
105
+
106
+ interface LiveTurn {
107
+ readonly root: LiveOperation;
108
+ /** Token of the last checkpoint this process wrote. */
109
+ revision: string | undefined;
110
+ writes: number;
111
+ }
112
+
113
+ export function createAgentRun(input: AgentRunOptions): AgentRun {
114
+ const telemetry = input.telemetry ?? otelTelemetry();
115
+ const checkpointer = input.checkpointer;
116
+ if (checkpointer !== undefined && telemetry.ids === undefined)
117
+ throw new Error(
118
+ "Durable agent tracing needs stable span IDs. Pass the AgentSpanIdGenerator installed on your tracer provider to otelTelemetry({ idGenerator }).",
119
+ );
120
+ const report: TraceErrorHandler = (error, context) => {
121
+ try {
122
+ input.onError?.(error, context);
123
+ } catch {}
124
+ };
125
+ const scopes = createScopes({
126
+ telemetry,
127
+ serializer: input.serializer ?? aiSdkContentSerializer,
128
+ durable: checkpointer !== undefined,
129
+ onError: input.onError,
130
+ });
131
+ // Turns hydrated in this process, so concurrent hooks share one tree. The
132
+ // checkpoint stays authoritative: another process's write replaces the entry.
133
+ const live = new Map<string, Promise<LiveTurn | undefined>>();
134
+
135
+ function turnKey(identity: RunIdentity): string {
136
+ if (checkpointer === undefined) return `turn:${randomUUID()}`;
137
+ return ["turn", identity.runId, identity.turnId].map(encodeURIComponent).join(":");
138
+ }
139
+
140
+ /** Writes are serialized so a slow store never persists an older tree last. */
141
+ function persistence(key: string, entry: () => LiveTurn | undefined) {
142
+ let queue = Promise.resolve();
143
+ return () => {
144
+ const turn = entry();
145
+ if (checkpointer === undefined || turn === undefined) return queue;
146
+ turn.writes++;
147
+ queue = queue.then(async () => {
148
+ try {
149
+ if (turn.root.settled) {
150
+ live.delete(key);
151
+ await checkpointer.delete(key);
152
+ } else {
153
+ const revision = randomUUID();
154
+ await checkpointer.set(key, traceSnapshot({ ...turn.root.record(), revision }));
155
+ turn.revision = revision;
156
+ }
157
+ } catch (error) {
158
+ report(error, { phase: "checkpoint", operation: "activation" });
159
+ } finally {
160
+ turn.writes--;
161
+ }
162
+ });
163
+ return queue;
164
+ };
165
+ }
166
+
167
+ async function loadCheckpoint(key: string): Promise<unknown> {
168
+ try {
169
+ return await checkpointer!.get(key);
170
+ } catch (error) {
171
+ report(error, { phase: "restore", operation: "activation" });
172
+ return undefined;
173
+ }
174
+ }
175
+
176
+ async function hydrate(
177
+ key: string,
178
+ cached: LiveTurn | undefined,
179
+ open: ((onChange: () => Promise<void>) => Promise<LiveOperation>) | undefined,
180
+ context: ExecutionContext | undefined,
181
+ capture?: CaptureDecision,
182
+ ): Promise<LiveTurn | undefined> {
183
+ const ceiling = currentAgentHandoff()?.capture;
184
+ capture = capture === undefined ? ceiling : intersectCapture(capture, ceiling);
185
+ const saved = checkpointer === undefined ? undefined : await loadCheckpoint(key);
186
+ if (
187
+ cached !== undefined &&
188
+ (cached.writes > 0 || (saved as ScopeRecord | undefined)?.revision === cached.revision)
189
+ ) {
190
+ if (context !== undefined) cached.root.useContext(context);
191
+ // A re-entry may decline content the cached turn was allowed to record.
192
+ if (capture !== undefined) cached.root.narrow(capture);
193
+ return cached;
194
+ }
195
+ let entry: LiveTurn | undefined;
196
+ const onChange = persistence(key, () => entry);
197
+ const restored =
198
+ saved === undefined ? undefined : await scopes.restore(saved, { capture, context, onChange });
199
+ const root = restored ?? (await open?.(onChange));
200
+ if (root === undefined) return undefined;
201
+ entry = { root, revision: (saved as ScopeRecord | undefined)?.revision, writes: 0 };
202
+ if (restored === undefined) await onChange();
203
+ return entry;
204
+ }
205
+
206
+ function cache(
207
+ key: string,
208
+ load: (cached: LiveTurn | undefined) => Promise<LiveTurn | undefined>,
209
+ ): Promise<LiveTurn | undefined> {
210
+ const previous = live.get(key);
211
+ const loading = (async () =>
212
+ load(previous === undefined ? undefined : await previous.catch(() => undefined)))();
213
+ live.set(key, loading);
214
+ void loading.then(
215
+ (turn) => {
216
+ if (turn === undefined || turn.root.settled) live.delete(key);
217
+ },
218
+ () => live.delete(key),
219
+ );
220
+ if (live.size > LIVE_TURNS) live.delete(live.keys().next().value!);
221
+ return loading;
222
+ }
223
+
224
+ function startTurn(data: TurnInput, key: string) {
225
+ return (onChange: () => Promise<void>) => {
226
+ const handoff = currentAgentHandoff();
227
+ const capture = intersectCapture(data.capture ?? METADATA_ONLY, handoff?.capture);
228
+ const first = handoff !== undefined && data.sequence === 0;
229
+ const parent =
230
+ data.parent ?? (first && handoff?.remote !== true ? handoff?.caller : undefined);
231
+ const caller =
232
+ first && handoff?.remote === true
233
+ ? [{ relationship: "agent.dispatch", context: handoff.caller }]
234
+ : [];
235
+ const links = [...caller, ...(data.links ?? [])];
236
+ return scopes.start({
237
+ identity: {
238
+ ...data.identity,
239
+ conversationId: handoff?.conversationId ?? data.identity.conversationId,
240
+ agentName: data.agentName,
241
+ framework: data.framework,
242
+ },
243
+ key,
244
+ capture,
245
+ parent,
246
+ reference: telemetry.ids === undefined ? undefined : data.reference,
247
+ startTimeMs: data.startTimeMs,
248
+ data: {
249
+ type: "activation",
250
+ options: {
251
+ sequence: data.sequence,
252
+ attributes: data.attributes,
253
+ channel: data.channel,
254
+ subagent: handoff !== undefined || data.lineage !== undefined,
255
+ subagentName: handoff?.agentName ?? data.lineage?.agentName,
256
+ parentRunId: handoff?.parentRunId ?? data.lineage?.parentRunId,
257
+ parentCallId: handoff?.parentCallId ?? data.lineage?.parentCallId,
258
+ },
259
+ },
260
+ links: links.length === 0 ? undefined : links,
261
+ context: data.context,
262
+ onChange,
263
+ });
264
+ };
265
+ }
266
+
267
+ function handle(root: LiveOperation): TurnOperation {
268
+ return operationHandle(
269
+ root,
270
+ { type: "activation", options: { sequence: 0 } },
271
+ input.onError,
272
+ ) as TurnOperation;
273
+ }
274
+
275
+ async function turnOperation(data: TurnInput): Promise<TurnOperation> {
276
+ const key = turnKey(data.identity);
277
+ const turn =
278
+ checkpointer === undefined
279
+ ? await hydrate(key, undefined, startTurn(data, key), data.context)
280
+ : await cache(key, (cached) =>
281
+ hydrate(key, cached, startTurn(data, key), data.context, data.capture),
282
+ );
283
+ return handle(turn!.root);
284
+ }
285
+
286
+ async function resume(data: ResumeInput): Promise<TurnOperation | undefined> {
287
+ if (checkpointer === undefined) return undefined;
288
+ const key = turnKey(data.identity);
289
+ const turn = await cache(key, (cached) => hydrate(key, cached, undefined, data.context));
290
+ return turn === undefined ? undefined : handle(turn.root);
291
+ }
292
+
293
+ async function lifecycle(callback: () => Promise<void>) {
294
+ try {
295
+ await callback();
296
+ } catch (error) {
297
+ report(error, { phase: "complete" });
298
+ }
299
+ }
300
+
301
+ async function memory<T>(
302
+ operation: "search_memory" | "upsert_memory",
303
+ data: MemoryInput<T>,
304
+ execute: () => T | PromiseLike<T>,
305
+ ): Promise<T> {
306
+ const active = telemetry.active();
307
+ const handle = await scopes.start({
308
+ identity: data.identity,
309
+ key: `memory:${randomUUID()}`,
310
+ capture: data.capture ?? active?.capture ?? METADATA_ONLY,
311
+ parent: data.parent ?? active?.reference,
312
+ context: data.context,
313
+ data: {
314
+ type: "memory",
315
+ options: { operation, phase: data.phase, slot: data.slot, storeId: data.storeId },
316
+ },
317
+ });
318
+ let value: T;
319
+ try {
320
+ value = await handle.run(execute);
321
+ } catch (error) {
322
+ await handle.fail(error).catch((tracingError) => report(tracingError, { phase: "complete" }));
323
+ throw error;
324
+ }
325
+ await handle
326
+ .complete({ outcome: "completed", ...data.describe?.(value) })
327
+ .catch((error) => report(error, { phase: "complete" }));
328
+ return value;
329
+ }
330
+
331
+ return {
332
+ turn: wrappedOperation(turnOperation, undefined, input.onError),
333
+ resume,
334
+ memory: {
335
+ search: (data, execute) => memory("search_memory", data, execute),
336
+ write: (data, execute) => memory("upsert_memory", data, execute),
337
+ },
338
+ forceFlush: () => lifecycle(() => telemetry.forceFlush()),
339
+ shutdown: () => lifecycle(() => telemetry.shutdown()),
340
+ };
341
+ }
package/src/ai-sdk.ts ADDED
@@ -0,0 +1,7 @@
1
+ /**
2
+ * AI SDK integration. `aiSdkTelemetry` records `generateText`, `streamText`,
3
+ * and AI SDK agents as turns; `modelUsage` and `modelContent` convert AI SDK
4
+ * payloads for hosts that record model calls themselves.
5
+ */
6
+ export { aiSdkTelemetry, type AiSdkTelemetryOptions } from "./adapters/ai-sdk-telemetry.js";
7
+ export { modelUsage, modelContent } from "./adapters/ai-sdk-payload.js";
package/src/capture.ts ADDED
@@ -0,0 +1,13 @@
1
+ import { context, createContextKey } from "@opentelemetry/api";
2
+ import type { CaptureDecision } from "./core/types.js";
3
+
4
+ const CAPTURE = createContextKey("agent.tracing.capture");
5
+ export function currentCapture(): CaptureDecision | undefined {
6
+ return context.active().getValue(CAPTURE) as CaptureDecision | undefined;
7
+ }
8
+ export function withCapture<T extends { setValue(key: symbol, value: unknown): T }>(
9
+ host: T,
10
+ capture: CaptureDecision,
11
+ ): T {
12
+ return host.setValue(CAPTURE, capture);
13
+ }
@@ -0,0 +1,42 @@
1
+ import type { ActiveOperation, CaptureDecision } from "./types.js";
2
+
3
+ export function intersectCapture(
4
+ capture: CaptureDecision,
5
+ ceiling?: CaptureDecision,
6
+ ): CaptureDecision {
7
+ const emit = capture.emit && (ceiling?.emit ?? true);
8
+ return {
9
+ emit,
10
+ recordInputs: emit && capture.recordInputs && (ceiling?.recordInputs ?? true),
11
+ recordOutputs: emit && capture.recordOutputs && (ceiling?.recordOutputs ?? true),
12
+ };
13
+ }
14
+
15
+ export function activeOperation(
16
+ operation: ActiveOperation,
17
+ capture: CaptureDecision,
18
+ ): ActiveOperation {
19
+ const mcp = operation.mcp;
20
+ return {
21
+ type: operation.type,
22
+ reference: operation.reference,
23
+ capture,
24
+ mcp:
25
+ mcp === undefined
26
+ ? undefined
27
+ : {
28
+ update: (input) => mcp.update(input),
29
+ error: (error, type) =>
30
+ mcp.error(
31
+ capture.recordOutputs ? error : undefined,
32
+ type ?? (error instanceof Error ? error.name : undefined),
33
+ ),
34
+ arguments: (value) => {
35
+ if (capture.recordInputs) mcp.arguments(value);
36
+ },
37
+ result: (value) => {
38
+ if (capture.recordOutputs) mcp.result(value);
39
+ },
40
+ },
41
+ };
42
+ }
@@ -0,0 +1,85 @@
1
+ import type { AttributeValue, Attributes, FrameworkIdentity, RunIdentity, Usage } from "./types.js";
2
+ const TRACE_SCHEMA_VERSION = 4;
3
+ export const USAGE_FIELDS = {
4
+ costUsd: "agent.usage.cost_usd",
5
+ inputTokens: "agent.usage.input_tokens",
6
+ outputTokens: "agent.usage.output_tokens",
7
+ cacheReadTokens: "agent.usage.cache_read_tokens",
8
+ cacheWriteTokens: "agent.usage.cache_write_tokens",
9
+ } as const;
10
+
11
+ export function identityAttributes(
12
+ identity: Pick<RunIdentity, "runId" | "conversationId">,
13
+ ): Attributes {
14
+ return {
15
+ "agent.run.id": identity.runId,
16
+ "agent.trace.schema.version": TRACE_SCHEMA_VERSION,
17
+ "gen_ai.conversation.id": identity.conversationId,
18
+ };
19
+ }
20
+
21
+ export function namingAttributes(name: string, operation = name): Attributes {
22
+ return { "operation.name": operation, "resource.name": name };
23
+ }
24
+
25
+ export function frameworkAttributes(framework?: FrameworkIdentity): Attributes {
26
+ return {
27
+ "agent.framework.name": framework?.name || undefined,
28
+ "agent.framework.version": framework?.version || undefined,
29
+ };
30
+ }
31
+
32
+ /** Invocation and model spans use GenAI token counts; structural spans use eve's. */
33
+ export function usageAttributes(usage: Usage, genAi = false): Attributes {
34
+ if (genAi)
35
+ return {
36
+ [USAGE_FIELDS.costUsd]: usage.costUsd,
37
+ "gen_ai.usage.input_tokens": usage.inputTokens,
38
+ "gen_ai.usage.output_tokens": usage.outputTokens,
39
+ "gen_ai.usage.cache_read.input_tokens": usage.inputTokenDetails?.cacheReadTokens,
40
+ "gen_ai.usage.cache_write.input_tokens": usage.inputTokenDetails?.cacheWriteTokens,
41
+ };
42
+ return {
43
+ [USAGE_FIELDS.costUsd]: usage.costUsd,
44
+ [USAGE_FIELDS.inputTokens]: usage.inputTokens,
45
+ [USAGE_FIELDS.outputTokens]: usage.outputTokens,
46
+ [USAGE_FIELDS.cacheReadTokens]: usage.inputTokenDetails?.cacheReadTokens,
47
+ [USAGE_FIELDS.cacheWriteTokens]: usage.inputTokenDetails?.cacheWriteTokens,
48
+ };
49
+ }
50
+
51
+ export function runtimeContextAttributes(
52
+ input: Readonly<Record<string, unknown>> | undefined,
53
+ ): Attributes {
54
+ const attributes: Record<string, Exclude<Attributes[string], undefined>> = {};
55
+ const seen = new WeakSet<object>();
56
+ let remaining = 256;
57
+ function visit(key: string, value: unknown, depth: number): void {
58
+ if (remaining <= 0 || depth > 8 || value == null) return;
59
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
60
+ attributes[key] = value;
61
+ remaining -= 1;
62
+ } else if (Array.isArray(value)) {
63
+ if (
64
+ value.length <= 256 &&
65
+ value.every(
66
+ (item) =>
67
+ typeof item === "string" || typeof item === "number" || typeof item === "boolean",
68
+ ) &&
69
+ new Set(value.map((item) => typeof item)).size === 1
70
+ ) {
71
+ attributes[key] = value as AttributeValue;
72
+ remaining -= 1;
73
+ }
74
+ } else if (typeof value === "object" && !seen.has(value)) {
75
+ seen.add(value);
76
+ for (const keyPart of Object.keys(value)) {
77
+ if (remaining <= 0) break;
78
+ visit(`${key}.${keyPart}`, Reflect.get(value, keyPart), depth + 1);
79
+ }
80
+ seen.delete(value);
81
+ }
82
+ }
83
+ if (input !== undefined) visit("ai.settings.context", input, 0);
84
+ return attributes;
85
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Which span attributes carry conversation content, and in which direction.
3
+ *
4
+ * Two vocabularies land on the same spans: the ones the library sets on its own
5
+ * `agent.*` spans, and the ones the AI SDK's OpenTelemetry integration sets on
6
+ * the model-call spans beneath them. A destination that declined content has to
7
+ * be rid of both, so both are listed here rather than in the module that writes
8
+ * each.
9
+ *
10
+ * Listed by name rather than by prefix because the prefixes are shared with
11
+ * metadata that must survive: `ai.response.finish_reason` and
12
+ * `gen_ai.tool.name` say what happened, not what was said. The cost is that a
13
+ * new content attribute in a dependency is not covered until it is added here.
14
+ */
15
+
16
+ /** Prompts, instructions, tool arguments — what went in. */
17
+ const INPUT_CONTENT_ATTRIBUTES: ReadonlySet<string> = new Set([
18
+ "agent.channel.delivery.input",
19
+ "agent.session.title",
20
+ "agent.approval.request",
21
+ "ai.documents",
22
+ "ai.prompt",
23
+ "ai.prompt.toolChoice",
24
+ "ai.prompt.tools",
25
+ "ai.value",
26
+ "ai.values",
27
+ "gen_ai.input.messages",
28
+ "gen_ai.memory.records",
29
+ "gen_ai.system_instructions",
30
+ "gen_ai.tool.call.arguments",
31
+ "gen_ai.tool.definitions",
32
+ ]);
33
+
34
+ /**
35
+ * Responses, reasoning, tool results — what came out.
36
+ *
37
+ * `ai.toolCall.args` is here rather than above because the AI SDK gates it on
38
+ * `recordOutputs`; matching that is what keeps one destination's view coherent.
39
+ */
40
+ const OUTPUT_CONTENT_ATTRIBUTES: ReadonlySet<string> = new Set([
41
+ "agent.approval.response",
42
+ "ai.embedding",
43
+ "ai.embeddings",
44
+ "ai.ranking",
45
+ "ai.response.files",
46
+ "ai.response.object",
47
+ "ai.response.reasoning",
48
+ "ai.response.text",
49
+ "ai.response.toolCalls",
50
+ "ai.response.tool_calls",
51
+ "ai.response.tool_results",
52
+ "ai.toolCall.args",
53
+ "ai.toolCall.result",
54
+ "gen_ai.output.messages",
55
+ "gen_ai.tool.call.result",
56
+ ]);
57
+
58
+ const INPUT_POLICY_ATTRIBUTE = "agent.trace.content.input";
59
+ const OUTPUT_POLICY_ATTRIBUTE = "agent.trace.content.output";
60
+
61
+ /** What one destination is willing to receive. */
62
+ export interface ResolvedContentOptions {
63
+ readonly recordInputs: boolean;
64
+ readonly recordOutputs: boolean;
65
+ }
66
+
67
+ function isDeclined(key: string, content: ResolvedContentOptions): boolean {
68
+ if (!content.recordInputs && INPUT_CONTENT_ATTRIBUTES.has(key)) return true;
69
+ return !content.recordOutputs && OUTPUT_CONTENT_ATTRIBUTES.has(key);
70
+ }
71
+
72
+ function narrowsContentPolicy(
73
+ attributes: Readonly<Record<string, unknown>>,
74
+ content: ResolvedContentOptions,
75
+ ): boolean {
76
+ return (
77
+ (!content.recordInputs && attributes[INPUT_POLICY_ATTRIBUTE] === true) ||
78
+ (!content.recordOutputs && attributes[OUTPUT_POLICY_ATTRIBUTE] === true)
79
+ );
80
+ }
81
+
82
+ function exportedAttributeValue(
83
+ key: string,
84
+ value: unknown,
85
+ content: ResolvedContentOptions,
86
+ ): unknown {
87
+ if (key === INPUT_POLICY_ATTRIBUTE && value === true && !content.recordInputs) return false;
88
+ if (key === OUTPUT_POLICY_ATTRIBUTE && value === true && !content.recordOutputs) return false;
89
+ return value;
90
+ }
91
+
92
+ /**
93
+ * The attributes with the declined content removed, or `undefined` when there
94
+ * was none to remove.
95
+ *
96
+ * The `undefined` return is what lets the caller forward the original span
97
+ * untouched in the common case, so a destination that declined a direction the
98
+ * span never carried costs one pass over its keys and no allocation.
99
+ */
100
+ export function withoutDeclinedContent(
101
+ attributes: Readonly<Record<string, unknown>>,
102
+ content: ResolvedContentOptions,
103
+ ): Record<string, unknown> | undefined {
104
+ const keys = Object.keys(attributes);
105
+ if (!keys.some((key) => isDeclined(key, content)) && !narrowsContentPolicy(attributes, content)) {
106
+ return undefined;
107
+ }
108
+
109
+ const kept: Record<string, unknown> = {};
110
+ for (const key of keys) {
111
+ if (!isDeclined(key, content)) {
112
+ kept[key] = exportedAttributeValue(key, attributes[key], content);
113
+ }
114
+ }
115
+ return kept;
116
+ }
@@ -0,0 +1,34 @@
1
+ import type { ActiveOperation, AgentTelemetry, ExecutionContext } from "./types.js";
2
+
3
+ /** Runs `execute` exactly once, even when the telemetry cannot enter the context. */
4
+ export function runTraceContext<T>(
5
+ telemetry: Pick<AgentTelemetry, "run" | "suppressed">,
6
+ operation: ActiveOperation,
7
+ execute: () => T,
8
+ host?: ExecutionContext,
9
+ ): T {
10
+ let entered = false;
11
+ try {
12
+ return telemetry.run(
13
+ operation,
14
+ () => {
15
+ entered = true;
16
+ return execute();
17
+ },
18
+ host,
19
+ );
20
+ } catch (error) {
21
+ if (entered) throw error;
22
+ if (telemetry.suppressed !== undefined) {
23
+ try {
24
+ return telemetry.suppressed(() => {
25
+ entered = true;
26
+ return execute();
27
+ });
28
+ } catch (error) {
29
+ if (entered) throw error;
30
+ }
31
+ }
32
+ return execute();
33
+ }
34
+ }
@@ -0,0 +1,20 @@
1
+ import { context, createContextKey } from "@opentelemetry/api";
2
+ import type { CaptureDecision, TraceReference } from "./types.js";
3
+
4
+ export interface AgentHandoff {
5
+ readonly caller: TraceReference;
6
+ readonly conversationId: string;
7
+ readonly parentRunId: string;
8
+ readonly parentCallId: string;
9
+ readonly agentName: string;
10
+ readonly capture: CaptureDecision;
11
+ /** Set when the handoff crossed a process boundary; the callee then starts its own trace. */
12
+ readonly remote?: boolean;
13
+ }
14
+
15
+ const HANDOFF = createContextKey("agent.tracing.handoff");
16
+ export const currentAgentHandoff = () =>
17
+ context.active().getValue(HANDOFF) as AgentHandoff | undefined;
18
+ export function withAgentHandoff<T>(handoff: AgentHandoff, execute: () => T): T {
19
+ return context.with(context.active().setValue(HANDOFF, handoff), execute);
20
+ }