@openai/agents-core
Version:
The OpenAI Agents SDK is a lightweight yet powerful framework for building multi-agent workflows.
490 lines • 21.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ServerConversationTracker = exports.getTurnInput = void 0;
exports.getServerConversationOwner = getServerConversationOwner;
exports.applyCallModelInputFilter = applyCallModelInputFilter;
const errors_1 = require("../errors.js");
const items_1 = require("../items.js");
const context_1 = require("../tracing/context.js");
const items_2 = require("./items.js");
const toolResultCorrelation_1 = require("./toolResultCorrelation.js");
var items_3 = require("./items.js");
Object.defineProperty(exports, "getTurnInput", { enumerable: true, get: function () { return items_3.getTurnInput; } });
/**
* Resolves the single server-side continuation owner for an accepted response.
*/
function getServerConversationOwner(conversationId, previousResponseId) {
if (typeof conversationId === 'string' &&
conversationId.length > 0 &&
previousResponseId === undefined) {
return { type: 'conversation', id: conversationId };
}
if (typeof previousResponseId === 'string' &&
previousResponseId.length > 0 &&
conversationId === undefined) {
return { type: 'previous_response', id: previousResponseId };
}
return undefined;
}
/**
* 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.
*/
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 array passed to filters and the original inputs.
const cloneMap = new WeakMap();
// Identity-preserving filters may opt into stable item identities. Other filters still
// receive deep copies so their mutations cannot affect the cached turn state.
const preserveInputIdentity = callModelInputFilter?.preserveInputIdentity === true;
const clonedBaseInput = preserveInputIdentity
? [...inputItems]
: cloneInputItems(inputItems, cloneMap);
if (preserveInputIdentity) {
for (const item of inputItems) {
if (item && typeof item === 'object') {
cloneMap.set(item, item);
}
}
}
const base = {
input: clonedBaseInput,
instructions: systemInstructions,
};
if (!callModelInputFilter) {
const normalizedInput = (0, items_2.deduplicateAgentInputItemsPreferringLatest)(clonedBaseInput);
return {
modelInput: { ...base, input: normalizedInput },
sourceItems: normalizedInput.map((item) => cloneMap.get(item) ?? item),
persistedItems: cloneInputItems(normalizedInput),
sourceMatchKinds: normalizedInput.map(() => 'identity'),
filterApplied: false,
preserveInputIdentity,
};
}
try {
const result = await callModelInputFilter({
modelData: base,
agent,
context: context.context,
});
if (!result || !Array.isArray(result.input)) {
throw new errors_1.UserError('callModelInputFilter must return a ModelInputData object with an input array.');
}
const normalizedBaseSources = (0, items_2.deduplicateAgentInputItemsPreferringLatest)(clonedBaseInput).map((item) => cloneMap.get(item) ?? item);
const originalPool = (0, items_2.buildAgentInputPool)(normalizedBaseSources);
const fallbackOriginals = normalizedBaseSources.filter((item) => item && typeof item === 'object');
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) {
(0, items_2.removeAgentInputFromPool)(originalPool, next);
}
return next;
};
const normalizedInput = (0, items_2.deduplicateAgentInputItemsPreferringLatest)(result.input);
const availableExactOccurrences = new WeakMap();
for (const item of clonedBaseInput) {
if (item && typeof item === 'object') {
availableExactOccurrences.set(item, (availableExactOccurrences.get(item) ?? 0) + 1);
}
}
const consumedExactOccurrences = new WeakMap();
const injectedExactCloneIndexes = new Set();
// Preserve a pointer to the original object backing each filtered clone so downstream
// trackers can keep their bookkeeping consistent even after redaction.
const sourceItems = normalizedInput.map((item, index) => {
if (!item || typeof item !== 'object') {
return undefined;
}
const original = cloneMap.get(item);
if (!original) {
return undefined;
}
const consumedOccurrences = consumedExactOccurrences.get(item) ?? 0;
if (consumedOccurrences >=
(availableExactOccurrences.get(item) ?? 0)) {
injectedExactCloneIndexes.add(index);
return undefined;
}
consumedExactOccurrences.set(item, consumedOccurrences + 1);
removeFromFallback(original);
(0, items_2.removeAgentInputFromPool)(originalPool, original);
return original;
});
const sourceMatchKinds = sourceItems.map((source) => source === undefined ? 'injected' : 'identity');
// Reserve all exact clone matches before considering equal-content items. A prepended clone
// that happens to equal a later unchanged item must remain an injected item.
for (let index = 0; index < sourceItems.length; index++) {
if (sourceItems[index] !== undefined) {
continue;
}
if (injectedExactCloneIndexes.has(index)) {
continue;
}
const item = normalizedInput[index];
if (!item || typeof item !== 'object') {
continue;
}
const key = (0, items_2.getAgentInputItemKey)(item);
const matchedByContent = (0, items_2.takeAgentInputFromPool)(originalPool, key);
if (matchedByContent) {
removeFromFallback(matchedByContent);
sourceItems[index] = matchedByContent;
sourceMatchKinds[index] = 'content';
}
}
// Assign replacement fallbacks only after every exact and content match has been reserved.
for (let index = 0; index < sourceItems.length; index++) {
if (sourceItems[index] !== undefined) {
continue;
}
if (injectedExactCloneIndexes.has(index)) {
continue;
}
const item = normalizedInput[index];
if (!item || typeof item !== 'object') {
continue;
}
const fallback = takeFallbackOriginal();
if (fallback) {
sourceItems[index] = fallback;
sourceMatchKinds[index] = 'fallback';
}
}
const clonedFilteredInput = cloneInputItems(normalizedInput);
return {
modelInput: {
input: clonedFilteredInput,
instructions: typeof result.instructions === 'undefined'
? systemInstructions
: result.instructions,
},
sourceItems,
persistedItems: clonedFilteredInput.map((item) => structuredClone(item)),
sourceMatchKinds,
filterApplied: true,
preserveInputIdentity,
};
}
catch (error) {
(0, context_1.addErrorToCurrentSpan)({
message: 'Error in callModelInputFilter',
data: { error: String(error) },
});
throw error;
}
}
function getResumeResponseBoundary(modelResponses, conversationId) {
let latestResponseId;
let latestResponseIdIndex = -1;
for (const [responseIndex, response] of modelResponses.entries()) {
if (response.responseId) {
latestResponseId = response.responseId;
latestResponseIdIndex = responseIndex;
}
}
// A conversation advances with every response. A previous-response chain only advances when the
// response has an ID that the next request can reference.
return {
latestResponseId,
acknowledgingResponseIndex: conversationId
? modelResponses.length - 1
: latestResponseIdIndex,
};
}
function incrementCorrelationCount(counts, correlation) {
const key = (0, toolResultCorrelation_1.getToolResultCorrelationKey)(correlation);
counts.set(key, (counts.get(key) ?? 0) + 1);
}
function consumeCorrelationCount(counts, correlation) {
const key = (0, toolResultCorrelation_1.getToolResultCorrelationKey)(correlation);
const remaining = counts.get(key) ?? 0;
if (remaining === 0) {
return false;
}
counts.set(key, remaining - 1);
return true;
}
// SDK discovery ownership does not change the item already stored by the provider.
function getServerItemKey(item) {
if (item.type === 'tool_search_output') {
const { toolSearchAgentName: _owner, toolSearchMcpToolNames: _provenance, ...providerItem } = item;
return (0, items_2.getAgentInputItemKey)(providerItem);
}
return (0, items_2.getAgentInputItemKey)(item);
}
/**
* 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.
*/
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 = (0, items_2.toAgentInputList)(originalInput);
const hasResponses = modelResponses.length > 0;
const serverItemKeys = new Set();
const acknowledgedToolCallCounts = new Map();
const { latestResponseId, acknowledgingResponseIndex } = getResumeResponseBoundary(modelResponses, this.conversationId);
for (const [responseIndex, response] of modelResponses.entries()) {
if (responseIndex < acknowledgingResponseIndex) {
const unresolvedCorrelations = (0, toolResultCorrelation_1.getUnresolvedToolResultCorrelationsForResponse)(response.output);
for (const correlation of unresolvedCorrelations) {
incrementCorrelationCount(acknowledgedToolCallCounts, correlation);
}
}
for (const item of response.output) {
if (item && typeof item === 'object') {
this.serverItems.add(item);
serverItemKeys.add(getServerItemKey(item));
}
}
}
if (hasResponses) {
for (const item of originalItems) {
if (item && typeof item === 'object') {
this.sentItems.add(item);
}
}
this.sentInitialInput = true;
this.remainingInitialInput = null;
}
// Mirror live tracking by preserving the last response that actually carried an id.
if (!this.conversationId && latestResponseId) {
this.previousResponseId = latestResponseId;
}
if (hasResponses) {
for (const item of generatedItems) {
const rawItem = item.rawItem;
if (!rawItem || typeof rawItem !== 'object') {
continue;
}
const rawItemKey = getServerItemKey(rawItem);
if (item instanceof items_1.RunInputItem) {
this.sentItems.add(rawItem);
continue;
}
if (this.serverItems.has(rawItem) || serverItemKeys.has(rawItemKey)) {
this.sentItems.add(rawItem);
continue;
}
const correlation = (0, toolResultCorrelation_1.getToolResultCorrelationForResult)(rawItem);
if (correlation &&
consumeCorrelationCount(acknowledgedToolCallCounts, correlation)) {
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, processedItems = []) {
if (!modelResponse) {
return;
}
for (const item of modelResponse.output) {
if (item && typeof item === 'object') {
this.serverItems.add(item);
}
}
const searchOutputs = processedItems.filter((item) => item instanceof items_1.RunToolSearchOutputItem);
if (searchOutputs.length > 0) {
const serverSearchKeys = new Set(modelResponse.output
.filter((item) => item.type === 'tool_search_output')
.map(getServerItemKey));
// A client-generated result is unsent unless the response actually contains that output.
for (const item of searchOutputs) {
if (serverSearchKeys.has(getServerItemKey(item.rawItem))) {
this.serverItems.add(item.rawItem);
}
}
}
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, supplementalGeneratedItems = [], pendingInputItems = []) {
const inputItems = [];
const generatedItemsForInput = [];
const pendingInputItemSet = new Set(pendingInputItems);
if (!this.sentInitialInput) {
const initialItems = (0, items_2.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 (!pendingInputItemSet.has(item) &&
(this.sentItems.has(rawItem) || this.serverItems.has(rawItem))) {
continue;
}
generatedItemsForInput.push(item);
}
const preparedGeneratedItems = (0, items_2.extractOutputItemsFromRunItems)(generatedItemsForInput, this.reasoningItemIdPolicy);
for (const [index, preparedItem] of preparedGeneratedItems.entries()) {
const sourceItem = generatedItemsForInput[index]?.rawItem;
this.registerPreparedItemSource(preparedItem, sourceItem);
}
inputItems.push(...preparedGeneratedItems);
const filteredSupplementalItems = filterSupplementalGeneratedItems(supplementalGeneratedItems, preparedGeneratedItems, this.sentItems, this.serverItems);
for (const item of filteredSupplementalItems) {
this.registerPreparedItemSource(item);
}
inputItems.push(...filteredSupplementalItems);
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;
}
}
}
exports.ServerConversationTracker = ServerConversationTracker;
function filterSupplementalGeneratedItems(supplementalGeneratedItems, preparedGeneratedItems, sentItems, serverItems) {
const preparedFunctionResultCallIds = new Set(preparedGeneratedItems.flatMap((item) => {
if (!item ||
typeof item !== 'object' ||
item.type !== 'function_call_result' ||
typeof item.callId !== 'string') {
return [];
}
return [item.callId];
}));
return supplementalGeneratedItems.filter((item) => {
if (!item || typeof item !== 'object') {
return true;
}
if (sentItems.has(item) || serverItems.has(item)) {
return false;
}
if (item.type !== 'function_call_result' ||
typeof item.callId !== 'string') {
return true;
}
return !preparedFunctionResultCallIds.has(item.callId);
});
}
//# sourceMappingURL=conversation.js.map