From b4a7717f440d1c2988428e78bf45e59775d00a0d Mon Sep 17 00:00:00 2001 From: jsl517 Date: Wed, 7 Jan 2026 11:26:31 -0800 Subject: [PATCH 1/6] update input message --- .../src/OpenAIAgentsTraceProcessor.ts | 24 +++--- .../openai/OpenAIAgentsTraceProcessor.test.ts | 78 ++++++++++++++++++- 2 files changed, 92 insertions(+), 10 deletions(-) diff --git a/packages/agents-a365-observability-extensions-openai/src/OpenAIAgentsTraceProcessor.ts b/packages/agents-a365-observability-extensions-openai/src/OpenAIAgentsTraceProcessor.ts index 0105689f..e549b7be 100644 --- a/packages/agents-a365-observability-extensions-openai/src/OpenAIAgentsTraceProcessor.ts +++ b/packages/agents-a365-observability-extensions-openai/src/OpenAIAgentsTraceProcessor.ts @@ -255,20 +255,26 @@ export class OpenAIAgentsTraceProcessor implements TracingProcessor { // Store the complete _input structure as JSON otelSpan.setAttribute( OpenTelemetryConstants.GEN_AI_INPUT_MESSAGES_KEY, - JSON.stringify(inputObj) + this.buildInputMessages(inputObj) ); + } + } + } - // Get attributes but filter out unwanted ones - const attrs = Utils.getAttributesFromInput(inputObj); - Object.entries(attrs).forEach(([key, value]) => { - if (value !== null && value !== undefined && - key !== Constants.GEN_AI_REQUEST_CONTENT_KEY) { - otelSpan.setAttribute(key, value as string | number | boolean); - } - }); + private buildInputMessages(arr: Array<{role?: string; parts?: Array<{type: string; content: unknown}>}>): string { + const userTexts = []; + for (const message of arr) { + if (message && message.role === "user" && Array.isArray(message.parts)) { + for (const p of message.parts) { + if (p && p.type === "text" && typeof p.content === "string") { + userTexts.push(p.content); + } } } } + return userTexts.length ? JSON.stringify(userTexts) : JSON.stringify(arr); +} + /** * Process generation span data diff --git a/tests/observability/extension/openai/OpenAIAgentsTraceProcessor.test.ts b/tests/observability/extension/openai/OpenAIAgentsTraceProcessor.test.ts index ed477be6..718eb9ca 100644 --- a/tests/observability/extension/openai/OpenAIAgentsTraceProcessor.test.ts +++ b/tests/observability/extension/openai/OpenAIAgentsTraceProcessor.test.ts @@ -161,7 +161,7 @@ describe('OpenAIAgentsTraceProcessor', () => { processor.onSpanStart(agentSpan); - const otelSpans = (processor as any).otelSpans; + const otelSpans = (processor as any).otelSpans; expect(otelSpans.has('agent-1')).toBe(true); processor.onSpanEnd(agentSpan); @@ -550,5 +550,81 @@ describe('OpenAIAgentsTraceProcessor', () => { const keys = (respMock._attrs as Array<[string, unknown]>).map(([k]) => k); expect(keys).not.toContain(OpenTelemetryConstants.GEN_AI_INPUT_MESSAGES_KEY); }); + + it('records full array JSON when only assistant messages are present', async () => { + const processor = new OpenAIAgentsTraceProcessor(tracer); + const traceData = { traceId: 'trace-assistant-only', name: 'Agent' } as any; + await processor.onTraceStart(traceData); + + const inputArray = [ + { + role: 'assistant', + parts: [ + { type: 'text', content: 'Assistant reply' }, + ], + }, + ]; + + const respSpan = { + spanId: 'resp-assistant-span', + traceId: 'trace-assistant-only', + startedAt: new Date().toISOString(), + spanData: { + type: 'response' as const, + name: 'ResponseAssistantOnly', + _input: inputArray, + _response: { model: 'gpt-4', output: 'ok' }, + }, + } as any; + + await processor.onSpanStart(respSpan); + await processor.onSpanEnd(respSpan); + + const respMock = spansByName['ResponseAssistantOnly']; + const attrs = respMock._attrs as Array<[string, unknown]>; + const entry = attrs.find(([k]) => k === OpenTelemetryConstants.GEN_AI_INPUT_MESSAGES_KEY); + expect(entry).toBeDefined(); + + const value = entry![1] as string; + const parsed = JSON.parse(value); + expect(parsed).toEqual(inputArray); + }); + it('records user text content for array _input on response spans', async () => { + const processor = new OpenAIAgentsTraceProcessor(tracer); + const traceData = { traceId: 'trace-array-input', name: 'Agent' } as any; + await processor.onTraceStart(traceData); + + const respSpan = { + spanId: 'resp-array-span', + traceId: 'trace-array-input', + startedAt: new Date().toISOString(), + spanData: { + type: 'response' as const, + name: 'ResponseArray', + _input: [ + { + role: 'user', + parts: [ + { type: 'text', content: 'Hello user 1' }, + { type: 'text', content: 'Hello user 2' }, + ], + } + ], + _response: { model: 'gpt-4', output: 'ok' }, + }, + } as any; + + await processor.onSpanStart(respSpan); + await processor.onSpanEnd(respSpan); + + const respMock = spansByName['ResponseArray']; + const attrs = respMock._attrs as Array<[string, unknown]>; + const entry = attrs.find(([k]) => k === OpenTelemetryConstants.GEN_AI_INPUT_MESSAGES_KEY); + expect(entry).toBeDefined(); + + const value = entry![1] as string; + const parsed = JSON.parse(value); + expect(parsed).toEqual(['Hello user 1', 'Hello user 2']); + }); }); }); From aa10faf31daad37731123082b002046f299f0fb7 Mon Sep 17 00:00:00 2001 From: jsl517 Date: Wed, 7 Jan 2026 11:32:01 -0800 Subject: [PATCH 2/6] comments --- .../src/OpenAIAgentsTraceProcessor.ts | 18 +++++++++--------- .../openai/OpenAIAgentsTraceProcessor.test.ts | 4 ++-- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/agents-a365-observability-extensions-openai/src/OpenAIAgentsTraceProcessor.ts b/packages/agents-a365-observability-extensions-openai/src/OpenAIAgentsTraceProcessor.ts index e549b7be..7b9342ec 100644 --- a/packages/agents-a365-observability-extensions-openai/src/OpenAIAgentsTraceProcessor.ts +++ b/packages/agents-a365-observability-extensions-openai/src/OpenAIAgentsTraceProcessor.ts @@ -261,19 +261,19 @@ export class OpenAIAgentsTraceProcessor implements TracingProcessor { } } - private buildInputMessages(arr: Array<{role?: string; parts?: Array<{type: string; content: unknown}>}>): string { - const userTexts = []; - for (const message of arr) { - if (message && message.role === "user" && Array.isArray(message.parts)) { - for (const p of message.parts) { - if (p && p.type === "text" && typeof p.content === "string") { - userTexts.push(p.content); + private buildInputMessages(arr: Array<{ role?: string; parts?: Array<{ type: string; content: unknown }> }>): string { + const userTexts: string[] = []; + for (const message of arr) { + if (message && message.role === 'user' && Array.isArray(message.parts)) { + for (const p of message.parts) { + if (p && p.type === 'text' && typeof p.content === 'string') { + userTexts.push(p.content); + } } } } - } return userTexts.length ? JSON.stringify(userTexts) : JSON.stringify(arr); -} + } /** diff --git a/tests/observability/extension/openai/OpenAIAgentsTraceProcessor.test.ts b/tests/observability/extension/openai/OpenAIAgentsTraceProcessor.test.ts index 718eb9ca..660ab937 100644 --- a/tests/observability/extension/openai/OpenAIAgentsTraceProcessor.test.ts +++ b/tests/observability/extension/openai/OpenAIAgentsTraceProcessor.test.ts @@ -161,7 +161,7 @@ describe('OpenAIAgentsTraceProcessor', () => { processor.onSpanStart(agentSpan); - const otelSpans = (processor as any).otelSpans; + const otelSpans = (processor as any).otelSpans; expect(otelSpans.has('agent-1')).toBe(true); processor.onSpanEnd(agentSpan); @@ -588,7 +588,7 @@ describe('OpenAIAgentsTraceProcessor', () => { const value = entry![1] as string; const parsed = JSON.parse(value); expect(parsed).toEqual(inputArray); - }); + }); it('records user text content for array _input on response spans', async () => { const processor = new OpenAIAgentsTraceProcessor(tracer); const traceData = { traceId: 'trace-array-input', name: 'Agent' } as any; From aa0f134466a6c83d1408c965a081252bcb7fea70 Mon Sep 17 00:00:00 2001 From: jsl517 Date: Wed, 7 Jan 2026 12:27:35 -0800 Subject: [PATCH 3/6] comment --- .../src/OpenAIAgentsTraceProcessor.ts | 12 +++++ .../openai/OpenAIAgentsTraceProcessor.test.ts | 46 +++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/packages/agents-a365-observability-extensions-openai/src/OpenAIAgentsTraceProcessor.ts b/packages/agents-a365-observability-extensions-openai/src/OpenAIAgentsTraceProcessor.ts index 7b9342ec..7729c374 100644 --- a/packages/agents-a365-observability-extensions-openai/src/OpenAIAgentsTraceProcessor.ts +++ b/packages/agents-a365-observability-extensions-openai/src/OpenAIAgentsTraceProcessor.ts @@ -250,6 +250,18 @@ export class OpenAIAgentsTraceProcessor implements TracingProcessor { if (inputObj && !this.suppressInvokeAgentInput) { if (typeof inputObj === 'string') { + try { + const parsed = JSON.parse(inputObj as string); + if (Array.isArray(parsed)) { + otelSpan.setAttribute( + OpenTelemetryConstants.GEN_AI_INPUT_MESSAGES_KEY, + this.buildInputMessages(parsed) + ); + return; + } + } catch { + // If parsing fails, fall back to raw string behavior + } otelSpan.setAttribute(OpenTelemetryConstants.GEN_AI_INPUT_MESSAGES_KEY, inputObj); } else if (Array.isArray(inputObj)) { // Store the complete _input structure as JSON diff --git a/tests/observability/extension/openai/OpenAIAgentsTraceProcessor.test.ts b/tests/observability/extension/openai/OpenAIAgentsTraceProcessor.test.ts index 660ab937..dc35781d 100644 --- a/tests/observability/extension/openai/OpenAIAgentsTraceProcessor.test.ts +++ b/tests/observability/extension/openai/OpenAIAgentsTraceProcessor.test.ts @@ -626,5 +626,51 @@ describe('OpenAIAgentsTraceProcessor', () => { const parsed = JSON.parse(value); expect(parsed).toEqual(['Hello user 1', 'Hello user 2']); }); + + it('parses stringified array _input and records only user text content', async () => { + const processor = new OpenAIAgentsTraceProcessor(tracer); + const traceData = { traceId: 'trace-array-input-string', name: 'Agent' } as any; + await processor.onTraceStart(traceData); + + const inputArray = [ + { + role: 'user', + parts: [ + { type: 'text', content: 'Hello user 1' }, + { type: 'text', content: 'Hello user 2' }, + ], + }, + { + role: 'assistant', + parts: [ + { type: 'text', content: 'Assistant reply' }, + ], + }, + ]; + + const respSpan = { + spanId: 'resp-array-span-string', + traceId: 'trace-array-input-string', + startedAt: new Date().toISOString(), + spanData: { + type: 'response' as const, + name: 'ResponseArrayString', + _input: JSON.stringify(inputArray), + _response: { model: 'gpt-4', output: 'ok' }, + }, + } as any; + + await processor.onSpanStart(respSpan); + await processor.onSpanEnd(respSpan); + + const respMock = spansByName['ResponseArrayString']; + const attrs = respMock._attrs as Array<[string, unknown]>; + const entry = attrs.find(([k]) => k === OpenTelemetryConstants.GEN_AI_INPUT_MESSAGES_KEY); + expect(entry).toBeDefined(); + + const value = entry![1] as string; + const parsed = JSON.parse(value); + expect(parsed).toEqual(['Hello user 1', 'Hello user 2']); + }); }); }); From 9ecfd833f87a55e6f5a9c4511bd0de6b121ac44b Mon Sep 17 00:00:00 2001 From: jsl517 Date: Wed, 7 Jan 2026 12:43:40 -0800 Subject: [PATCH 4/6] comment --- .../src/OpenAIAgentsTraceProcessor.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/agents-a365-observability-extensions-openai/src/OpenAIAgentsTraceProcessor.ts b/packages/agents-a365-observability-extensions-openai/src/OpenAIAgentsTraceProcessor.ts index 7729c374..120843b6 100644 --- a/packages/agents-a365-observability-extensions-openai/src/OpenAIAgentsTraceProcessor.ts +++ b/packages/agents-a365-observability-extensions-openai/src/OpenAIAgentsTraceProcessor.ts @@ -264,7 +264,7 @@ export class OpenAIAgentsTraceProcessor implements TracingProcessor { } otelSpan.setAttribute(OpenTelemetryConstants.GEN_AI_INPUT_MESSAGES_KEY, inputObj); } else if (Array.isArray(inputObj)) { - // Store the complete _input structure as JSON + // build the input messages from array otelSpan.setAttribute( OpenTelemetryConstants.GEN_AI_INPUT_MESSAGES_KEY, this.buildInputMessages(inputObj) @@ -287,7 +287,6 @@ export class OpenAIAgentsTraceProcessor implements TracingProcessor { return userTexts.length ? JSON.stringify(userTexts) : JSON.stringify(arr); } - /** * Process generation span data */ From a20c19a94104d34e7b4407285ed122666af920c8 Mon Sep 17 00:00:00 2001 From: jsl517 Date: Wed, 7 Jan 2026 15:26:22 -0800 Subject: [PATCH 5/6] add test to cover handling non standard input. --- .../openai/OpenAIAgentsTraceProcessor.test.ts | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/observability/extension/openai/OpenAIAgentsTraceProcessor.test.ts b/tests/observability/extension/openai/OpenAIAgentsTraceProcessor.test.ts index dc35781d..0179b3a8 100644 --- a/tests/observability/extension/openai/OpenAIAgentsTraceProcessor.test.ts +++ b/tests/observability/extension/openai/OpenAIAgentsTraceProcessor.test.ts @@ -672,5 +672,38 @@ describe('OpenAIAgentsTraceProcessor', () => { const parsed = JSON.parse(value); expect(parsed).toEqual(['Hello user 1', 'Hello user 2']); }); + + it('records [gen_ai.input.messages] attribute for array input with non standard schema on response spans', async () => { + const processor = new OpenAIAgentsTraceProcessor(tracer); + const traceData = { traceId: 'trace-array-input', name: 'Agent' } as any; + await processor.onTraceStart(traceData); + const inputArray = [ + { type: 'text', content: 'message 1' }, + { type: 'text', content: 'message 2' }, + ]; + const respSpan = { + spanId: 'resp-array-span', + traceId: 'trace-array-input', + startedAt: new Date().toISOString(), + spanData: { + type: 'response' as const, + name: 'ResponseArray', + _input: inputArray, + _response: { model: 'gpt-4', output: 'ok' }, + }, + } as any; + + await processor.onSpanStart(respSpan); + await processor.onSpanEnd(respSpan); + + const respMock = spansByName['ResponseArray']; + const attrs = respMock._attrs as Array<[string, unknown]>; + const entry = attrs.find(([k]) => k === OpenTelemetryConstants.GEN_AI_INPUT_MESSAGES_KEY); + expect(entry).toBeDefined(); + + const value = entry![1] as string; + const parsed = JSON.parse(value); + expect(parsed).toEqual(inputArray); + }); }); }); From 7e9dc627aec73a91bbdc65770428e561bc3f9da8 Mon Sep 17 00:00:00 2001 From: jsl517 Date: Mon, 12 Jan 2026 11:52:03 -0800 Subject: [PATCH 6/6] fix the message process to match open ai agent --- .../src/OpenAIAgentsTraceProcessor.ts | 33 +++++-- .../openai/OpenAIAgentsTraceProcessor.test.ts | 96 ++++++++++++++----- 2 files changed, 97 insertions(+), 32 deletions(-) diff --git a/packages/agents-a365-observability-extensions-openai/src/OpenAIAgentsTraceProcessor.ts b/packages/agents-a365-observability-extensions-openai/src/OpenAIAgentsTraceProcessor.ts index 120843b6..68ce01ed 100644 --- a/packages/agents-a365-observability-extensions-openai/src/OpenAIAgentsTraceProcessor.ts +++ b/packages/agents-a365-observability-extensions-openai/src/OpenAIAgentsTraceProcessor.ts @@ -231,7 +231,10 @@ export class OpenAIAgentsTraceProcessor implements TracingProcessor { if (typeof resp.output === 'string') { otelSpan.setAttribute(OpenTelemetryConstants.GEN_AI_OUTPUT_MESSAGES_KEY, resp.output); } else { - otelSpan.setAttribute(OpenTelemetryConstants.GEN_AI_OUTPUT_MESSAGES_KEY, JSON.stringify(resp.output)); + otelSpan.setAttribute( + OpenTelemetryConstants.GEN_AI_OUTPUT_MESSAGES_KEY, + this.buildOutputMessages(resp.output as Array<{ role: string; content: Array<{ type: string; text: string }> }>) + ); } } @@ -273,18 +276,30 @@ export class OpenAIAgentsTraceProcessor implements TracingProcessor { } } - private buildInputMessages(arr: Array<{ role?: string; parts?: Array<{ type: string; content: unknown }> }>): string { + private buildInputMessages(arr: Array<{ role: string; content: string }>): string { + const userTexts = arr + .filter((m) => m && m.role === 'user' && typeof m.content === 'string') + .map((m) => m.content); + + return JSON.stringify(userTexts.length ? userTexts : arr); + } + + private buildOutputMessages(arr: Array<{ role: string; content: Array<{ type: string; text: string }> }>): string { const userTexts: string[] = []; - for (const message of arr) { - if (message && message.role === 'user' && Array.isArray(message.parts)) { - for (const p of message.parts) { - if (p && p.type === 'text' && typeof p.content === 'string') { - userTexts.push(p.content); - } + + for (const { content } of arr) { + if (!Array.isArray(content)) { + continue; + } + + for (const { type, text } of content) { + if (type === 'output_text' && typeof text === 'string') { + userTexts.push(text); } } } - return userTexts.length ? JSON.stringify(userTexts) : JSON.stringify(arr); + + return JSON.stringify(userTexts.length ? userTexts : arr); } /** diff --git a/tests/observability/extension/openai/OpenAIAgentsTraceProcessor.test.ts b/tests/observability/extension/openai/OpenAIAgentsTraceProcessor.test.ts index 0179b3a8..9fd453cb 100644 --- a/tests/observability/extension/openai/OpenAIAgentsTraceProcessor.test.ts +++ b/tests/observability/extension/openai/OpenAIAgentsTraceProcessor.test.ts @@ -559,9 +559,7 @@ describe('OpenAIAgentsTraceProcessor', () => { const inputArray = [ { role: 'assistant', - parts: [ - { type: 'text', content: 'Assistant reply' }, - ], + content: 'Assistant reply', }, ]; @@ -602,13 +600,8 @@ describe('OpenAIAgentsTraceProcessor', () => { type: 'response' as const, name: 'ResponseArray', _input: [ - { - role: 'user', - parts: [ - { type: 'text', content: 'Hello user 1' }, - { type: 'text', content: 'Hello user 2' }, - ], - } + { role: 'user', content: 'Hello user 1' }, + { role: 'user', content: 'Hello user 2' }, ], _response: { model: 'gpt-4', output: 'ok' }, }, @@ -633,19 +626,9 @@ describe('OpenAIAgentsTraceProcessor', () => { await processor.onTraceStart(traceData); const inputArray = [ - { - role: 'user', - parts: [ - { type: 'text', content: 'Hello user 1' }, - { type: 'text', content: 'Hello user 2' }, - ], - }, - { - role: 'assistant', - parts: [ - { type: 'text', content: 'Assistant reply' }, - ], - }, + { role: 'user', content: 'Hello user 1' }, + { role: 'user', content: 'Hello user 2' }, + { role: 'assistant', content: 'Assistant reply' }, ]; const respSpan = { @@ -705,5 +688,72 @@ describe('OpenAIAgentsTraceProcessor', () => { const parsed = JSON.parse(value); expect(parsed).toEqual(inputArray); }); + + it('records GEN_AI_OUTPUT_MESSAGES as plain string when output is a string', async () => { + const processor = new OpenAIAgentsTraceProcessor(tracer); + const traceData = { traceId: 'trace-output-string', name: 'Agent' } as any; + await processor.onTraceStart(traceData); + + const respSpan = { + spanId: 'resp-output-string', + traceId: 'trace-output-string', + startedAt: new Date().toISOString(), + spanData: { + type: 'response' as const, + name: 'ResponseOutputString', + _input: 'ignored', + _response: { model: 'gpt-4', output: 'final answer' }, + }, + } as any; + + await processor.onSpanStart(respSpan); + await processor.onSpanEnd(respSpan); + + const respMock = spansByName['ResponseOutputString']; + const attrs = respMock._attrs as Array<[string, unknown]>; + const entry = attrs.find(([k]) => k === OpenTelemetryConstants.GEN_AI_OUTPUT_MESSAGES_KEY); + expect(entry).toBeDefined(); + expect(entry![1]).toBe('final answer'); + }); + + it('records GEN_AI_OUTPUT_MESSAGES as aggregated texts when output is structured', async () => { + const processor = new OpenAIAgentsTraceProcessor(tracer); + const traceData = { traceId: 'trace-output-structured', name: 'Agent' } as any; + await processor.onTraceStart(traceData); + + const outputArray = [ + { + role: 'assistant', + content: [ + { type: 'output_text', text: 'Hello user 1' }, + { type: 'output_text', text: 'Hello user 2' }, + ], + }, + ]; + + const respSpan = { + spanId: 'resp-output-structured', + traceId: 'trace-output-structured', + startedAt: new Date().toISOString(), + spanData: { + type: 'response' as const, + name: 'ResponseOutputStructured', + _input: 'ignored', + _response: { model: 'gpt-4', output: outputArray }, + }, + } as any; + + await processor.onSpanStart(respSpan); + await processor.onSpanEnd(respSpan); + + const respMock = spansByName['ResponseOutputStructured']; + const attrs = respMock._attrs as Array<[string, unknown]>; + const entry = attrs.find(([k]) => k === OpenTelemetryConstants.GEN_AI_OUTPUT_MESSAGES_KEY); + expect(entry).toBeDefined(); + + const value = entry![1] as string; + const parsed = JSON.parse(value); + expect(parsed).toEqual(['Hello user 1', 'Hello user 2']); + }); }); });