@openai/agents-core
Version:
The OpenAI Agents SDK is a lightweight yet powerful framework for building multi-agent workflows.
438 lines • 16.6 kB
JavaScript
import { UserError } from "../errors.mjs";
import { isOpenAIResponsesCompactionAwareSession, } from "../memory/session.mjs";
import { Usage } from "../usage.mjs";
import { encodeUint8ArrayToBase64 } from "../utils/base64.mjs";
import { toUint8ArrayFromBinary } from "../utils/binary.mjs";
import { buildAgentInputPool, extractOutputItemsFromRunItems, toAgentInputList, getAgentInputItemKey, removeAgentInputFromPool, } from "./items.mjs";
import logger from "../logger.mjs";
export function createSessionPersistenceTracker(options) {
const { session } = options;
if (!session) {
return undefined;
}
class SessionPersistenceTrackerImpl {
session;
hasCallModelInputFilter;
persistInput;
originalSnapshot;
filteredSnapshot;
pendingWriteCounts;
persistedInput = false;
constructor() {
this.session = options.session;
this.hasCallModelInputFilter = options.hasCallModelInputFilter;
this.persistInput = options.persistInput;
this.originalSnapshot = options.resumingFromState ? [] : undefined;
this.filteredSnapshot = undefined;
this.pendingWriteCounts = options.resumingFromState
? new Map()
: undefined;
}
setPreparedItems = (items) => {
const sessionItems = items ?? [];
this.originalSnapshot = sessionItems.map((item) => structuredClone(item));
this.pendingWriteCounts = new Map();
for (const item of sessionItems) {
const key = getAgentInputItemKey(item);
this.pendingWriteCounts.set(key, (this.pendingWriteCounts.get(key) ?? 0) + 1);
}
};
recordTurnItems = (sourceItems, filteredItems) => {
const pendingCounts = this.pendingWriteCounts;
if (filteredItems !== undefined) {
if (!pendingCounts) {
this.filteredSnapshot = cloneItems(filteredItems);
return;
}
const nextSnapshot = collectPersistableFilteredItems({
pendingCounts,
sourceItems,
filteredItems,
existingSnapshot: this.filteredSnapshot,
});
if (nextSnapshot !== undefined) {
this.filteredSnapshot = nextSnapshot;
}
return;
}
this.filteredSnapshot = buildSnapshotForUnfilteredItems({
pendingCounts,
sourceItems,
existingSnapshot: this.filteredSnapshot,
});
};
getItemsForPersistence = () => {
if (this.filteredSnapshot !== undefined) {
return this.filteredSnapshot;
}
if (this.hasCallModelInputFilter) {
return undefined;
}
return this.originalSnapshot;
};
buildPersistInputOnce = (serverManagesConversation) => {
if (!this.session || serverManagesConversation) {
return undefined;
}
const persistInput = this.persistInput ?? saveStreamInputToSession;
return async () => {
if (this.persistedInput) {
return;
}
const itemsToPersist = this.getItemsForPersistence();
if (!itemsToPersist || itemsToPersist.length === 0) {
return;
}
this.persistedInput = true;
await persistInput(this.session, itemsToPersist);
};
};
}
return new SessionPersistenceTrackerImpl();
}
function cloneItems(items) {
return items.map((item) => structuredClone(item));
}
function buildSourceOccurrenceCounts(sourceItems) {
const sourceOccurrenceCounts = new WeakMap();
for (const source of sourceItems) {
if (!source || typeof source !== 'object') {
continue;
}
const nextCount = (sourceOccurrenceCounts.get(source) ?? 0) + 1;
sourceOccurrenceCounts.set(source, nextCount);
}
return sourceOccurrenceCounts;
}
function collectPersistableFilteredItems(options) {
const { pendingCounts, sourceItems, filteredItems, existingSnapshot } = options;
const persistableItems = [];
const sourceOccurrenceCounts = buildSourceOccurrenceCounts(sourceItems);
const consumeAnyPendingWriteSlot = () => {
for (const [key, remaining] of pendingCounts) {
if (remaining > 0) {
pendingCounts.set(key, remaining - 1);
return true;
}
}
return false;
};
for (let i = 0; i < filteredItems.length; i++) {
const filteredItem = filteredItems[i];
if (!filteredItem) {
continue;
}
let allocated = false;
const source = sourceItems[i];
if (source && typeof source === 'object') {
const pendingOccurrences = (sourceOccurrenceCounts.get(source) ?? 0) - 1;
sourceOccurrenceCounts.set(source, pendingOccurrences);
if (pendingOccurrences > 0) {
continue;
}
const sourceKey = getAgentInputItemKey(source);
const remaining = pendingCounts.get(sourceKey) ?? 0;
if (remaining > 0) {
pendingCounts.set(sourceKey, remaining - 1);
persistableItems.push(structuredClone(filteredItem));
allocated = true;
continue;
}
}
const filteredKey = getAgentInputItemKey(filteredItem);
const filteredRemaining = pendingCounts.get(filteredKey) ?? 0;
if (filteredRemaining > 0) {
pendingCounts.set(filteredKey, filteredRemaining - 1);
persistableItems.push(structuredClone(filteredItem));
allocated = true;
continue;
}
if (!source && consumeAnyPendingWriteSlot()) {
persistableItems.push(structuredClone(filteredItem));
allocated = true;
}
if (!allocated && !source && existingSnapshot === undefined) {
persistableItems.push(structuredClone(filteredItem));
}
}
if (persistableItems.length > 0 || existingSnapshot === undefined) {
return persistableItems;
}
return existingSnapshot;
}
function buildSnapshotForUnfilteredItems(options) {
const { pendingCounts, sourceItems, existingSnapshot } = options;
if (!pendingCounts) {
const filtered = sourceItems
.filter((item) => Boolean(item))
.map((item) => structuredClone(item));
return filtered.length > 0
? filtered
: existingSnapshot === undefined
? []
: existingSnapshot;
}
const filtered = [];
for (const item of sourceItems) {
if (!item) {
continue;
}
const key = getAgentInputItemKey(item);
const remaining = pendingCounts.get(key) ?? 0;
if (remaining <= 0) {
continue;
}
pendingCounts.set(key, remaining - 1);
filtered.push(structuredClone(item));
}
if (filtered.length > 0) {
return filtered;
}
return existingSnapshot === undefined ? [] : existingSnapshot;
}
export async function saveToSession(session, sessionInputItems, result) {
const state = result.state;
const alreadyPersisted = state._currentTurnPersistedItemCount ?? 0;
const newRunItems = result.newItems.slice(alreadyPersisted);
if (process.env.OPENAI_AGENTS__DEBUG_SAVE_SESSION) {
console.debug('saveToSession:newRunItems', newRunItems.map((item) => item.type));
}
await persistRunItemsToSession({
session,
state,
newRunItems,
extraInputItems: sessionInputItems,
lastResponseId: result.lastResponseId,
alreadyPersistedCount: alreadyPersisted,
});
}
export async function saveStreamInputToSession(session, sessionInputItems) {
if (!session) {
return;
}
if (!sessionInputItems || sessionInputItems.length === 0) {
return;
}
const sanitizedInput = normalizeItemsForSessionPersistence(sessionInputItems);
await session.addItems(sanitizedInput);
}
export async function saveStreamResultToSession(session, result) {
const state = result.state;
const alreadyPersisted = state._currentTurnPersistedItemCount ?? 0;
const newRunItems = result.newItems.slice(alreadyPersisted);
await persistRunItemsToSession({
session,
state,
newRunItems,
lastResponseId: result.lastResponseId,
alreadyPersistedCount: alreadyPersisted,
});
}
export async function prepareInputItemsWithSession(input, session, sessionInputCallback, options) {
if (!session) {
return {
preparedInput: input,
sessionItems: undefined,
};
}
const includeHistoryInPreparedInput = options?.includeHistoryInPreparedInput ?? true;
const preserveDroppedNewItems = options?.preserveDroppedNewItems ?? false;
const history = await session.getItems();
const newInputItems = toAgentInputList(input);
if (!sessionInputCallback) {
return {
preparedInput: includeHistoryInPreparedInput
? [...history, ...newInputItems]
: newInputItems,
sessionItems: newInputItems,
};
}
const historySnapshot = history.slice();
const newInputSnapshot = newInputItems.slice();
const combined = await sessionInputCallback(history, newInputItems);
if (!Array.isArray(combined)) {
throw new UserError('Session input callback must return an array of AgentInputItem objects.');
}
const historyCounts = buildItemFrequencyMap(historySnapshot);
const newInputCounts = buildItemFrequencyMap(newInputSnapshot);
const historyRefs = buildAgentInputPool(historySnapshot);
const newInputRefs = buildAgentInputPool(newInputSnapshot);
const appended = [];
for (const item of combined) {
const key = getAgentInputItemKey(item);
if (removeAgentInputFromPool(newInputRefs, item)) {
decrementCount(newInputCounts, key);
appended.push(item);
continue;
}
if (removeAgentInputFromPool(historyRefs, item)) {
decrementCount(historyCounts, key);
continue;
}
const historyRemaining = historyCounts.get(key) ?? 0;
if (historyRemaining > 0) {
historyCounts.set(key, historyRemaining - 1);
continue;
}
const newRemaining = newInputCounts.get(key) ?? 0;
if (newRemaining > 0) {
newInputCounts.set(key, newRemaining - 1);
appended.push(item);
continue;
}
appended.push(item);
}
const preparedItems = includeHistoryInPreparedInput
? combined
: appended.length > 0
? appended
: preserveDroppedNewItems
? newInputSnapshot
: [];
if (preserveDroppedNewItems &&
appended.length === 0 &&
newInputSnapshot.length > 0) {
// In server-managed conversations we cannot drop the turn delta; restore it and warn callers.
logger.warn('sessionInputCallback dropped all new inputs in a server-managed conversation; original turn inputs were restored to avoid losing the API delta. Keep at least one new item or omit conversationId if you intended to drop them.');
}
return {
preparedInput: preparedItems,
sessionItems: appended,
};
}
function normalizeItemsForSessionPersistence(items) {
return items.map((item) => sanitizeValueForSession(stripTransientCallIds(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 isPlainObject(value) {
if (typeof value !== 'object' || value === null) {
return false;
}
const proto = Object.getPrototypeOf(value);
return proto === Object.prototype || proto === null;
}
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 && shouldStripIdForType(record.type);
for (const [key, entry] of Object.entries(record)) {
if (shouldStripId && key === 'id') {
continue;
}
result[key] = stripTransientCallIds(entry);
}
return result;
}
function shouldStripIdForType(type) {
switch (type) {
case 'function_call':
case 'function_call_result':
return true;
default:
return false;
}
}
async function persistRunItemsToSession(options) {
const { session, state, newRunItems, extraInputItems = [], lastResponseId, alreadyPersistedCount, } = options;
if (!session) {
return;
}
const itemsToSave = [
...extraInputItems,
...extractOutputItemsFromRunItems(newRunItems, state._reasoningItemIdPolicy),
];
if (itemsToSave.length === 0) {
state._currentTurnPersistedItemCount =
alreadyPersistedCount + newRunItems.length;
await runCompactionOnSession(session, lastResponseId, state);
return;
}
const sanitizedItems = normalizeItemsForSessionPersistence(itemsToSave);
await session.addItems(sanitizedItems);
await runCompactionOnSession(session, lastResponseId, state);
state._currentTurnPersistedItemCount =
alreadyPersistedCount + newRunItems.length;
}
async function runCompactionOnSession(session, responseId, state) {
if (!isOpenAIResponsesCompactionAwareSession(session)) {
return;
}
const store = state._lastModelSettings?.store ?? state._currentAgent.modelSettings?.store;
const compactionArgs = typeof responseId === 'undefined' && typeof store === 'undefined'
? undefined
: {
...(typeof responseId === 'undefined' ? {} : { responseId }),
...(typeof store === 'undefined' ? {} : { store }),
};
const compactionResult = await session.runCompaction(compactionArgs);
if (!compactionResult) {
return;
}
const usage = compactionResult.usage;
state._context.usage.add(new Usage({
requests: 1,
inputTokens: usage.inputTokens,
outputTokens: usage.outputTokens,
totalTokens: usage.totalTokens,
inputTokensDetails: usage.inputTokensDetails,
outputTokensDetails: usage.outputTokensDetails,
requestUsageEntries: [usage],
}));
}
function buildItemFrequencyMap(items) {
const counts = new Map();
for (const item of items) {
const key = getAgentInputItemKey(item);
counts.set(key, (counts.get(key) ?? 0) + 1);
}
return counts;
}
function decrementCount(map, key) {
const remaining = (map.get(key) ?? 0) - 1;
if (remaining <= 0) {
map.delete(key);
}
else {
map.set(key, remaining);
}
}
//# sourceMappingURL=sessionPersistence.mjs.map