diff --git a/src/client.ts b/src/client.ts index 5a8cc38..a26ef5b 100644 --- a/src/client.ts +++ b/src/client.ts @@ -153,7 +153,6 @@ export class Client { process.on('SIGTERM', () => this.shutdown()); process.on('uncaughtException', (err) => { console.error('Uncaught exception:', err); - // TODO we can handle error states on unexported spans here this.shutdown(); process.exit(1); }); diff --git a/src/instrumentation/openai-agents/response.ts b/src/instrumentation/openai-agents/response.ts index ae73027..833710b 100644 --- a/src/instrumentation/openai-agents/response.ts +++ b/src/instrumentation/openai-agents/response.ts @@ -31,31 +31,6 @@ import { } from '../../attributes'; import { debug } from './index'; -/** - * OpenAI Agents ResponseSpanData type definition (from @openai/agents-core/dist/tracing/spans): - * - * interface ResponseSpanData { - * type: 'response'; - * response_id?: string; - * _input?: any; - * _response?: { - * id: string; - * model: string; - * usage?: { - * input_tokens: number; - * output_tokens: number; - * total_tokens: number; - * }; - * output?: Array<{ - * type: string; - * role?: string; - * content?: string; - * }>; - * output_text?: string; - * }; - * } - */ - const RESPONSE_ATTRIBUTES: AttributeMap = { [RESPONSE_ID]: 'response_id', [RESPONSE_INPUT]: '_input' @@ -93,12 +68,6 @@ const RESPONSE_INPUT_FUNCTION_CALL_ATTRIBUTES: IndexedAttributeMap = { [GEN_AI_MESSAGE_FUNCTION_CALL_ARGUMENTS]: 'arguments' }; -/** - * Converts OpenAI Agents ResponseSpanData to OpenTelemetry semantic conventions. - * - * Maps response spans to standard response semantic conventions using - * our centralized semantic convention constants and attribute mapping system. - */ export function convertResponseSpan(data: ResponseSpanData): AttributeMap { const attributes: AttributeMap = {}; Object.assign(attributes, extractAttributesFromMapping(data, RESPONSE_ATTRIBUTES)); @@ -106,16 +75,16 @@ export function convertResponseSpan(data: ResponseSpanData): AttributeMap { if (data._input && Array.isArray(data._input)) { for (const item of data._input) { switch (item.type) { - case 'message': // Input message + case 'message': Object.assign(attributes, extractAttributesFromMapping(item, RESPONSE_INPUT_ATTRIBUTES)); break; - case 'function_call': // Input function call + case 'function_call': Object.assign(attributes, extractAttributesFromMapping(item, RESPONSE_INPUT_FUNCTION_CALL_ATTRIBUTES)); debug('Extracted input function call:', item.name); break; - case 'function_call_result': // Function call result + case 'function_call_result': debug('Skipping function call result'); break; default: @@ -125,27 +94,26 @@ export function convertResponseSpan(data: ResponseSpanData): AttributeMap { } } - // _response was added with https://github.com/openai/openai-agents-js/pull/85 - if (data._response) { + if ((data as any)._response) { Object.assign(attributes, - extractAttributesFromMapping(data._response, RESPONSE_MODEL_ATTRIBUTES)); + extractAttributesFromMapping((data as any)._response, RESPONSE_MODEL_ATTRIBUTES)); Object.assign(attributes, - extractAttributesFromMapping(data._response.usage, RESPONSE_USAGE_ATTRIBUTES)); + extractAttributesFromMapping((data as any)._response.usage, RESPONSE_USAGE_ATTRIBUTES)); const completions = []; - if (Array.isArray(data._response.output)) { - for (const item of data._response.output) { + if (Array.isArray((data as any)._response.output)) { + for (const item of (data as any)._response.output) { switch (item.type) { - case 'message': { // ResponseOutputMessage + case 'message': { for (const contentItem of item.content || []) { switch (contentItem.type) { - case 'output_text': // ResponseOutputText + case 'output_text': completions.push({ role: item.role || 'assistant', content: contentItem.text }); break; - case 'refusal': // ResponseOutputRefusal + case 'refusal': completions.push({ role: item.role || 'assistant', content: contentItem.refusal @@ -158,7 +126,7 @@ export function convertResponseSpan(data: ResponseSpanData): AttributeMap { } break; } - case 'reasoning': { // ResponseReasoningItem + case 'reasoning': { const reasoningText = item.summary ?.filter((item: any) => item.type === 'summary_text') ?.map((item: any) => item.text) @@ -172,20 +140,20 @@ export function convertResponseSpan(data: ResponseSpanData): AttributeMap { } break; } - case 'function_call': // ResponseFunctionToolCall - case 'file_search_call': // ResponseFileSearchToolCall - case 'web_search_call': // ResponseFunctionWebSearch - case 'computer_call': { // ResponseComputerToolCall + case 'function_call': + case 'file_search_call': + case 'web_search_call': + case 'computer_call': { Object.assign(attributes, extractAttributesFromMapping(item, RESPONSE_TOOL_CALL_ATTRIBUTES)); break; } - case 'image_generation_call': // ResponseOutputItem.ImageGenerationCall - case 'code_interpreter_call': // ResponseCodeInterpreterToolCall - case 'local_shell_call': // ResponseOutputItem.LocalShellCall - case 'mcp_call': // ResponseOutputItem.McpCall - case 'mcp_list_tools': // ResponseOutputItem.McpListTools - case 'mcp_approval_request': { // ResponseOutputItem.McpApprovalRequest + case 'image_generation_call': + case 'code_interpreter_call': + case 'local_shell_call': + case 'mcp_call': + case 'mcp_list_tools': + case 'mcp_approval_request': { debug('Unhandled output item type:', item.type); break; } @@ -193,6 +161,7 @@ export function convertResponseSpan(data: ResponseSpanData): AttributeMap { debug('Unknown output item type:', item.type); break; } + } } } @@ -205,3 +174,40 @@ export function convertResponseSpan(data: ResponseSpanData): AttributeMap { return attributes; } +export function createEnhancedResponseSpanData( + baseData: { model: string; input: Array<{ type: string; role?: string; content?: string }> }, + metadata: { responseId: string; usage: { inputTokens: number; outputTokens: number; totalTokens: number } } +): any { + return { + type: 'response', + response_id: metadata.responseId, + _input: baseData.input, + _response: { + id: metadata.responseId, + model: baseData.model, + usage: { + input_tokens: metadata.usage.inputTokens, + output_tokens: metadata.usage.outputTokens, + total_tokens: metadata.usage.totalTokens + } + } + }; +} + +export function convertEnhancedResponseSpan(data: any): AttributeMap { + const attributes: AttributeMap = {}; + + Object.assign(attributes, extractAttributesFromMapping(data, RESPONSE_ATTRIBUTES)); + + if (data._input && Array.isArray(data._input)) { + Object.assign(attributes, extractAttributesFromArray(data._input, RESPONSE_INPUT_ATTRIBUTES)); + } + + if (data._response) { + Object.assign(attributes, extractAttributesFromMapping(data._response, RESPONSE_MODEL_ATTRIBUTES)); + Object.assign(attributes, extractAttributesFromMapping(data._response.usage, RESPONSE_USAGE_ATTRIBUTES)); + } + + return attributes; +} + diff --git a/src/semconv/model.ts b/src/semconv/model.ts index 40cbbb7..9e4ba79 100644 --- a/src/semconv/model.ts +++ b/src/semconv/model.ts @@ -13,4 +13,3 @@ export const GEN_AI_RESPONSE_FINISH_REASONS = 'gen_ai.response.finish_reasons'; export const GEN_AI_USAGE_INPUT_TOKENS = 'gen_ai.usage.prompt_tokens'; export const GEN_AI_USAGE_OUTPUT_TOKENS = 'gen_ai.usage.completion_tokens'; export const GEN_AI_USAGE_TOTAL_TOKENS = 'gen_ai.usage.total_tokens'; -// TODO cache and reasoning tokens \ No newline at end of file diff --git a/src/tracing.ts b/src/tracing.ts index ef0a0a3..7f495b6 100644 --- a/src/tracing.ts +++ b/src/tracing.ts @@ -16,7 +16,6 @@ const MAX_EXPORT_BATCH_SIZE = 1; // Export immediately const SCHEDULED_DELAY_MILLIS = 0; // No delay between exports const EXPORT_TIMEOUT_MILLIS = 5000; // 5 second timeout -// TODO make this part of config const DASHBOARD_URL = "https://app.agentops.ai"; @@ -132,7 +131,7 @@ export class TracingCore { this.sdk = new OpenTelemetryNodeSDK({ resource: resource, instrumentations: instrumentations, - spanProcessor: this.processor, + spanProcessor: this.processor as any, }); // Configure logging after resource attributes are settled @@ -169,4 +168,4 @@ export class TracingCore { diag.setLogger(new DiagConsoleLogger(), diagLevel); } -} \ No newline at end of file +} diff --git a/tests/base.test.ts b/tests/base.test.ts index a9b5043..ace0516 100644 --- a/tests/base.test.ts +++ b/tests/base.test.ts @@ -1,4 +1,5 @@ import { InstrumentationBase } from '../src/instrumentation/base'; +import { Client } from '../src/client'; class DummyInstrumentation extends InstrumentationBase { static readonly metadata = { @@ -20,6 +21,12 @@ class RuntimeInstrumentation extends DummyInstrumentation { } describe('InstrumentationBase', () => { + let mockClient: Client; + + beforeEach(() => { + mockClient = new Client(); + }); + it('reports availability of target module', () => { expect(DummyInstrumentation.available).toBe(true); class Missing extends DummyInstrumentation { static readonly metadata = { ...DummyInstrumentation.metadata, targetLibrary: 'nonexistentlib' }; } @@ -27,7 +34,7 @@ describe('InstrumentationBase', () => { }); it('runtime targeting runs setup only once', () => { - const inst = new RuntimeInstrumentation('n','v',{}); + const inst = new RuntimeInstrumentation(mockClient); inst.setupRuntimeTargeting(); expect(inst.setup).toHaveBeenCalledTimes(1); inst.setupRuntimeTargeting(); diff --git a/tests/registry.test.ts b/tests/registry.test.ts index 3b6832f..a918b35 100644 --- a/tests/registry.test.ts +++ b/tests/registry.test.ts @@ -1,4 +1,5 @@ import { InstrumentationBase } from '../src/instrumentation/base'; +import { Client } from '../src/client'; class RuntimeInst extends InstrumentationBase { static readonly metadata = { @@ -29,10 +30,11 @@ describe('InstrumentationRegistry', () => { AVAILABLE_INSTRUMENTORS: [RuntimeInst, SimpleInst] })); const { InstrumentationRegistry } = require('../src/instrumentation/registry'); - const registry = new InstrumentationRegistry(); + const mockClient = new Client(); + const registry = new InstrumentationRegistry(mockClient); registry.initialize(); expect(registry.getAvailable().length).toBe(2); - const active = registry.getActiveInstrumentors('svc'); + const active = registry.getActiveInstrumentors(); expect(active.some((i: any) => i instanceof RuntimeInst)).toBe(true); expect(active.some((i: any) => i instanceof SimpleInst)).toBe(true); });