UNPKG

@openai/agents-core

Version:

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

490 lines • 21.8 kB
"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