From 1a9ce66f9a57d8db9457c4f5521b7883e8557286 Mon Sep 17 00:00:00 2001 From: Trevor Colby <106617125+obs-gh-trevorColby@users.noreply.github.com> Date: Mon, 23 Feb 2026 16:08:50 -0800 Subject: [PATCH 1/2] fix: preserve tool-call and tool-result parts in gen_ai.input.messages The transformPrompts function was converting all message content to text-only parts, which lost important tool call information. This fix adds a new processMessageParts function that properly preserves: - Text parts: { type: "text", content: "..." } - Tool-call parts: { type: "tool_call", tool_call: { id, name, arguments } } - Tool-result parts: { type: "tool_result", tool_call_id, tool_name, content } This allows observability tools to see the full context of tool interactions in the gen_ai.input.messages attribute, including: - Which tools were called (tool-call parts) - What arguments were passed to tools - What results were returned (tool-result parts) Fixes #889 --- .../src/lib/tracing/ai-sdk-transformations.ts | 90 ++++++++++++++++--- 1 file changed, 78 insertions(+), 12 deletions(-) diff --git a/packages/traceloop-sdk/src/lib/tracing/ai-sdk-transformations.ts b/packages/traceloop-sdk/src/lib/tracing/ai-sdk-transformations.ts index 6f609533..416ba9d6 100644 --- a/packages/traceloop-sdk/src/lib/tracing/ai-sdk-transformations.ts +++ b/packages/traceloop-sdk/src/lib/tracing/ai-sdk-transformations.ts @@ -59,6 +59,7 @@ const AI_PROMPT_TOOLS = "ai.prompt.tools"; const AI_TELEMETRY_METADATA_PREFIX = "ai.telemetry.metadata."; const TYPE_TEXT = "text"; const TYPE_TOOL_CALL = "tool_call"; +const TYPE_TOOL_RESULT = "tool_result"; const ROLE_ASSISTANT = "assistant"; const ROLE_USER = "user"; @@ -234,6 +235,79 @@ const processMessageContent = (content: any): string => { return String(content); }; +/** + * Process message content into proper parts array for gen_ai.input.messages. + * This preserves tool-call and tool-result parts instead of converting everything to text. + * Fixes: https://github.com/traceloop/openllmetry-js/issues/889 + */ +const processMessageParts = (content: any): any[] => { + const parts: any[] = []; + + if (Array.isArray(content)) { + for (const item of content) { + if (!item || typeof item !== "object") continue; + + if (item.type === TYPE_TEXT && item.text) { + // Text part + parts.push({ type: TYPE_TEXT, content: item.text }); + } else if (item.type === "tool-call" || item.type === "tool_call") { + // Tool call part - preserve the tool call information + const toolArgs = item.args ?? item.input; + parts.push({ + type: TYPE_TOOL_CALL, + tool_call: { + id: item.toolCallId, + name: item.toolName, + arguments: + typeof toolArgs === "string" ? toolArgs : JSON.stringify(toolArgs), + }, + }); + } else if (item.type === "tool-result") { + // Tool result part - preserve the tool result information + const toolOutput = item.result ?? item.output; + parts.push({ + type: TYPE_TOOL_RESULT, + tool_call_id: item.toolCallId, + tool_name: item.toolName, + content: + typeof toolOutput === "string" + ? toolOutput + : JSON.stringify(toolOutput), + }); + } else { + // Unknown part type - serialize as text + parts.push({ type: TYPE_TEXT, content: JSON.stringify(item) }); + } + } + } else if (content && typeof content === "object") { + if (content.type === TYPE_TEXT && content.text) { + parts.push({ type: TYPE_TEXT, content: content.text }); + } else { + parts.push({ type: TYPE_TEXT, content: JSON.stringify(content) }); + } + } else if (typeof content === "string") { + // Try to parse as JSON array of parts + try { + const parsed = JSON.parse(content); + if (Array.isArray(parsed)) { + return processMessageParts(parsed); + } + } catch { + // Not JSON, treat as plain text + } + parts.push({ type: TYPE_TEXT, content: content }); + } else if (content != null) { + parts.push({ type: TYPE_TEXT, content: String(content) }); + } + + // If no parts were extracted, return a single empty text part + if (parts.length === 0) { + parts.push({ type: TYPE_TEXT, content: "" }); + } + + return parts; +}; + const transformTools = (attributes: Record): void => { if (AI_PROMPT_TOOLS in attributes) { try { @@ -302,14 +376,10 @@ const transformPrompts = (attributes: Record): void => { attributes[`${ATTR_GEN_AI_PROMPT}.${index}.role`] = msg.role; // Add to OpenTelemetry standard gen_ai.input.messages format + // Use processMessageParts to preserve tool-call and tool-result parts inputMessages.push({ role: msg.role, - parts: [ - { - type: TYPE_TEXT, - content: processedContent, - }, - ], + parts: processMessageParts(msg.content), }); }); @@ -338,14 +408,10 @@ const transformPrompts = (attributes: Record): void => { attributes[contentKey] = processedContent; attributes[`${ATTR_GEN_AI_PROMPT}.${index}.role`] = msg.role; + // Use processMessageParts to preserve tool-call and tool-result parts inputMessages.push({ role: msg.role, - parts: [ - { - type: TYPE_TEXT, - content: processedContent, - }, - ], + parts: processMessageParts(msg.content), }); }, ); From 9f0e109fb3fcda4611453ed4816bfa5ad64c4e9b Mon Sep 17 00:00:00 2001 From: Trevor Colby <106617125+obs-gh-trevorColby@users.noreply.github.com> Date: Mon, 23 Feb 2026 16:24:15 -0800 Subject: [PATCH 2/2] fix: correct v4/v5 field precedence and symmetric type handling - Prefer v5 input over v4 args (item.input ?? item.args) - Prefer v5 output over v4 result (item.output ?? item.result) - Accept both 'tool-result' and 'tool_result' type values for symmetry with tool-call handling --- .../src/lib/tracing/ai-sdk-transformations.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/traceloop-sdk/src/lib/tracing/ai-sdk-transformations.ts b/packages/traceloop-sdk/src/lib/tracing/ai-sdk-transformations.ts index 416ba9d6..1025ae91 100644 --- a/packages/traceloop-sdk/src/lib/tracing/ai-sdk-transformations.ts +++ b/packages/traceloop-sdk/src/lib/tracing/ai-sdk-transformations.ts @@ -252,7 +252,9 @@ const processMessageParts = (content: any): any[] => { parts.push({ type: TYPE_TEXT, content: item.text }); } else if (item.type === "tool-call" || item.type === "tool_call") { // Tool call part - preserve the tool call information - const toolArgs = item.args ?? item.input; + // Support both v4 (args) and v5 (input) formats + // Prefer v5 (input) if present + const toolArgs = item.input ?? item.args; parts.push({ type: TYPE_TOOL_CALL, tool_call: { @@ -262,9 +264,11 @@ const processMessageParts = (content: any): any[] => { typeof toolArgs === "string" ? toolArgs : JSON.stringify(toolArgs), }, }); - } else if (item.type === "tool-result") { + } else if (item.type === "tool-result" || item.type === TYPE_TOOL_RESULT) { // Tool result part - preserve the tool result information - const toolOutput = item.result ?? item.output; + // Support both v4 (result) and v5 (output) formats + // Prefer v5 (output) if present + const toolOutput = item.output ?? item.result; parts.push({ type: TYPE_TOOL_RESULT, tool_call_id: item.toolCallId,