UNPKG

@openai/agents-core

Version:

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

309 lines 12.9 kB
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