@openai/agents-core
Version:
The OpenAI Agents SDK is a lightweight yet powerful framework for building multi-agent workflows.
309 lines • 12.9 kB
JavaScript
import { UserError } from "../errors.mjs";
import { addErrorToCurrentSpan } from "../tracing/context.mjs";
import { buildAgentInputPool, extractOutputItemsFromRunItems, getAgentInputItemKey, removeAgentInputFromPool, takeAgentInputFromPool, toAgentInputList, } from "./items.mjs";
export { getTurnInput } from "./items.mjs";
/**
* Applies the optional callModelInputFilter and returns the filtered input alongside the original
* items so downstream tracking and session persistence stay in sync with what the model saw.
*/
export async function applyCallModelInputFilter(agent, callModelInputFilter, context, inputItems, systemInstructions) {
const cloneInputItems = (items, map) => items.map((item) => {
const cloned = structuredClone(item);
if (map && cloned && typeof cloned === 'object') {
map.set(cloned, item);
}
return cloned;
});
// Record the relationship between the cloned array passed to filters and the original inputs.
const cloneMap = new WeakMap();
const originalPool = buildAgentInputPool(inputItems);
const fallbackOriginals = [];
// Track any original object inputs so filtered replacements can still mark them as delivered.
for (const item of inputItems) {
if (item && typeof item === 'object') {
fallbackOriginals.push(item);
}
}
const removeFromFallback = (candidate) => {
if (!candidate || typeof candidate !== 'object') {
return;
}
const index = fallbackOriginals.findIndex((original) => original === candidate);
if (index !== -1) {
fallbackOriginals.splice(index, 1);
}
};
const takeFallbackOriginal = () => {
const next = fallbackOriginals.shift();
if (next) {
removeAgentInputFromPool(originalPool, next);
}
return next;
};
// Always create a deep copy so downstream mutations inside filters cannot affect
// the cached turn state.
const clonedBaseInput = cloneInputItems(inputItems, cloneMap);
const base = {
input: clonedBaseInput,
instructions: systemInstructions,
};
if (!callModelInputFilter) {
return {
modelInput: base,
sourceItems: [...inputItems],
persistedItems: [],
filterApplied: false,
};
}
try {
const result = await callModelInputFilter({
modelData: base,
agent,
context: context.context,
});
if (!result || !Array.isArray(result.input)) {
throw new UserError('callModelInputFilter must return a ModelInputData object with an input array.');
}
// Preserve a pointer to the original object backing each filtered clone so downstream
// trackers can keep their bookkeeping consistent even after redaction.
const sourceItems = result.input.map((item) => {
if (!item || typeof item !== 'object') {
return undefined;
}
const original = cloneMap.get(item);
if (original) {
removeFromFallback(original);
removeAgentInputFromPool(originalPool, original);
return original;
}
const key = getAgentInputItemKey(item);
const matchedByContent = takeAgentInputFromPool(originalPool, key);
if (matchedByContent) {
removeFromFallback(matchedByContent);
return matchedByContent;
}
const fallback = takeFallbackOriginal();
if (fallback) {
return fallback;
}
return undefined;
});
const clonedFilteredInput = cloneInputItems(result.input);
return {
modelInput: {
input: clonedFilteredInput,
instructions: typeof result.instructions === 'undefined'
? systemInstructions
: result.instructions,
},
sourceItems,
persistedItems: clonedFilteredInput.map((item) => structuredClone(item)),
filterApplied: true,
};
}
catch (error) {
addErrorToCurrentSpan({
message: 'Error in callModelInputFilter',
data: { error: String(error) },
});
throw error;
}
}
/**
* Tracks which items have already been sent to or received from the Responses API when the caller
* supplies `conversationId`/`previousResponseId`. This ensures we only send the delta each turn.
*/
export class ServerConversationTracker {
conversationId;
previousResponseId;
reasoningItemIdPolicy;
// Using this flag because WeakSet does not provide a way to check its size.
sentInitialInput = false;
// The items already sent to the model; using WeakSet for memory efficiency.
sentItems = new WeakSet();
// The items received from the server; using WeakSet for memory efficiency.
serverItems = new WeakSet();
// Tracks which prepared turn-input item originated from which source object.
preparedItemSources = new WeakMap();
// Track initial input items that have not yet been sent so they can be retried on later turns.
remainingInitialInput = null;
constructor({ conversationId, previousResponseId, reasoningItemIdPolicy, }) {
this.conversationId = conversationId ?? undefined;
this.previousResponseId = previousResponseId ?? undefined;
this.reasoningItemIdPolicy = reasoningItemIdPolicy;
}
/**
* Pre-populates tracker caches from an existing RunState when resuming server-managed runs.
*/
primeFromState({ originalInput, generatedItems, modelResponses, }) {
if (this.sentInitialInput) {
return;
}
const originalItems = toAgentInputList(originalInput);
const hasResponses = modelResponses.length > 0;
const serverItemKeys = new Set();
for (const response of modelResponses) {
for (const item of response.output) {
if (item && typeof item === 'object') {
this.serverItems.add(item);
serverItemKeys.add(getAgentInputItemKey(item));
}
}
}
if (hasResponses) {
for (const item of originalItems) {
if (item && typeof item === 'object') {
this.sentItems.add(item);
}
}
this.sentInitialInput = true;
this.remainingInitialInput = null;
}
const latestResponse = modelResponses[modelResponses.length - 1];
if (!this.conversationId && latestResponse?.responseId) {
this.previousResponseId = latestResponse.responseId;
}
if (hasResponses) {
for (const item of generatedItems) {
const rawItem = item.rawItem;
if (!rawItem || typeof rawItem !== 'object') {
continue;
}
const rawItemKey = getAgentInputItemKey(rawItem);
if (this.serverItems.has(rawItem) || serverItemKeys.has(rawItemKey)) {
this.sentItems.add(rawItem);
}
}
}
}
/**
* Records the raw items returned by the server so future delta calculations skip them.
* Also captures the latest response identifier to chain follow-up calls when possible.
*/
trackServerItems(modelResponse) {
if (!modelResponse) {
return;
}
for (const item of modelResponse.output) {
if (item && typeof item === 'object') {
this.serverItems.add(item);
}
}
if (!this.conversationId && modelResponse.responseId) {
this.previousResponseId = modelResponse.responseId;
}
}
/**
* Returns the minimum set of items that still need to be delivered to the server for the
* current turn. This includes the original turn inputs (until acknowledged) plus any
* newly generated items that have not yet been echoed back by the API.
*/
prepareInput(originalInput, generatedItems) {
const inputItems = [];
const generatedItemsForInput = [];
if (!this.sentInitialInput) {
const initialItems = toAgentInputList(originalInput);
// Preserve the full initial payload so a filter can drop items without losing their originals.
inputItems.push(...initialItems);
for (const item of initialItems) {
this.registerPreparedItemSource(item);
}
this.remainingInitialInput = initialItems.filter((item) => Boolean(item) && typeof item === 'object');
this.sentInitialInput = true;
}
else if (this.remainingInitialInput &&
this.remainingInitialInput.length > 0) {
// Re-queue prior initial items until the tracker confirms they were delivered to the API.
inputItems.push(...this.remainingInitialInput);
for (const item of this.remainingInitialInput) {
this.registerPreparedItemSource(item);
}
}
for (const item of generatedItems) {
if (item.type === 'tool_approval_item') {
continue;
}
const rawItem = item.rawItem;
if (!rawItem || typeof rawItem !== 'object') {
continue;
}
if (this.sentItems.has(rawItem) || this.serverItems.has(rawItem)) {
continue;
}
generatedItemsForInput.push(item);
}
const preparedGeneratedItems = extractOutputItemsFromRunItems(generatedItemsForInput, this.reasoningItemIdPolicy);
for (const [index, preparedItem] of preparedGeneratedItems.entries()) {
const sourceItem = generatedItemsForInput[index]?.rawItem;
this.registerPreparedItemSource(preparedItem, sourceItem);
}
inputItems.push(...preparedGeneratedItems);
return inputItems;
}
/**
* Marks the provided originals as delivered so future turns do not resend them and any
* pending initial inputs can be dropped once the server acknowledges receipt.
*/
markInputAsSent(items, options) {
const delivered = new Set();
const dropRemainingInitialInput = options?.filterApplied ?? false;
const markFilteredItemsAsSent = options?.filterApplied && Boolean(options.allTurnItems);
this.addDeliveredItems(delivered, items);
const allTurnItems = options?.allTurnItems;
if (markFilteredItemsAsSent && allTurnItems) {
this.addDeliveredItems(delivered, allTurnItems);
}
this.updateRemainingInitialInput(delivered, Boolean(dropRemainingInitialInput));
}
addDeliveredItems(delivered, items) {
for (const item of items) {
if (!item || typeof item !== 'object') {
continue;
}
const sourceItem = this.resolvePreparedItemSource(item);
if (!sourceItem || typeof sourceItem !== 'object') {
continue;
}
if (delivered.has(sourceItem)) {
continue;
}
// Some inputs may be repeated in the filtered list; only mark unique originals once.
delivered.add(sourceItem);
this.sentItems.add(sourceItem);
}
}
registerPreparedItemSource(preparedItem, sourceItem) {
if (!preparedItem || typeof preparedItem !== 'object') {
return;
}
if (!sourceItem || typeof sourceItem !== 'object') {
this.preparedItemSources.set(preparedItem, preparedItem);
return;
}
this.preparedItemSources.set(preparedItem, sourceItem);
}
resolvePreparedItemSource(item) {
if (!item || typeof item !== 'object') {
return item;
}
return this.preparedItemSources.get(item) ?? item;
}
updateRemainingInitialInput(delivered, dropRemainingInitialInput) {
if (!this.remainingInitialInput ||
this.remainingInitialInput.length === 0 ||
delivered.size === 0) {
if (dropRemainingInitialInput && this.remainingInitialInput) {
this.remainingInitialInput = null;
}
return;
}
this.remainingInitialInput = this.remainingInitialInput.filter((item) => !delivered.has(item));
if (this.remainingInitialInput.length === 0) {
this.remainingInitialInput = null;
}
else if (dropRemainingInitialInput) {
this.remainingInitialInput = null;
}
}
}
//# sourceMappingURL=conversation.mjs.map