@openai/agents-core
Version:
The OpenAI Agents SDK is a lightweight yet powerful framework for building multi-agent workflows.
176 lines • 6.84 kB
JavaScript
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