From 611c0a2d1115cf9f523a1dff8acae7a73ff2da6d Mon Sep 17 00:00:00 2001 From: Alex Z Date: Wed, 4 Feb 2026 14:58:51 -0800 Subject: [PATCH 1/5] make responses working out of agents sdk work --- .../tests/two-step-conversion.test.ts | 75 +++++++++++++++++++ crates/lingua/src/processing/import.rs | 73 +++++++++++++++--- 2 files changed, 137 insertions(+), 11 deletions(-) create mode 100644 bindings/typescript/tests/two-step-conversion.test.ts diff --git a/bindings/typescript/tests/two-step-conversion.test.ts b/bindings/typescript/tests/two-step-conversion.test.ts new file mode 100644 index 0000000..b4dccc7 --- /dev/null +++ b/bindings/typescript/tests/two-step-conversion.test.ts @@ -0,0 +1,75 @@ +/** + * Two-step conversion test: Responses API format -> Lingua Messages -> Thread + * + * This test demonstrates the full conversion pipeline: + * 1. Import messages from spans (Responses API format -> Lingua Messages) + * 2. Thread preprocessor extracts and filters messages + */ + +import { describe, test, expect } from "vitest"; +import { importMessagesFromSpans } from "../src/index"; +import * as fs from "fs"; + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +describe("Two-step conversion: Responses API to Thread", () => { + test("should extract messages from Responses API output format", () => { + // Your actual trace data - focusing on the output field + const outputFromTrace = [ + { + id: "rs_0c7105cd8354f2660069824fe9039081938557c4fcb69a4d1a", + summary: [], + type: "reasoning", + }, + { + content: [ + { + annotations: [], + logprobs: [], + text: "I consulted the magic 8-ball for you (I will not reveal its exact words). Its guidance leans positively — so take this as a hopeful, mystical nudge toward yes.", + type: "output_text", + }, + ], + id: "msg_0c7105cd8354f2660069824fef675481938a1fde9d9e5917b9", + role: "assistant", + status: "completed", + type: "message", + }, + ]; + + // Step 1: Try importing from just the output field + console.log("\n=== Testing Output Field Conversion ==="); + const messagesFromOutput = importMessagesFromSpans([ + { output: outputFromTrace }, + ]); + console.log( + "Messages from output:", + JSON.stringify(messagesFromOutput, null, 2) + ); + + // Check if assistant message was extracted + const assistantMessages = messagesFromOutput.filter( + (m: any) => m.role === "assistant" + ); + console.log(`Found ${assistantMessages.length} assistant message(s)`); + + if (assistantMessages.length > 0) { + // Find the message with actual text content (not reasoning) + const messageWithText = assistantMessages.find((m: any) => { + const content = JSON.stringify(m.content); + return content.includes("magic 8-ball"); + }); + + if (messageWithText) { + console.log("✅ Found assistant message with magic 8-ball content"); + expect(messageWithText).toBeDefined(); + } else { + console.log("❌ No assistant message contains 'magic 8-ball'"); + expect(messageWithText).toBeDefined(); + } + } else { + console.log("❌ No assistant messages found from output field!"); + expect(assistantMessages.length).toBeGreaterThan(0); + } + }); +}); diff --git a/crates/lingua/src/processing/import.rs b/crates/lingua/src/processing/import.rs index 127a963..2705227 100644 --- a/crates/lingua/src/processing/import.rs +++ b/crates/lingua/src/processing/import.rs @@ -26,19 +26,25 @@ pub struct Span { /// Returns early to avoid expensive deserialization attempts on non-message data fn has_message_structure(data: &Value) -> bool { match data { - // Check if it's an array where first element has "role" field or is a choice object + // Check if it's an array where ANY element has "role" field or is a choice object Value::Array(arr) => { if arr.is_empty() { return false; } - if let Some(Value::Object(first)) = arr.first() { - // Direct message format: has "role" field - if first.contains_key("role") { - return true; - } - // Chat completions response choices format: has "message" field with role inside - if let Some(Value::Object(msg)) = first.get("message") { - return msg.contains_key("role"); + // Check if ANY element in the array looks like a message (not just the first) + // This handles mixed-type arrays from Responses API + for item in arr { + if let Value::Object(obj) = item { + // Direct message format: has "role" field + if obj.contains_key("role") { + return true; + } + // Chat completions response choices format: has "message" field with role inside + if let Some(Value::Object(msg)) = obj.get("message") { + if msg.contains_key("role") { + return true; + } + } } } false @@ -99,6 +105,7 @@ fn try_converting_to_messages(data: &Value) -> Vec { } // Try Responses API format + // First try to parse the entire array as InputItems (for uniform arrays) if let Ok(provider_messages) = serde_json::from_value::>(data_to_parse.clone()) { @@ -111,6 +118,48 @@ fn try_converting_to_messages(data: &Value) -> Vec { } } + // If that fails, try parsing individual items (for mixed-type arrays like Responses API output) + // This handles arrays with reasoning, function calls, and messages mixed together + if let Value::Array(arr) = data_to_parse { + let mut messages = Vec::new(); + for item in arr { + // Check if this item has type: "message" and extract it using lenient parsing + if let Some(obj) = item.as_object() { + if let Some(Value::String(type_str)) = obj.get("type") { + if type_str == "message" { + // This is a message item, try lenient parsing + if let Some(msg) = parse_lenient_message_item(item) { + messages.push(msg); + continue; + } + // If lenient parsing fails, log why (temporarily for debugging) + eprintln!("Failed to parse message item with type='message': {:?}", item); + } + } + } + + // Try to parse as InputItem + if let Ok(input_item) = serde_json::from_value::(item.clone()) { + if let Ok(msg_vec) = + as TryFromLLM>>::try_from(vec![input_item]) + { + messages.extend(msg_vec); + } + } + // Also try to parse as OutputItem (for output arrays) + else if let Ok(output_item) = serde_json::from_value::(item.clone()) { + if let Ok(msg_vec) = + as TryFromLLM>>::try_from(vec![output_item]) + { + messages.extend(msg_vec); + } + } + } + if !messages.is_empty() { + return messages; + } + } + // Try Anthropic format if let Ok(provider_messages) = serde_json::from_value::>(data_to_parse.clone()) @@ -198,7 +247,8 @@ fn parse_user_content(value: &Value) -> Option { for item in arr { if let Some(obj) = item.as_object() { if let Some(Value::String(text_type)) = obj.get("type") { - if text_type == "text" { + // Handle both "text" and "input_text" (Responses API format) + if text_type == "text" || text_type == "input_text" { if let Some(Value::String(text)) = obj.get("text") { parts.push(UserContentPart::Text(TextContentPart { text: text.clone(), @@ -228,7 +278,8 @@ fn parse_assistant_content(value: &Value) -> Option { for item in arr { if let Some(obj) = item.as_object() { if let Some(Value::String(text_type)) = obj.get("type") { - if text_type == "text" { + // Handle both "text" and "output_text" (Responses API format) + if text_type == "text" || text_type == "output_text" { if let Some(Value::String(text)) = obj.get("text") { parts.push(crate::universal::AssistantContentPart::Text( TextContentPart { From ac22d82b48e43ee73533d08757fcd264cb434b4c Mon Sep 17 00:00:00 2001 From: Alex Z Date: Wed, 4 Feb 2026 14:59:39 -0800 Subject: [PATCH 2/5] unused import --- bindings/typescript/tests/two-step-conversion.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/bindings/typescript/tests/two-step-conversion.test.ts b/bindings/typescript/tests/two-step-conversion.test.ts index b4dccc7..6c722c4 100644 --- a/bindings/typescript/tests/two-step-conversion.test.ts +++ b/bindings/typescript/tests/two-step-conversion.test.ts @@ -8,7 +8,6 @@ import { describe, test, expect } from "vitest"; import { importMessagesFromSpans } from "../src/index"; -import * as fs from "fs"; /* eslint-disable @typescript-eslint/no-explicit-any */ From a539f23f9cec340cb427d84fd26291839895874b Mon Sep 17 00:00:00 2001 From: Alex Z Date: Thu, 5 Feb 2026 11:55:21 -0800 Subject: [PATCH 3/5] Revert "unused import" This reverts commit ac22d82b48e43ee73533d08757fcd264cb434b4c. --- bindings/typescript/tests/two-step-conversion.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/bindings/typescript/tests/two-step-conversion.test.ts b/bindings/typescript/tests/two-step-conversion.test.ts index 6c722c4..b4dccc7 100644 --- a/bindings/typescript/tests/two-step-conversion.test.ts +++ b/bindings/typescript/tests/two-step-conversion.test.ts @@ -8,6 +8,7 @@ import { describe, test, expect } from "vitest"; import { importMessagesFromSpans } from "../src/index"; +import * as fs from "fs"; /* eslint-disable @typescript-eslint/no-explicit-any */ From 131eeca54f4277676395ea616b2b08537c422729 Mon Sep 17 00:00:00 2001 From: Alex Z Date: Thu, 5 Feb 2026 11:55:41 -0800 Subject: [PATCH 4/5] Revert "make responses working out of agents sdk work" This reverts commit 611c0a2d1115cf9f523a1dff8acae7a73ff2da6d. --- .../tests/two-step-conversion.test.ts | 75 ------------------- crates/lingua/src/processing/import.rs | 73 +++--------------- 2 files changed, 11 insertions(+), 137 deletions(-) delete mode 100644 bindings/typescript/tests/two-step-conversion.test.ts diff --git a/bindings/typescript/tests/two-step-conversion.test.ts b/bindings/typescript/tests/two-step-conversion.test.ts deleted file mode 100644 index b4dccc7..0000000 --- a/bindings/typescript/tests/two-step-conversion.test.ts +++ /dev/null @@ -1,75 +0,0 @@ -/** - * Two-step conversion test: Responses API format -> Lingua Messages -> Thread - * - * This test demonstrates the full conversion pipeline: - * 1. Import messages from spans (Responses API format -> Lingua Messages) - * 2. Thread preprocessor extracts and filters messages - */ - -import { describe, test, expect } from "vitest"; -import { importMessagesFromSpans } from "../src/index"; -import * as fs from "fs"; - -/* eslint-disable @typescript-eslint/no-explicit-any */ - -describe("Two-step conversion: Responses API to Thread", () => { - test("should extract messages from Responses API output format", () => { - // Your actual trace data - focusing on the output field - const outputFromTrace = [ - { - id: "rs_0c7105cd8354f2660069824fe9039081938557c4fcb69a4d1a", - summary: [], - type: "reasoning", - }, - { - content: [ - { - annotations: [], - logprobs: [], - text: "I consulted the magic 8-ball for you (I will not reveal its exact words). Its guidance leans positively — so take this as a hopeful, mystical nudge toward yes.", - type: "output_text", - }, - ], - id: "msg_0c7105cd8354f2660069824fef675481938a1fde9d9e5917b9", - role: "assistant", - status: "completed", - type: "message", - }, - ]; - - // Step 1: Try importing from just the output field - console.log("\n=== Testing Output Field Conversion ==="); - const messagesFromOutput = importMessagesFromSpans([ - { output: outputFromTrace }, - ]); - console.log( - "Messages from output:", - JSON.stringify(messagesFromOutput, null, 2) - ); - - // Check if assistant message was extracted - const assistantMessages = messagesFromOutput.filter( - (m: any) => m.role === "assistant" - ); - console.log(`Found ${assistantMessages.length} assistant message(s)`); - - if (assistantMessages.length > 0) { - // Find the message with actual text content (not reasoning) - const messageWithText = assistantMessages.find((m: any) => { - const content = JSON.stringify(m.content); - return content.includes("magic 8-ball"); - }); - - if (messageWithText) { - console.log("✅ Found assistant message with magic 8-ball content"); - expect(messageWithText).toBeDefined(); - } else { - console.log("❌ No assistant message contains 'magic 8-ball'"); - expect(messageWithText).toBeDefined(); - } - } else { - console.log("❌ No assistant messages found from output field!"); - expect(assistantMessages.length).toBeGreaterThan(0); - } - }); -}); diff --git a/crates/lingua/src/processing/import.rs b/crates/lingua/src/processing/import.rs index 2705227..127a963 100644 --- a/crates/lingua/src/processing/import.rs +++ b/crates/lingua/src/processing/import.rs @@ -26,25 +26,19 @@ pub struct Span { /// Returns early to avoid expensive deserialization attempts on non-message data fn has_message_structure(data: &Value) -> bool { match data { - // Check if it's an array where ANY element has "role" field or is a choice object + // Check if it's an array where first element has "role" field or is a choice object Value::Array(arr) => { if arr.is_empty() { return false; } - // Check if ANY element in the array looks like a message (not just the first) - // This handles mixed-type arrays from Responses API - for item in arr { - if let Value::Object(obj) = item { - // Direct message format: has "role" field - if obj.contains_key("role") { - return true; - } - // Chat completions response choices format: has "message" field with role inside - if let Some(Value::Object(msg)) = obj.get("message") { - if msg.contains_key("role") { - return true; - } - } + if let Some(Value::Object(first)) = arr.first() { + // Direct message format: has "role" field + if first.contains_key("role") { + return true; + } + // Chat completions response choices format: has "message" field with role inside + if let Some(Value::Object(msg)) = first.get("message") { + return msg.contains_key("role"); } } false @@ -105,7 +99,6 @@ fn try_converting_to_messages(data: &Value) -> Vec { } // Try Responses API format - // First try to parse the entire array as InputItems (for uniform arrays) if let Ok(provider_messages) = serde_json::from_value::>(data_to_parse.clone()) { @@ -118,48 +111,6 @@ fn try_converting_to_messages(data: &Value) -> Vec { } } - // If that fails, try parsing individual items (for mixed-type arrays like Responses API output) - // This handles arrays with reasoning, function calls, and messages mixed together - if let Value::Array(arr) = data_to_parse { - let mut messages = Vec::new(); - for item in arr { - // Check if this item has type: "message" and extract it using lenient parsing - if let Some(obj) = item.as_object() { - if let Some(Value::String(type_str)) = obj.get("type") { - if type_str == "message" { - // This is a message item, try lenient parsing - if let Some(msg) = parse_lenient_message_item(item) { - messages.push(msg); - continue; - } - // If lenient parsing fails, log why (temporarily for debugging) - eprintln!("Failed to parse message item with type='message': {:?}", item); - } - } - } - - // Try to parse as InputItem - if let Ok(input_item) = serde_json::from_value::(item.clone()) { - if let Ok(msg_vec) = - as TryFromLLM>>::try_from(vec![input_item]) - { - messages.extend(msg_vec); - } - } - // Also try to parse as OutputItem (for output arrays) - else if let Ok(output_item) = serde_json::from_value::(item.clone()) { - if let Ok(msg_vec) = - as TryFromLLM>>::try_from(vec![output_item]) - { - messages.extend(msg_vec); - } - } - } - if !messages.is_empty() { - return messages; - } - } - // Try Anthropic format if let Ok(provider_messages) = serde_json::from_value::>(data_to_parse.clone()) @@ -247,8 +198,7 @@ fn parse_user_content(value: &Value) -> Option { for item in arr { if let Some(obj) = item.as_object() { if let Some(Value::String(text_type)) = obj.get("type") { - // Handle both "text" and "input_text" (Responses API format) - if text_type == "text" || text_type == "input_text" { + if text_type == "text" { if let Some(Value::String(text)) = obj.get("text") { parts.push(UserContentPart::Text(TextContentPart { text: text.clone(), @@ -278,8 +228,7 @@ fn parse_assistant_content(value: &Value) -> Option { for item in arr { if let Some(obj) = item.as_object() { if let Some(Value::String(text_type)) = obj.get("type") { - // Handle both "text" and "output_text" (Responses API format) - if text_type == "text" || text_type == "output_text" { + if text_type == "text" { if let Some(Value::String(text)) = obj.get("text") { parts.push(crate::universal::AssistantContentPart::Text( TextContentPart { From 19f723478a40b1360058ff614c05fbc3f6798c6c Mon Sep 17 00:00:00 2001 From: Alex Z Date: Thu, 5 Feb 2026 12:06:27 -0800 Subject: [PATCH 5/5] tried a new thing, doesn't work --- .../tests/two-step-conversion.test.ts | 74 +++++++++++++++++++ crates/lingua/src/processing/import.rs | 13 ++++ 2 files changed, 87 insertions(+) create mode 100644 bindings/typescript/tests/two-step-conversion.test.ts diff --git a/bindings/typescript/tests/two-step-conversion.test.ts b/bindings/typescript/tests/two-step-conversion.test.ts new file mode 100644 index 0000000..6c722c4 --- /dev/null +++ b/bindings/typescript/tests/two-step-conversion.test.ts @@ -0,0 +1,74 @@ +/** + * Two-step conversion test: Responses API format -> Lingua Messages -> Thread + * + * This test demonstrates the full conversion pipeline: + * 1. Import messages from spans (Responses API format -> Lingua Messages) + * 2. Thread preprocessor extracts and filters messages + */ + +import { describe, test, expect } from "vitest"; +import { importMessagesFromSpans } from "../src/index"; + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +describe("Two-step conversion: Responses API to Thread", () => { + test("should extract messages from Responses API output format", () => { + // Your actual trace data - focusing on the output field + const outputFromTrace = [ + { + id: "rs_0c7105cd8354f2660069824fe9039081938557c4fcb69a4d1a", + summary: [], + type: "reasoning", + }, + { + content: [ + { + annotations: [], + logprobs: [], + text: "I consulted the magic 8-ball for you (I will not reveal its exact words). Its guidance leans positively — so take this as a hopeful, mystical nudge toward yes.", + type: "output_text", + }, + ], + id: "msg_0c7105cd8354f2660069824fef675481938a1fde9d9e5917b9", + role: "assistant", + status: "completed", + type: "message", + }, + ]; + + // Step 1: Try importing from just the output field + console.log("\n=== Testing Output Field Conversion ==="); + const messagesFromOutput = importMessagesFromSpans([ + { output: outputFromTrace }, + ]); + console.log( + "Messages from output:", + JSON.stringify(messagesFromOutput, null, 2) + ); + + // Check if assistant message was extracted + const assistantMessages = messagesFromOutput.filter( + (m: any) => m.role === "assistant" + ); + console.log(`Found ${assistantMessages.length} assistant message(s)`); + + if (assistantMessages.length > 0) { + // Find the message with actual text content (not reasoning) + const messageWithText = assistantMessages.find((m: any) => { + const content = JSON.stringify(m.content); + return content.includes("magic 8-ball"); + }); + + if (messageWithText) { + console.log("✅ Found assistant message with magic 8-ball content"); + expect(messageWithText).toBeDefined(); + } else { + console.log("❌ No assistant message contains 'magic 8-ball'"); + expect(messageWithText).toBeDefined(); + } + } else { + console.log("❌ No assistant messages found from output field!"); + expect(assistantMessages.length).toBeGreaterThan(0); + } + }); +}); diff --git a/crates/lingua/src/processing/import.rs b/crates/lingua/src/processing/import.rs index 127a963..9a6c918 100644 --- a/crates/lingua/src/processing/import.rs +++ b/crates/lingua/src/processing/import.rs @@ -111,6 +111,19 @@ fn try_converting_to_messages(data: &Value) -> Vec { } } + // Try Responses API output format + if let Ok(provider_messages) = + serde_json::from_value::>(data_to_parse.clone()) + { + if let Ok(messages) = + as TryFromLLM>>::try_from(provider_messages) + { + if !messages.is_empty() { + return messages; + } + } + } + // Try Anthropic format if let Ok(provider_messages) = serde_json::from_value::>(data_to_parse.clone())