From 154d5456126cb9502fd77c08368ee5d62719d913 Mon Sep 17 00:00:00 2001 From: Steven C Date: Mon, 10 Nov 2025 16:30:12 -0500 Subject: [PATCH 1/6] Agent parse conversation history --- src/__tests__/unit/agents.test.ts | 94 +++++++++++++++++++-- src/agents.ts | 136 ++++++++++++++++++++++++++---- 2 files changed, 209 insertions(+), 21 deletions(-) diff --git a/src/__tests__/unit/agents.test.ts b/src/__tests__/unit/agents.test.ts index 4486b34..2308d90 100644 --- a/src/__tests__/unit/agents.test.ts +++ b/src/__tests__/unit/agents.test.ts @@ -358,14 +358,98 @@ describe('GuardrailAgent', () => { // Test the guardrail function const guardrailFunction = agent.inputGuardrails[0]; - const result = await guardrailFunction.execute('test input'); + const result = await guardrailFunction.execute('test input'); - expect(result).toHaveProperty('outputInfo'); - expect(result).toHaveProperty('tripwireTriggered'); - expect(typeof result.tripwireTriggered).toBe('boolean'); + expect(result).toHaveProperty('outputInfo'); + expect(result).toHaveProperty('tripwireTriggered'); + expect(typeof result.tripwireTriggered).toBe('boolean'); + }); + + it('passes the latest user message text to guardrails for conversation inputs', async () => { + process.env.OPENAI_API_KEY = 'test'; + const config = { + version: 1, + input: { + version: 1, + guardrails: [{ name: 'Moderation', config: {} }], + }, + }; + + const { instantiateGuardrails } = await import('../../runtime'); + const runSpy = vi.fn().mockResolvedValue({ + tripwireTriggered: false, + info: { guardrail_name: 'Moderation' }, }); - it('should handle guardrail execution errors based on raiseGuardrailErrors setting', async () => { + vi.mocked(instantiateGuardrails).mockImplementationOnce(() => + Promise.resolve([ + { + definition: { + name: 'Moderation', + description: 'Moderation guardrail', + mediaType: 'text/plain', + configSchema: z.object({}), + checkFn: vi.fn(), + metadata: {}, + ctxRequirements: z.object({}), + schema: () => ({}), + instantiate: vi.fn(), + }, + config: {}, + run: runSpy, + } as unknown as Parameters[0] extends Promise + ? T extends readonly (infer U)[] + ? U + : never + : never, + ]) + ); + + const agent = (await GuardrailAgent.create( + config, + 'Conversation Agent', + 'Handle multi-turn conversations' + )) as MockAgent; + + const guardrail = agent.inputGuardrails[0] as unknown as { + execute: (args: { input: unknown; context?: unknown }) => Promise<{ + outputInfo: Record; + tripwireTriggered: boolean; + }>; + }; + + const conversation = [ + { role: 'system', content: 'You are helpful.' }, + { role: 'user', content: [{ type: 'input_text', text: 'First question?' }] }, + { role: 'assistant', content: [{ type: 'output_text', text: 'An answer.' }] }, + { + role: 'user', + content: [ + { type: 'input_text', text: 'Latest user message' }, + { type: 'input_text', text: 'with additional context.' }, + ], + }, + ]; + + const result = await guardrail.execute({ input: conversation, context: {} }); + + expect(runSpy).toHaveBeenCalledTimes(1); + const [ctxArgRaw, dataArg] = runSpy.mock.calls[0] as [unknown, string]; + const ctxArg = ctxArgRaw as { getConversationHistory?: () => unknown[] }; + expect(dataArg).toBe('Latest user message with additional context.'); + expect(typeof ctxArg.getConversationHistory).toBe('function'); + + const history = ctxArg.getConversationHistory?.() as Array<{ content?: unknown }> | undefined; + expect(Array.isArray(history)).toBe(true); + expect(history && history[history.length - 1]?.content).toBe( + 'Latest user message with additional context.' + ); + + expect(result.tripwireTriggered).toBe(false); + expect(result.outputInfo.input).toBe('Latest user message with additional context.'); + }); + + it('should handle guardrail execution errors based on raiseGuardrailErrors setting', async () => { process.env.OPENAI_API_KEY = 'test'; const config = { version: 1, diff --git a/src/agents.ts b/src/agents.ts index 43fafdd..7f75630 100644 --- a/src/agents.ts +++ b/src/agents.ts @@ -13,8 +13,7 @@ import type { InputGuardrailFunctionArgs, OutputGuardrailFunctionArgs, } from '@openai/agents-core'; -import { GuardrailLLMContext, GuardrailResult, TextOnlyContent, ContentPart } from './types'; -import { ContentUtils } from './utils/content'; +import { GuardrailLLMContext, GuardrailResult, TextOnlyContent } from './types'; import { loadPipelineBundles, instantiateGuardrails, @@ -250,6 +249,122 @@ function ensureGuardrailContext( } as GuardrailLLMContext; } +function extractTextFromContentParts(content: unknown): string { + if (typeof content === 'string') { + return content.trim(); + } + + if (Array.isArray(content)) { + const parts: string[] = []; + for (const item of content) { + const text = extractTextFromMessageEntry(item); + if (text) { + parts.push(text); + } + } + return parts.join(' ').trim(); + } + + if (content && typeof content === 'object') { + const record = content as Record; + if (typeof record.text === 'string') { + return record.text.trim(); + } + if (record.content !== undefined) { + const nested = extractTextFromContentParts(record.content); + if (nested) { + return nested; + } + } + } + + return ''; +} + +function extractTextFromMessageEntry(entry: unknown): string { + if (entry == null) { + return ''; + } + + if (typeof entry === 'string') { + return entry.trim(); + } + + if (Array.isArray(entry)) { + return extractTextFromContentParts(entry); + } + + if (typeof entry === 'object') { + const record = entry as Record; + if (record.content !== undefined) { + const contentText = extractTextFromContentParts(record.content); + if (contentText) { + return contentText; + } + } + + if (typeof record.text === 'string') { + return record.text.trim(); + } + } + + return ''; +} + +function extractTextFromAgentInput(input: unknown): string { + if (typeof input === 'string') { + return input.trim(); + } + + if (Array.isArray(input)) { + for (let idx = input.length - 1; idx >= 0; idx -= 1) { + const candidate = input[idx]; + if (candidate && typeof candidate === 'object') { + const record = candidate as Record; + if (record.role === 'user') { + const text = extractTextFromMessageEntry(candidate); + if (text) { + return text; + } + } + } else { + const text = extractTextFromMessageEntry(candidate); + if (text) { + return text; + } + } + } + return ''; + } + + if (input && typeof input === 'object') { + const record = input as Record; + if (record.role === 'user') { + const text = extractTextFromMessageEntry(record); + if (text) { + return text; + } + } + + if (record.content !== undefined) { + const contentText = extractTextFromContentParts(record.content); + if (contentText) { + return contentText; + } + } + + if (typeof record.text === 'string') { + return record.text.trim(); + } + } + + if (input == null) { + return ''; + } + + return String(input); +} + function extractLatestUserText(history: NormalizedConversationEntry[]): string { for (let i = history.length - 1; i >= 0; i -= 1) { const entry = history[i]; @@ -261,20 +376,9 @@ function extractLatestUserText(history: NormalizedConversationEntry[]): string { } function resolveInputText(input: unknown, history: NormalizedConversationEntry[]): string { - if (typeof input === 'string') { - return input; - } - - if (input && typeof input === 'object' && 'content' in (input as Record)) { - const content = (input as { content: string | ContentPart[] }).content; - const message = { - role: 'user', - content, - }; - const extracted = ContentUtils.extractTextFromMessage(message); - if (extracted) { - return extracted; - } + const directText = extractTextFromAgentInput(input); + if (directText) { + return directText; } return extractLatestUserText(history); From 612a5c4e968296a34944c5889e7399f9398e6718 Mon Sep 17 00:00:00 2001 From: Steven C Date: Mon, 10 Nov 2025 17:02:45 -0500 Subject: [PATCH 2/6] Add doc strings. Extract shared logic --- src/__tests__/unit/agents.test.ts | 162 +++++++++++++++--------------- src/agents.ts | 80 ++++++++++++--- 2 files changed, 145 insertions(+), 97 deletions(-) diff --git a/src/__tests__/unit/agents.test.ts b/src/__tests__/unit/agents.test.ts index 2308d90..58a9791 100644 --- a/src/__tests__/unit/agents.test.ts +++ b/src/__tests__/unit/agents.test.ts @@ -358,98 +358,98 @@ describe('GuardrailAgent', () => { // Test the guardrail function const guardrailFunction = agent.inputGuardrails[0]; - const result = await guardrailFunction.execute('test input'); + const result = await guardrailFunction.execute('test input'); - expect(result).toHaveProperty('outputInfo'); - expect(result).toHaveProperty('tripwireTriggered'); - expect(typeof result.tripwireTriggered).toBe('boolean'); - }); + expect(result).toHaveProperty('outputInfo'); + expect(result).toHaveProperty('tripwireTriggered'); + expect(typeof result.tripwireTriggered).toBe('boolean'); + }); - it('passes the latest user message text to guardrails for conversation inputs', async () => { - process.env.OPENAI_API_KEY = 'test'; - const config = { - version: 1, - input: { + it('passes the latest user message text to guardrails for conversation inputs', async () => { + process.env.OPENAI_API_KEY = 'test'; + const config = { version: 1, - guardrails: [{ name: 'Moderation', config: {} }], - }, - }; + input: { + version: 1, + guardrails: [{ name: 'Moderation', config: {} }], + }, + }; - const { instantiateGuardrails } = await import('../../runtime'); - const runSpy = vi.fn().mockResolvedValue({ - tripwireTriggered: false, - info: { guardrail_name: 'Moderation' }, - }); + const { instantiateGuardrails } = await import('../../runtime'); + const runSpy = vi.fn().mockResolvedValue({ + tripwireTriggered: false, + info: { guardrail_name: 'Moderation' }, + }); - vi.mocked(instantiateGuardrails).mockImplementationOnce(() => - Promise.resolve([ + vi.mocked(instantiateGuardrails).mockImplementationOnce(() => + Promise.resolve([ + { + definition: { + name: 'Moderation', + description: 'Moderation guardrail', + mediaType: 'text/plain', + configSchema: z.object({}), + checkFn: vi.fn(), + metadata: {}, + ctxRequirements: z.object({}), + schema: () => ({}), + instantiate: vi.fn(), + }, + config: {}, + run: runSpy, + } as unknown as Parameters[0] extends Promise + ? T extends readonly (infer U)[] + ? U + : never + : never, + ]) + ); + + const agent = (await GuardrailAgent.create( + config, + 'Conversation Agent', + 'Handle multi-turn conversations' + )) as MockAgent; + + const guardrail = agent.inputGuardrails[0] as unknown as { + execute: (args: { input: unknown; context?: unknown }) => Promise<{ + outputInfo: Record; + tripwireTriggered: boolean; + }>; + }; + + const conversation = [ + { role: 'system', content: 'You are helpful.' }, + { role: 'user', content: [{ type: 'input_text', text: 'First question?' }] }, + { role: 'assistant', content: [{ type: 'output_text', text: 'An answer.' }] }, { - definition: { - name: 'Moderation', - description: 'Moderation guardrail', - mediaType: 'text/plain', - configSchema: z.object({}), - checkFn: vi.fn(), - metadata: {}, - ctxRequirements: z.object({}), - schema: () => ({}), - instantiate: vi.fn(), - }, - config: {}, - run: runSpy, - } as unknown as Parameters[0] extends Promise - ? T extends readonly (infer U)[] - ? U - : never - : never, - ]) - ); - - const agent = (await GuardrailAgent.create( - config, - 'Conversation Agent', - 'Handle multi-turn conversations' - )) as MockAgent; - - const guardrail = agent.inputGuardrails[0] as unknown as { - execute: (args: { input: unknown; context?: unknown }) => Promise<{ - outputInfo: Record; - tripwireTriggered: boolean; - }>; - }; - - const conversation = [ - { role: 'system', content: 'You are helpful.' }, - { role: 'user', content: [{ type: 'input_text', text: 'First question?' }] }, - { role: 'assistant', content: [{ type: 'output_text', text: 'An answer.' }] }, - { - role: 'user', - content: [ - { type: 'input_text', text: 'Latest user message' }, - { type: 'input_text', text: 'with additional context.' }, - ], - }, - ]; + role: 'user', + content: [ + { type: 'input_text', text: 'Latest user message' }, + { type: 'input_text', text: 'with additional context.' }, + ], + }, + ]; - const result = await guardrail.execute({ input: conversation, context: {} }); + const result = await guardrail.execute({ input: conversation, context: {} }); - expect(runSpy).toHaveBeenCalledTimes(1); - const [ctxArgRaw, dataArg] = runSpy.mock.calls[0] as [unknown, string]; - const ctxArg = ctxArgRaw as { getConversationHistory?: () => unknown[] }; - expect(dataArg).toBe('Latest user message with additional context.'); - expect(typeof ctxArg.getConversationHistory).toBe('function'); + expect(runSpy).toHaveBeenCalledTimes(1); + const [ctxArgRaw, dataArg] = runSpy.mock.calls[0] as [unknown, string]; + const ctxArg = ctxArgRaw as { getConversationHistory?: () => unknown[] }; + expect(dataArg).toBe('Latest user message with additional context.'); + expect(typeof ctxArg.getConversationHistory).toBe('function'); - const history = ctxArg.getConversationHistory?.() as Array<{ content?: unknown }> | undefined; - expect(Array.isArray(history)).toBe(true); - expect(history && history[history.length - 1]?.content).toBe( - 'Latest user message with additional context.' - ); + const history = ctxArg.getConversationHistory?.() as Array<{ content?: unknown }> | undefined; + expect(Array.isArray(history)).toBe(true); + expect(history && history[history.length - 1]?.content).toBe( + 'Latest user message with additional context.' + ); - expect(result.tripwireTriggered).toBe(false); - expect(result.outputInfo.input).toBe('Latest user message with additional context.'); - }); + expect(result.tripwireTriggered).toBe(false); + expect(result.outputInfo.input).toBe('Latest user message with additional context.'); + }); - it('should handle guardrail execution errors based on raiseGuardrailErrors setting', async () => { + it('should handle guardrail execution errors based on raiseGuardrailErrors setting', async () => { process.env.OPENAI_API_KEY = 'test'; const config = { version: 1, diff --git a/src/agents.ts b/src/agents.ts index 7f75630..6225171 100644 --- a/src/agents.ts +++ b/src/agents.ts @@ -249,15 +249,30 @@ function ensureGuardrailContext( } as GuardrailLLMContext; } -function extractTextFromContentParts(content: unknown): string { - if (typeof content === 'string') { - return content.trim(); +const TEXTUAL_CONTENT_TYPES = new Set(['input_text', 'text', 'output_text', 'summary_text']); +const MAX_CONTENT_EXTRACTION_DEPTH = 10; + +/** + * Extract text from any nested content value with optional type filtering. + * + * @param value Arbitrary content value (string, array, or object) to inspect. + * @param depth Current recursion depth, used to guard against circular structures. + * @param filterByType When true, only content parts with recognised text types are returned. + * @returns The extracted text, or an empty string when no text is found. + */ +function extractTextFromValue(value: unknown, depth: number, filterByType: boolean): string { + if (depth > MAX_CONTENT_EXTRACTION_DEPTH) { + return ''; + } + + if (typeof value === 'string') { + return value.trim(); } - if (Array.isArray(content)) { + if (Array.isArray(value)) { const parts: string[] = []; - for (const item of content) { - const text = extractTextFromMessageEntry(item); + for (const item of value) { + const text = extractTextFromValue(item, depth + 1, filterByType); if (text) { parts.push(text); } @@ -265,13 +280,19 @@ function extractTextFromContentParts(content: unknown): string { return parts.join(' ').trim(); } - if (content && typeof content === 'object') { - const record = content as Record; + if (value && typeof value === 'object') { + const record = value as Record; + const typeValue = typeof record.type === 'string' ? record.type : null; + const isRecognisedTextType = typeValue ? TEXTUAL_CONTENT_TYPES.has(typeValue) : false; + if (typeof record.text === 'string') { - return record.text.trim(); + if (!filterByType || isRecognisedTextType) { + return record.text.trim(); + } } + if (record.content !== undefined) { - const nested = extractTextFromContentParts(record.content); + const nested = extractTextFromValue(record.content, depth + 1, filterByType); if (nested) { return nested; } @@ -281,7 +302,27 @@ function extractTextFromContentParts(content: unknown): string { return ''; } -function extractTextFromMessageEntry(entry: unknown): string { +/** + * Extract text from structured content parts (e.g., the `content` field on a message). + * + * Only recognised textual content-part types are considered to match the behaviour of + * `ContentUtils.filterToTextOnly`, ensuring non-text modalities are ignored. + */ +function extractTextFromContentParts(content: unknown, depth = 0): string { + return extractTextFromValue(content, depth, true); +} + +/** + * Extract text from a single message entry. + * + * Handles strings, arrays of content parts, or message-like objects that contain a + * `content` collection or a plain `text` field. + */ +function extractTextFromMessageEntry(entry: unknown, depth = 0): string { + if (depth > MAX_CONTENT_EXTRACTION_DEPTH) { + return ''; + } + if (entry == null) { return ''; } @@ -291,13 +332,14 @@ function extractTextFromMessageEntry(entry: unknown): string { } if (Array.isArray(entry)) { - return extractTextFromContentParts(entry); + return extractTextFromContentParts(entry, depth + 1); } if (typeof entry === 'object') { const record = entry as Record; + if (record.content !== undefined) { - const contentText = extractTextFromContentParts(record.content); + const contentText = extractTextFromContentParts(record.content, depth + 1); if (contentText) { return contentText; } @@ -308,9 +350,15 @@ function extractTextFromMessageEntry(entry: unknown): string { } } - return ''; + return extractTextFromValue(entry, depth + 1, false); } +/** + * Extract the latest user-authored text from raw agent input. + * + * Accepts strings, message objects, or arrays of mixed items. Arrays are scanned + * from newest to oldest, returning the first user-role message with textual content. + */ function extractTextFromAgentInput(input: unknown): string { if (typeof input === 'string') { return input.trim(); @@ -327,8 +375,8 @@ function extractTextFromAgentInput(input: unknown): string { return text; } } - } else { - const text = extractTextFromMessageEntry(candidate); + } else if (typeof candidate === 'string') { + const text = candidate.trim(); if (text) { return text; } From f684ff0ed1de9aa756ba55e5a082806627f8603c Mon Sep 17 00:00:00 2001 From: Steven C Date: Mon, 10 Nov 2025 17:19:13 -0500 Subject: [PATCH 3/6] Nit Copilot comments --- src/agents.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/agents.ts b/src/agents.ts index 6225171..29a6c78 100644 --- a/src/agents.ts +++ b/src/agents.ts @@ -257,7 +257,7 @@ const MAX_CONTENT_EXTRACTION_DEPTH = 10; * * @param value Arbitrary content value (string, array, or object) to inspect. * @param depth Current recursion depth, used to guard against circular structures. - * @param filterByType When true, only content parts with recognised text types are returned. + * @param filterByType When true, only content parts with recognized text types are returned. * @returns The extracted text, or an empty string when no text is found. */ function extractTextFromValue(value: unknown, depth: number, filterByType: boolean): string { @@ -283,10 +283,10 @@ function extractTextFromValue(value: unknown, depth: number, filterByType: boole if (value && typeof value === 'object') { const record = value as Record; const typeValue = typeof record.type === 'string' ? record.type : null; - const isRecognisedTextType = typeValue ? TEXTUAL_CONTENT_TYPES.has(typeValue) : false; + const isRecognizedTextType = typeValue ? TEXTUAL_CONTENT_TYPES.has(typeValue) : false; if (typeof record.text === 'string') { - if (!filterByType || isRecognisedTextType) { + if (!filterByType || isRecognizedTextType) { return record.text.trim(); } } @@ -305,8 +305,8 @@ function extractTextFromValue(value: unknown, depth: number, filterByType: boole /** * Extract text from structured content parts (e.g., the `content` field on a message). * - * Only recognised textual content-part types are considered to match the behaviour of - * `ContentUtils.filterToTextOnly`, ensuring non-text modalities are ignored. + * Only textual content-part types enumerated in TEXTUAL_CONTENT_TYPES are considered so + * that non-text modalities (images, tools, etc.) remain ignored. */ function extractTextFromContentParts(content: unknown, depth = 0): string { return extractTextFromValue(content, depth, true); From 793b39c1c355357e98c321086c8d2cb3d19124fb Mon Sep 17 00:00:00 2001 From: Steven C Date: Mon, 10 Nov 2025 17:33:31 -0500 Subject: [PATCH 4/6] Add null guard --- src/agents.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/agents.ts b/src/agents.ts index 29a6c78..0564788 100644 --- a/src/agents.ts +++ b/src/agents.ts @@ -286,13 +286,15 @@ function extractTextFromValue(value: unknown, depth: number, filterByType: boole const isRecognizedTextType = typeValue ? TEXTUAL_CONTENT_TYPES.has(typeValue) : false; if (typeof record.text === 'string') { - if (!filterByType || isRecognizedTextType) { + if (!filterByType || isRecognizedTextType || typeValue === null) { return record.text.trim(); } } - if (record.content !== undefined) { - const nested = extractTextFromValue(record.content, depth + 1, filterByType); + const contentValue = record.content; + // If a direct text field was skipped due to type filtering, fall back to nested content. + if (contentValue != null) { + const nested = extractTextFromValue(contentValue, depth + 1, filterByType); if (nested) { return nested; } From 8583cdd203886cf714a27bec3f630f301974ee0c Mon Sep 17 00:00:00 2001 From: Steven C Date: Mon, 10 Nov 2025 17:45:00 -0500 Subject: [PATCH 5/6] Address Copilot comments --- src/agents.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/agents.ts b/src/agents.ts index 0564788..17f3e51 100644 --- a/src/agents.ts +++ b/src/agents.ts @@ -352,7 +352,7 @@ function extractTextFromMessageEntry(entry: unknown, depth = 0): string { } } - return extractTextFromValue(entry, depth + 1, false); + return extractTextFromValue(entry, depth + 1, false /* allow all types when falling back */); } /** @@ -362,6 +362,10 @@ function extractTextFromMessageEntry(entry: unknown, depth = 0): string { * from newest to oldest, returning the first user-role message with textual content. */ function extractTextFromAgentInput(input: unknown): string { + if (input == null) { + return ''; + } + if (typeof input === 'string') { return input.trim(); } @@ -408,10 +412,6 @@ function extractTextFromAgentInput(input: unknown): string { } } - if (input == null) { - return ''; - } - return String(input); } From f2b5cb9b704cd96117511186c1247cdeb8e7d08f Mon Sep 17 00:00:00 2001 From: Steven C Date: Mon, 10 Nov 2025 17:57:49 -0500 Subject: [PATCH 6/6] Reuse content types --- src/agents.ts | 15 ++++++++++++--- src/utils/content.ts | 8 ++++---- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/agents.ts b/src/agents.ts index 17f3e51..95d897d 100644 --- a/src/agents.ts +++ b/src/agents.ts @@ -14,6 +14,7 @@ import type { OutputGuardrailFunctionArgs, } from '@openai/agents-core'; import { GuardrailLLMContext, GuardrailResult, TextOnlyContent } from './types'; +import { TEXT_CONTENT_TYPES } from './utils/content'; import { loadPipelineBundles, instantiateGuardrails, @@ -249,7 +250,7 @@ function ensureGuardrailContext( } as GuardrailLLMContext; } -const TEXTUAL_CONTENT_TYPES = new Set(['input_text', 'text', 'output_text', 'summary_text']); +const TEXTUAL_CONTENT_TYPES = new Set(TEXT_CONTENT_TYPES); const MAX_CONTENT_EXTRACTION_DEPTH = 10; /** @@ -400,7 +401,7 @@ function extractTextFromAgentInput(input: unknown): string { } } - if (record.content !== undefined) { + if (record.content != null) { const contentText = extractTextFromContentParts(record.content); if (contentText) { return contentText; @@ -412,7 +413,15 @@ function extractTextFromAgentInput(input: unknown): string { } } - return String(input); + if ( + typeof input === 'number' || + typeof input === 'boolean' || + typeof input === 'bigint' + ) { + return String(input); + } + + return ''; } function extractLatestUserText(history: NormalizedConversationEntry[]): string { diff --git a/src/utils/content.ts b/src/utils/content.ts index a5e74ad..9cafd32 100644 --- a/src/utils/content.ts +++ b/src/utils/content.ts @@ -7,15 +7,15 @@ import { Message, ContentPart, TextContentPart, TextOnlyMessageArray } from '../types'; +export const TEXT_CONTENT_TYPES = ['input_text', 'text', 'output_text', 'summary_text'] as const; +const TEXT_CONTENT_TYPES_SET = new Set(TEXT_CONTENT_TYPES); + export class ContentUtils { - // Clear: what types are considered text - private static readonly TEXT_TYPES = ['input_text', 'text', 'output_text', 'summary_text'] as const; - /** * Check if a content part is text-based. */ static isText(part: ContentPart): boolean { - return this.TEXT_TYPES.includes(part.type as typeof this.TEXT_TYPES[number]); + return typeof part.type === 'string' && TEXT_CONTENT_TYPES_SET.has(part.type); } /**