UNPKG

@openai/agents-core

Version:

The OpenAI Agents SDK is a lightweight yet powerful framework for building multi-agent workflows.

176 lines • 6.84 kB
import { encodeUint8ArrayToBase64 } from "../utils/base64.mjs"; import { toUint8ArrayFromBinary } from "../utils/binary.mjs"; import { deduplicateAgentInputItemsPreferringLatest, stripReasoningItemIdForPolicy, } from "./items.mjs"; import { getToolResultCorrelationForCall, getToolResultCorrelationForResult, getToolResultCorrelationKey, } from "./toolResultCorrelation.mjs"; /** Return model-visible representations only when every stored occurrence is covered in order. */ export function getModelVisibleSessionItems(session, stored, exchange) { const comparable = (items) => { // Normalize each record separately: persistence's whole-array deduplication would lose counts. const normalized = items.map((item) => normalizeItemsForSessionPersistence([item])[0]); return (session.prepareHistoryItemsForPersistenceComparison?.(structuredClone(normalized)) ?? normalized); }; const history = comparable(stored); const modelItems = comparable(exchange.items); if (history.length !== stored.length || modelItems.length !== exchange.items.length) { return undefined; } const visible = []; let cursor = 0; for (const item of history) { const replayItem = stripReasoningItemIdForPolicy(item, exchange.reasoningItemIdPolicy); while (cursor < modelItems.length && !sessionItemArraysMatch([item], [modelItems[cursor]]) && !sessionItemArraysMatch([replayItem], [modelItems[cursor]]) && !sessionItemArraysMatch([item], [ stripReasoningItemIdForPolicy(modelItems[cursor], exchange.reasoningItemIdPolicy), ])) { cursor++; } if (cursor === modelItems.length) return undefined; visible.push(exchange.items[cursor++]); } return visible; } /** Check the covered history before automatically compacting an approval group. */ export function hasCompleteApprovalGroups(items) { const pending = new Map(); for (const item of items) { if (item.type !== 'function_call' && item.type !== 'function_call_result' && item.type !== 'hosted_tool_call') { continue; } const call = getToolResultCorrelationForCall(item); const correlation = call ?? getToolResultCorrelationForResult(item); if (!correlation) continue; const key = getToolResultCorrelationKey(correlation); const count = pending.get(key) ?? 0; if (call) { pending.set(key, count + 1); } else if (count === 0) { return false; } else if (count === 1) { pending.delete(key); } else { pending.set(key, count - 1); } } return pending.size === 0; } export function normalizeItemsForSessionPersistence(items) { return deduplicateAgentInputItemsPreferringLatest(items.map((item) => sanitizeValueForSession(stripTransientCallIds(item)))); } export function sessionItemArraysMatch(items, expected) { return (items.length === expected.length && agentItemRangeMatches(items, expected, 0)); } export function agentItemRangeMatches(items, expected, start) { if (start < 0 || start + expected.length > items.length) { return false; } return expected.every((item, offset) => getSessionReconciliationItemKey(items[start + offset]) === getSessionReconciliationItemKey(item)); } function sanitizeValueForSession(value, context = {}) { if (value === null || value === undefined) { return value; } const binary = toUint8ArrayFromBinary(value); if (binary) { return toDataUrlFromBytes(binary, context.mediaType); } if (Array.isArray(value)) { return value.map((entry) => sanitizeValueForSession(entry, context)); } if (!isPlainObject(value)) { return value; } const record = value; const result = {}; const mediaType = typeof record.mediaType === 'string' && record.mediaType.length > 0 ? record.mediaType : context.mediaType; for (const [key, entry] of Object.entries(record)) { const nextContext = key === 'data' || key === 'fileData' ? { mediaType } : context; result[key] = sanitizeValueForSession(entry, nextContext); } return result; } function toDataUrlFromBytes(bytes, mediaType) { const base64 = encodeUint8ArrayToBase64(bytes); const type = mediaType && !mediaType.startsWith('data:') ? mediaType : 'text/plain'; return `data:${type};base64,${base64}`; } function stripTransientCallIds(value) { if (value === null || value === undefined) { return value; } if (Array.isArray(value)) { return value.map((entry) => stripTransientCallIds(entry)); } if (!isPlainObject(value)) { return value; } const record = value; const result = {}; const isProtocolItem = typeof record.type === 'string' && record.type.length > 0; const shouldStripId = isProtocolItem && shouldStripIdForProtocolItem(record); for (const [key, entry] of Object.entries(record)) { if (shouldStripId && key === 'id') { continue; } result[key] = stripTransientCallIds(entry); } return result; } function shouldStripIdForProtocolItem(record) { switch (record.type) { case 'function_call': case 'function_call_result': return true; case 'tool_search_call': case 'tool_search_output': return hasToolSearchCallId(record); default: return false; } } function hasToolSearchCallId(record) { const topLevelCallId = record.call_id ?? record.callId; if (typeof topLevelCallId === 'string' && topLevelCallId.length > 0) { return true; } const providerData = isPlainObject(record.providerData) ? record.providerData : undefined; const providerCallId = providerData?.call_id ?? providerData?.callId; return typeof providerCallId === 'string' && providerCallId.length > 0; } function getSessionReconciliationItemKey(item) { return JSON.stringify(sortSessionReconciliationValue(item)); } function sortSessionReconciliationValue(value) { if (Array.isArray(value)) { return value.map(sortSessionReconciliationValue); } if (!isPlainObject(value)) { return value; } return Object.fromEntries(Object.keys(value) .sort() .map((key) => [key, sortSessionReconciliationValue(value[key])])); } function isPlainObject(value) { if (typeof value !== 'object' || value === null) { return false; } const proto = Object.getPrototypeOf(value); return proto === Object.prototype || proto === null; } //# sourceMappingURL=sessionItems.mjs.map