@telemetry-dev/bedrock 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "@telemetry-dev/bedrock",
3
+ "version": "0.1.0",
4
+ "description": "AWS Bedrock instrumentation for telemetry.dev: Converse, InvokeModel, and Bedrock Agents spans for AWS SDK v3 clients.",
5
+ "keywords": [
6
+ "aws",
7
+ "bedrock",
8
+ "genai",
9
+ "llm",
10
+ "observability",
11
+ "opentelemetry",
12
+ "telemetry",
13
+ "tracing"
14
+ ],
15
+ "homepage": "https://telemetry.dev",
16
+ "license": "MIT",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/telemetry-dev/telemetry.dev.git",
20
+ "directory": "packages/bedrock"
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "src"
25
+ ],
26
+ "type": "module",
27
+ "exports": {
28
+ ".": {
29
+ "types": "./dist/index.d.mts",
30
+ "import": "./dist/index.mjs",
31
+ "default": "./dist/index.mjs"
32
+ },
33
+ "./agents": {
34
+ "types": "./dist/agents.d.mts",
35
+ "import": "./dist/agents.mjs",
36
+ "default": "./dist/agents.mjs"
37
+ },
38
+ "./package.json": "./package.json"
39
+ },
40
+ "publishConfig": {
41
+ "access": "public"
42
+ },
43
+ "devDependencies": {
44
+ "@aws-sdk/client-bedrock-agent-runtime": "^3.1000.0",
45
+ "@aws-sdk/client-bedrock-runtime": "^3.1000.0",
46
+ "@opentelemetry/api": "^1.9.1",
47
+ "@opentelemetry/sdk-logs": "^0.218.0",
48
+ "@opentelemetry/sdk-trace-base": "^2.7.1",
49
+ "@smithy/eventstream-codec": "^4.2.5",
50
+ "@smithy/protocol-http": "^5.3.10",
51
+ "@smithy/types": "^4.12.0",
52
+ "@types/node": "^25.5.0",
53
+ "@typescript/native-preview": "7.0.0-dev.20260328.1",
54
+ "typescript": "^6.0.2",
55
+ "vite-plus": "0.1.20",
56
+ "vitest": "npm:@voidzero-dev/vite-plus-test@0.1.20",
57
+ "@telemetry-dev/sdk": "^0.1.0"
58
+ },
59
+ "peerDependencies": {
60
+ "@aws-sdk/client-bedrock-agent-runtime": ">=3.600.0 <4",
61
+ "@aws-sdk/client-bedrock-runtime": ">=3.600.0 <4",
62
+ "@telemetry-dev/sdk": "^0.1.0"
63
+ },
64
+ "peerDependenciesMeta": {
65
+ "@aws-sdk/client-bedrock-agent-runtime": {
66
+ "optional": true
67
+ }
68
+ },
69
+ "scripts": {
70
+ "build": "pnpm exec vp pack",
71
+ "dev": "pnpm exec vp pack --watch",
72
+ "test": "vp test",
73
+ "check": "vp check"
74
+ }
75
+ }
@@ -0,0 +1,315 @@
1
+ import type { SpanFields } from "@telemetry-dev/sdk";
2
+
3
+ import {
4
+ arrayValue,
5
+ awsMetadataFields,
6
+ basenameArn,
7
+ bytesToString,
8
+ isRecord,
9
+ mergeFields,
10
+ modeledStreamError,
11
+ numberValue,
12
+ omitUndefined,
13
+ PROVIDER,
14
+ stringValue,
15
+ type BedrockInstrumentationOptions,
16
+ type StreamState,
17
+ } from "./internal.ts";
18
+
19
+ export function agentInputMessages(inputText: unknown): unknown {
20
+ const text = stringValue(inputText);
21
+ return text ? [{ role: "user", parts: [{ type: "text", content: text }] }] : undefined;
22
+ }
23
+
24
+ export function agentRequestFields(input: Record<string, unknown>): SpanFields {
25
+ return omitUndefined({
26
+ provider: PROVIDER,
27
+ input: agentInputMessages(input.inputText),
28
+ agentId: stringValue(input.agentId),
29
+ metadata: omitUndefined({
30
+ bedrock_agent_alias_id: stringValue(input.agentAliasId),
31
+ bedrock_session_id: stringValue(input.sessionId),
32
+ bedrock_memory_id: stringValue(input.memoryId),
33
+ }),
34
+ });
35
+ }
36
+
37
+ export function inlineAgentRequestFields(input: Record<string, unknown>): SpanFields {
38
+ return omitUndefined({
39
+ input: agentInputMessages(input.inputText),
40
+ model: stringValue(input.foundationModel),
41
+ provider: PROVIDER,
42
+ agentName: stringValue(input.agentName),
43
+ metadata: omitUndefined({ bedrock_session_id: stringValue(input.sessionId) }),
44
+ });
45
+ }
46
+
47
+ export class AgentStreamState implements StreamState {
48
+ private text = "";
49
+ private usage: NonNullable<SpanFields["usage"]> = {};
50
+ private traceEventCount = 0;
51
+ private failureReason: string | undefined;
52
+ private guardrailAction: string | undefined;
53
+ private returnControl: unknown;
54
+ private readonly traces: unknown[] = [];
55
+ private error: Error | undefined;
56
+ private errorFields: SpanFields | undefined;
57
+ private citationCount = 0;
58
+ private readonly outputFiles: unknown[] = [];
59
+
60
+ constructor(private readonly options: BedrockInstrumentationOptions) {}
61
+
62
+ feed(event: unknown): void {
63
+ if (!isRecord(event)) return;
64
+ const streamError = modeledStreamError(event);
65
+ if (streamError) {
66
+ this.error = streamError.error;
67
+ this.errorFields = streamError.fields;
68
+ return;
69
+ }
70
+ const chunk = isRecord(event.chunk) ? event.chunk : undefined;
71
+ const bytes = chunk?.bytes;
72
+ if (bytes !== undefined) this.text += bytesToString(bytes) ?? "";
73
+ const attribution = isRecord(chunk?.attribution) ? chunk.attribution : undefined;
74
+ const citations = arrayValue<unknown>(attribution?.citations);
75
+ if (citations) this.citationCount += citations.length;
76
+
77
+ const files = isRecord(event.files) ? arrayValue<unknown>(event.files.files) : undefined;
78
+ if (files) this.outputFiles.push(...files.map(sanitizeOutputFile));
79
+
80
+ const returnControl = isRecord(event.returnControl) ? event.returnControl : undefined;
81
+ if (returnControl) this.returnControl = returnControl;
82
+
83
+ const traceEvent = isRecord(event.trace) ? event.trace : undefined;
84
+ if (traceEvent) {
85
+ this.traceEventCount += 1;
86
+ if (this.options.captureAgentTrace) this.traces.push(traceEvent);
87
+ collectTrace(traceEvent, (usage) => this.addUsage(usage));
88
+ const failure = findKey(traceEvent, "failureTrace");
89
+ if (isRecord(failure))
90
+ this.failureReason = stringValue(failure.failureReason) ?? this.failureReason;
91
+ const guardrail = findKey(traceEvent, "guardrailTrace");
92
+ if (isRecord(guardrail))
93
+ this.guardrailAction = stringValue(guardrail.action) ?? this.guardrailAction;
94
+ }
95
+ }
96
+
97
+ finish(): SpanFields {
98
+ const metadata = omitUndefined({
99
+ trace_event_count: this.traceEventCount || undefined,
100
+ failure_reason: this.failureReason,
101
+ guardrail_action: this.guardrailAction,
102
+ return_control: this.returnControl ? true : undefined,
103
+ citation_count: this.citationCount || undefined,
104
+ output_files: this.outputFiles.length > 0 ? this.outputFiles : undefined,
105
+ agent_trace:
106
+ this.options.captureAgentTrace && this.traces.length > 0 ? this.traces : undefined,
107
+ });
108
+ return mergeFields(
109
+ omitUndefined({
110
+ output: this.returnControl
111
+ ? { returnControl: this.returnControl }
112
+ : this.text
113
+ ? [{ role: "assistant", parts: [{ type: "text", content: this.text }] }]
114
+ : undefined,
115
+ usage: Object.keys(this.usage).length > 0 ? this.usage : undefined,
116
+ metadata,
117
+ error: this.error,
118
+ }),
119
+ this.errorFields,
120
+ );
121
+ }
122
+ private addUsage(value: unknown): void {
123
+ if (!isRecord(value)) return;
124
+ const inputTokens = numberValue(value.inputTokens);
125
+ const outputTokens = numberValue(value.outputTokens);
126
+ const totalTokens = numberValue(value.totalTokens);
127
+ if (inputTokens !== undefined)
128
+ this.usage.inputTokens = (this.usage.inputTokens ?? 0) + inputTokens;
129
+ if (outputTokens !== undefined)
130
+ this.usage.outputTokens = (this.usage.outputTokens ?? 0) + outputTokens;
131
+ if (totalTokens !== undefined)
132
+ this.usage.totalTokens = (this.usage.totalTokens ?? 0) + totalTokens;
133
+ else if (inputTokens !== undefined || outputTokens !== undefined) {
134
+ this.usage.totalTokens = (this.usage.inputTokens ?? 0) + (this.usage.outputTokens ?? 0);
135
+ }
136
+ }
137
+ }
138
+
139
+ function sanitizeOutputFile(value: unknown): unknown {
140
+ if (!isRecord(value)) return value;
141
+ return omitUndefined({
142
+ name: stringValue(value.name),
143
+ type: stringValue(value.type),
144
+ });
145
+ }
146
+
147
+ export function retrieveRequestFields(input: Record<string, unknown>): SpanFields {
148
+ return omitUndefined({
149
+ provider: PROVIDER,
150
+ input: isRecord(input.retrievalQuery) ? input.retrievalQuery : undefined,
151
+ metadata: { knowledge_base_id: stringValue(input.knowledgeBaseId) },
152
+ });
153
+ }
154
+
155
+ export function retrieveResponseFields(output: unknown): SpanFields {
156
+ const result = isRecord(output) ? output : {};
157
+ const retrievalResults = arrayValue<unknown>(result.retrievalResults);
158
+ return mergeFields(awsMetadataFields(result.$metadata), {
159
+ output: retrievalResults,
160
+ metadata: omitUndefined({
161
+ citation_count: retrievalResults?.length,
162
+ guardrail_action: stringValue(result.guardrailAction),
163
+ }),
164
+ });
165
+ }
166
+
167
+ export function ragModel(input: Record<string, unknown>): string | undefined {
168
+ const config = isRecord(input.retrieveAndGenerateConfiguration)
169
+ ? input.retrieveAndGenerateConfiguration
170
+ : undefined;
171
+ const kb = isRecord(config?.knowledgeBaseConfiguration)
172
+ ? config?.knowledgeBaseConfiguration
173
+ : undefined;
174
+ const external = isRecord(config?.externalSourcesConfiguration)
175
+ ? config?.externalSourcesConfiguration
176
+ : undefined;
177
+ return stringValue(kb?.modelArn) ?? stringValue(external?.modelArn);
178
+ }
179
+
180
+ export function ragRequestFields(input: Record<string, unknown>): SpanFields {
181
+ const text = isRecord(input.input) ? stringValue(input.input.text) : undefined;
182
+ return omitUndefined({
183
+ provider: PROVIDER,
184
+ model: ragModel(input),
185
+ input: text ? [{ role: "user", parts: [{ type: "text", content: text }] }] : undefined,
186
+ metadata: omitUndefined({ bedrock_session_id: stringValue(input.sessionId) }),
187
+ });
188
+ }
189
+
190
+ export function ragResponseFields(output: unknown): SpanFields {
191
+ const result = isRecord(output) ? output : {};
192
+ const text = isRecord(result.output) ? stringValue(result.output.text) : undefined;
193
+ return mergeFields(awsMetadataFields(result.$metadata), {
194
+ output: text ? [{ role: "assistant", parts: [{ type: "text", content: text }] }] : undefined,
195
+ metadata: omitUndefined({
196
+ citation_count: arrayValue<unknown>(result.citations)?.length,
197
+ guardrail_action: stringValue(result.guardrailAction),
198
+ bedrock_session_id: stringValue(result.sessionId),
199
+ }),
200
+ });
201
+ }
202
+
203
+ export class RagStreamState implements StreamState {
204
+ private text = "";
205
+ private citationCount = 0;
206
+ private guardrailAction: string | undefined;
207
+ private error: Error | undefined;
208
+ private errorFields: SpanFields | undefined;
209
+
210
+ feed(event: unknown): void {
211
+ if (!isRecord(event)) return;
212
+ const streamError = modeledStreamError(event);
213
+ if (streamError) {
214
+ this.error = streamError.error;
215
+ this.errorFields = streamError.fields;
216
+ return;
217
+ }
218
+ const output = isRecord(event.output) ? event.output : undefined;
219
+ this.text += stringValue(output?.text) ?? "";
220
+ if (event.citation) this.citationCount += 1;
221
+ const guardrail = isRecord(event.guardrail) ? event.guardrail : undefined;
222
+ this.guardrailAction = stringValue(guardrail?.action) ?? this.guardrailAction;
223
+ }
224
+
225
+ finish(): SpanFields {
226
+ return mergeFields(
227
+ omitUndefined({
228
+ output: this.text
229
+ ? [{ role: "assistant", parts: [{ type: "text", content: this.text }] }]
230
+ : undefined,
231
+ metadata: omitUndefined({
232
+ citation_count: this.citationCount || undefined,
233
+ guardrail_action: this.guardrailAction,
234
+ }),
235
+ error: this.error,
236
+ }),
237
+ this.errorFields,
238
+ );
239
+ }
240
+ }
241
+
242
+ export function flowRequestFields(input: Record<string, unknown>): SpanFields {
243
+ return omitUndefined({
244
+ provider: PROVIDER,
245
+ input: input.inputs,
246
+ agentId: stringValue(input.flowIdentifier),
247
+ });
248
+ }
249
+
250
+ export class FlowStreamState implements StreamState {
251
+ private outputs: unknown[] = [];
252
+ private completionReason: string | undefined;
253
+ private error: Error | undefined;
254
+ private errorFields: SpanFields | undefined;
255
+
256
+ feed(event: unknown): void {
257
+ if (!isRecord(event)) return;
258
+ const streamError = modeledStreamError(event);
259
+ if (streamError) {
260
+ this.error = streamError.error;
261
+ this.errorFields = streamError.fields;
262
+ return;
263
+ }
264
+ const output = isRecord(event.flowOutputEvent) ? event.flowOutputEvent : undefined;
265
+ if (output) this.outputs.push(output.content);
266
+ const inputRequest = isRecord(event.flowMultiTurnInputRequestEvent)
267
+ ? event.flowMultiTurnInputRequestEvent
268
+ : undefined;
269
+ if (inputRequest) this.outputs.push(inputRequest.content);
270
+ const completion = isRecord(event.flowCompletionEvent) ? event.flowCompletionEvent : undefined;
271
+ this.completionReason = stringValue(completion?.completionReason) ?? this.completionReason;
272
+ }
273
+
274
+ finish(): SpanFields {
275
+ return mergeFields(
276
+ omitUndefined({
277
+ output: this.outputs.length > 0 ? this.outputs : undefined,
278
+ finishReason: this.completionReason,
279
+ error: this.error,
280
+ }),
281
+ this.errorFields,
282
+ );
283
+ }
284
+ }
285
+
286
+ export function flowName(input: Record<string, unknown>): string {
287
+ return `invoke_flow ${stringValue(input.flowIdentifier) ?? "unknown"}`;
288
+ }
289
+
290
+ export function ragName(input: Record<string, unknown>): string {
291
+ return `retrieve_and_generate ${basenameArn(ragModel(input)) ?? "knowledge-base"}`;
292
+ }
293
+
294
+ function collectTrace(value: unknown, onUsage: (usage: unknown) => void): void {
295
+ if (!isRecord(value)) return;
296
+ const output = isRecord(value.modelInvocationOutput) ? value.modelInvocationOutput : undefined;
297
+ if (output) {
298
+ const metadata = isRecord(output.metadata) ? output.metadata : undefined;
299
+ onUsage(metadata?.usage);
300
+ }
301
+ for (const child of Object.values(value)) {
302
+ if (isRecord(child)) collectTrace(child, onUsage);
303
+ else if (Array.isArray(child)) child.forEach((item) => collectTrace(item, onUsage));
304
+ }
305
+ }
306
+
307
+ function findKey(value: unknown, key: string): unknown {
308
+ if (!isRecord(value)) return undefined;
309
+ if (value[key] !== undefined) return value[key];
310
+ for (const child of Object.values(value)) {
311
+ const found = findKey(child, key);
312
+ if (found !== undefined) return found;
313
+ }
314
+ return undefined;
315
+ }
package/src/agents.ts ADDED
@@ -0,0 +1,69 @@
1
+ import { BedrockAgentRuntimeClient } from "@aws-sdk/client-bedrock-agent-runtime";
2
+
3
+ import { AGENT_HANDLERS } from "./internal-handlers.ts";
4
+ import {
5
+ createInstrumentedSend,
6
+ type BedrockInstrumentationOptions,
7
+ type SendFn,
8
+ } from "./internal.ts";
9
+
10
+ export type { BedrockInstrumentationOptions } from "./internal.ts";
11
+
12
+ export interface WrappableBedrockAgentClient {
13
+ send: (...args: never[]) => unknown;
14
+ }
15
+
16
+ const WRAPPED = new WeakSet<object>();
17
+ let ORIGINAL: { proto: object; hadOwn: boolean; value: unknown; wrapper: SendFn } | undefined;
18
+
19
+ export function wrapBedrockAgents<T extends WrappableBedrockAgentClient>(
20
+ client: T,
21
+ options: BedrockInstrumentationOptions = {},
22
+ ): T {
23
+ if (WRAPPED.has(client as object)) return client;
24
+ const original = client.send as SendFn;
25
+ Object.defineProperty(client, "send", {
26
+ value: createInstrumentedSend(original, options, AGENT_HANDLERS),
27
+ configurable: true,
28
+ writable: true,
29
+ });
30
+ WRAPPED.add(client as object);
31
+ return client;
32
+ }
33
+
34
+ export function instrumentBedrockAgents(options: BedrockInstrumentationOptions = {}): void {
35
+ if (ORIGINAL) return;
36
+ const proto = BedrockAgentRuntimeClient.prototype as unknown as { send: SendFn };
37
+ const hadOwn = Object.prototype.hasOwnProperty.call(proto, "send");
38
+ const original = proto.send;
39
+ const instrumentedSend = createInstrumentedSend(original, options, AGENT_HANDLERS);
40
+ const wrapper = function telemetryDevBedrockAgentSend(
41
+ this: WrappableBedrockAgentClient,
42
+ ...args: unknown[]
43
+ ) {
44
+ if (WRAPPED.has(this as object)) return original.apply(this, args);
45
+ return instrumentedSend.apply(this, args);
46
+ };
47
+ ORIGINAL = { proto, hadOwn, value: original, wrapper };
48
+ Object.defineProperty(proto, "send", {
49
+ configurable: true,
50
+ writable: true,
51
+ value: wrapper,
52
+ });
53
+ }
54
+
55
+ export function uninstrumentBedrockAgents(): void {
56
+ const original = ORIGINAL;
57
+ if (!original) return;
58
+ if (Object.getOwnPropertyDescriptor(original.proto, "send")?.value !== original.wrapper) return;
59
+ ORIGINAL = undefined;
60
+ if (original.hadOwn) {
61
+ Object.defineProperty(original.proto, "send", {
62
+ configurable: true,
63
+ writable: true,
64
+ value: original.value,
65
+ });
66
+ } else {
67
+ Reflect.deleteProperty(original.proto, "send");
68
+ }
69
+ }