UNPKG

@openai/agents-core

Version:

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

494 lines • 18.3 kB
import { UserError } from "../errors.mjs"; import { getToolSearchProviderCallId } from "../tooling.mjs"; import * as protocol from "../types/protocol.mjs"; import { serializeBinary } from "../utils/binary.mjs"; import { getToolResultCorrelationForCall, getToolResultCorrelationForResult, getToolResultCorrelationKey, getSimpleToolResultTypeForCall, isSimpleToolResultType, } from "./toolResultCorrelation.mjs"; export class CompactionItemValidationError extends UserError { } // Normalizes user-provided input into the structure the model expects. Strings become user messages, // arrays are kept as-is so downstream loops can treat both scenarios uniformly. export function toAgentInputList(originalInput) { if (typeof originalInput === 'string') { return [{ type: 'message', role: 'user', content: originalInput }]; } return [...originalInput]; } export function assertValidCompactionItems(items) { for (const item of items) { if (item.type === 'compaction' && !protocol.CompactionItem.safeParse(item).success) { throw new CompactionItemValidationError('Compaction item missing encrypted_content'); } } } export function getAgentInputItemKey(item) { return JSON.stringify(item, agentInputSerializationReplacer); } export function buildAgentInputPool(items) { const pool = new Map(); for (const item of items) { const key = getAgentInputItemKey(item); const existing = pool.get(key); if (existing) { existing.push(item); } else { pool.set(key, [item]); } } return pool; } export function takeAgentInputFromPool(pool, key) { const candidates = pool.get(key); if (!candidates || candidates.length === 0) { return undefined; } const [first] = candidates; candidates.shift(); if (candidates.length === 0) { pool.delete(key); } return first; } export function removeAgentInputFromPool(pool, item) { const key = getAgentInputItemKey(item); const candidates = pool.get(key); if (!candidates || candidates.length === 0) { return false; } const index = candidates.findIndex((candidate) => candidate === item); if (index === -1) { return false; } candidates.splice(index, 1); if (candidates.length === 0) { pool.delete(key); } return true; } function getAgentInputItemDeduplicationKey(item) { if (!item || typeof item !== 'object') { return undefined; } const candidate = item; const itemType = candidate.type; if (typeof candidate.role === 'string' || itemType === 'message') { return undefined; } if (typeof itemType !== 'string') { return undefined; } if (itemType === 'tool_search_call' || itemType === 'tool_search_output') { const callId = getToolSearchProviderCallId(item); if (callId) { return JSON.stringify(['call', itemType, callId]); } } const callCorrelation = getToolResultCorrelationForCall(item); if (callCorrelation && callCorrelation.id.length > 0) { return JSON.stringify([ 'call', itemType, getToolResultCorrelationKey(callCorrelation), ]); } const resultCorrelation = getToolResultCorrelationForResult(item); if (resultCorrelation && resultCorrelation.id.length > 0) { return JSON.stringify([ 'result', itemType, getToolResultCorrelationKey(resultCorrelation), ]); } if (typeof candidate.id === 'string' && candidate.id.length > 0) { return JSON.stringify(['item', itemType, candidate.id]); } return undefined; } function isCausalPrecursorItem(item) { if (!item || typeof item !== 'object') { return false; } return (item.type === 'tool_search_call' || (item.type === 'hosted_tool_call' && getToolResultCorrelationForResult(item) === undefined) || item.type === 'compaction' || item.type === 'reasoning' || getToolResultCorrelationForCall(item) !== undefined); } /** * Deduplicates provider-identified items while preserving causal ordering. * * The latest payload wins. Calls, compaction, reasoning, and approval requests stay at their * earliest occurrence so they cannot move behind a required follower; other identified items * stay at their latest occurrence so stale outputs are not moved earlier. Items without stable * provider identity, including ordinary messages, remain untouched. */ export function deduplicateAgentInputItemsPreferringLatest(items) { const latestByKey = new Map(); const anchorIndexByKey = new Map(); for (const [index, item] of items.entries()) { const key = getAgentInputItemDeduplicationKey(item); if (!key) { continue; } latestByKey.set(key, item); if (!anchorIndexByKey.has(key) || !isCausalPrecursorItem(item)) { anchorIndexByKey.set(key, index); } } const deduplicated = []; for (const [index, item] of items.entries()) { const key = getAgentInputItemDeduplicationKey(item); if (!key) { deduplicated.push(item); } else if (anchorIndexByKey.get(key) === index) { deduplicated.push(latestByKey.get(key)); } } return deduplicated; } export function agentInputSerializationReplacer(_key, value) { const serialized = serializeBinary(value); if (serialized) { return serialized; } return value; } // Keep each model-input normalization stable for one generated item so identity-preserving // filters can reuse caches across turns without mutating the provider item. const normalizedOutputItemByRunItem = new WeakMap(); export function invalidateOutputItemNormalization(items) { for (const item of items) { normalizedOutputItemByRunItem.delete(item); } } function shouldOmitReasoningItemIds(reasoningItemIdPolicy) { return reasoningItemIdPolicy === 'omit'; } export function stripReasoningItemIdForPolicy(item, reasoningItemIdPolicy) { if (!shouldOmitReasoningItemIds(reasoningItemIdPolicy) || !item || typeof item !== 'object' || item.type !== 'reasoning' || !('id' in item)) { return item; } const { id: _id, ...withoutId } = item; return withoutId; } // Extracts model-ready output items from run items, excluding approval placeholders. export function extractOutputItemsFromRunItems(items, reasoningItemIdPolicy) { return items .filter((item) => item.type !== 'tool_approval_item') .map((item) => { const normalizationKey = item.type === 'reasoning_item' && shouldOmitReasoningItemIds(reasoningItemIdPolicy) ? 'omit' : 'preserve'; const cached = normalizedOutputItemByRunItem.get(item)?.[normalizationKey]; if (cached) { return cached; } const withoutOutputOnlyMetadata = stripOutputOnlyCreatedBy(item.rawItem); const withoutNullStatusItem = withoutNullStatus(withoutOutputOnlyMetadata); const normalizedItem = normalizationKey === 'omit' ? stripReasoningItemIdForPolicy(withoutNullStatusItem, reasoningItemIdPolicy) : withoutNullStatusItem; const cachedByPolicy = normalizedOutputItemByRunItem.get(item) ?? {}; cachedByPolicy[normalizationKey] = normalizedItem; normalizedOutputItemByRunItem.set(item, cachedByPolicy); return normalizedItem; }); } function stripOutputOnlyCreatedBy(item) { if (!item || typeof item !== 'object') { return item; } const candidate = item; const hasTopLevelCreatedBy = Object.prototype.hasOwnProperty.call(candidate, 'created_by'); const output = candidate.output; const hasShellOutputCreatedBy = candidate.type === 'shell_call_output' && Array.isArray(output) && output.some((entry) => entry !== null && typeof entry === 'object' && Object.prototype.hasOwnProperty.call(entry, 'created_by')); if (!hasTopLevelCreatedBy && !hasShellOutputCreatedBy) { return item; } const { created_by: _createdBy, ...withoutCreatedBy } = candidate; const normalized = withoutCreatedBy; if (hasShellOutputCreatedBy) { normalized.output = output.map((entry) => { if (entry === null || typeof entry !== 'object' || !Object.prototype.hasOwnProperty.call(entry, 'created_by')) { return entry; } const { created_by: _chunkCreatedBy, ...withoutChunkCreatedBy } = entry; return withoutChunkCreatedBy; }); } return normalized; } function withoutNullStatus(item) { if (!item || typeof item !== 'object' || !('status' in item) || item.status !== null) { return item; } const { status: _status, ...withoutStatus } = item; return withoutStatus; } function collectCompletedCallIdsByResultType(items) { const completed = new Map(); for (const item of items) { if (!item || typeof item !== 'object') { continue; } const type = item.type; const callId = item.callId; if (typeof type !== 'string' || typeof callId !== 'string') { continue; } if (!isSimpleToolResultType(type)) { continue; } const existing = completed.get(type); if (existing) { existing.add(callId); } else { completed.set(type, new Set([callId])); } } return completed; } function collectProgramCallIds(items) { const callIds = new Set(); for (const item of items) { if (!item || typeof item !== 'object' || item.type !== 'program') { continue; } if (typeof item.callId === 'string') { callIds.add(item.callId); } } return callIds; } function isPendingHostedShellCall(item) { if (!item || typeof item !== 'object' || item.type !== 'shell_call') { return false; } const status = item.status; return status === undefined || status === 'in_progress'; } function getProgramCallerId(item) { if (!item || typeof item !== 'object' || !('caller' in item)) { return undefined; } const caller = item.caller; if (!caller || typeof caller !== 'object') { return undefined; } const candidate = caller; return candidate.type === 'program' && typeof candidate.callerId === 'string' ? candidate.callerId : undefined; } function getProgramOwnedCorrelationKey(programCallId, correlation) { return correlation ? JSON.stringify([programCallId, getToolResultCorrelationKey(correlation)]) : undefined; } function collectProgramOwnedCallKeys(items) { const callKeys = new Set(); for (const item of items) { const programCallId = getProgramCallerId(item); if (!programCallId) { continue; } const key = getProgramOwnedCorrelationKey(programCallId, getToolResultCorrelationForCall(item)); if (key) { callKeys.add(key); } } return callKeys; } function hasRetainedProgramOwnedItem(items, programCallId, pruningIndexes) { const retainedCallKeys = new Set(); const retainedResultKeys = new Set(); for (const [index, item] of items.entries()) { if (getProgramCallerId(item) !== programCallId) { continue; } const result = getToolResultCorrelationForResult(item); if (!(pruningIndexes?.has(index) ?? false) && (isPendingHostedShellCall(item) || (item && typeof item === 'object' && item.type === 'hosted_tool_call' && !result))) { return true; } const call = getToolResultCorrelationForCall(item); if (call) { retainedCallKeys.add(getToolResultCorrelationKey(call)); } if (result) { retainedResultKeys.add(getToolResultCorrelationKey(result)); } } return [...retainedCallKeys].some((key) => retainedResultKeys.has(key)); } export function dropOrphanToolCalls(items, options) { const pruningIndexes = options?.pruningIndexes; const completedByResultType = collectCompletedCallIdsByResultType(items); const programCallIds = collectProgramCallIds(items); const programOwnedCallKeys = collectProgramOwnedCallKeys(items); const droppedIndexes = new Set(); const activeProgramCallIds = new Set(); const orphanProgramCallIds = new Set(); for (const [index, item] of items.entries()) { if (pruningIndexes && !pruningIndexes.has(index)) { continue; } if (!item || typeof item !== 'object' || item.type !== 'program') { continue; } if (completedByResultType.get('program_output')?.has(item.callId) ?? false) { continue; } const hasRetainedOwnedItem = hasRetainedProgramOwnedItem(items, item.callId, pruningIndexes); if (hasRetainedOwnedItem) { activeProgramCallIds.add(item.callId); } else { orphanProgramCallIds.add(item.callId); } } const filtered = items.filter((item, index) => { if (!item || typeof item !== 'object') { return true; } const programCallerId = getProgramCallerId(item); if (programCallerId) { if (!programCallIds.has(programCallerId) || orphanProgramCallIds.has(programCallerId)) { droppedIndexes.add(index); return false; } const resultKey = getProgramOwnedCorrelationKey(programCallerId, getToolResultCorrelationForResult(item)); if (resultKey && !programOwnedCallKeys.has(resultKey)) { droppedIndexes.add(index); return false; } } if (pruningIndexes && !pruningIndexes.has(index)) { return true; } const type = item.type; const callId = item.callId; if (typeof type !== 'string' || typeof callId !== 'string') { return true; } if (type === 'program_output' && !programCallIds.has(callId)) { droppedIndexes.add(index); return false; } const resultType = getSimpleToolResultTypeForCall(type); if (!resultType) { return true; } if (isPendingHostedShellCall(item)) { return true; } if (type === 'program' && activeProgramCallIds.has(callId)) { return true; } if (completedByResultType.get(resultType)?.has(callId) ?? false) { return true; } droppedIndexes.add(index); return false; }); if (droppedIndexes.size === 0) { return filtered; } return dropReasoningItemsPrecedingDroppedCalls(items, droppedIndexes, pruningIndexes); } function dropReasoningItemsPrecedingDroppedCalls(items, droppedIndexes, pruningIndexes) { const dropReasoning = new Set(); for (let index = items.length - 1; index >= 0; index -= 1) { if (pruningIndexes && !pruningIndexes.has(index)) { continue; } const item = items[index]; if (!item || typeof item !== 'object' || item.type !== 'reasoning' || droppedIndexes.has(index)) { continue; } for (let nextIndex = index + 1; nextIndex < items.length; nextIndex += 1) { if (dropReasoning.has(nextIndex)) { continue; } const nextItem = items[nextIndex]; if (nextItem && typeof nextItem === 'object' && nextItem.type === 'reasoning') { continue; } if (droppedIndexes.has(nextIndex)) { dropReasoning.add(index); } break; } } const excluded = new Set([...droppedIndexes, ...dropReasoning]); return items.filter((_item, index) => !excluded.has(index)); } export function prepareModelInputItems(originalInput, generatedItems, reasoningItemIdPolicy) { const callerItems = toAgentInputList(originalInput); const preparedGeneratedItems = getContinuationOutputItems(generatedItems, reasoningItemIdPolicy); return trimToLatestCompaction([...callerItems, ...preparedGeneratedItems]); } function getContinuationOutputItems(generatedItems, reasoningItemIdPolicy) { const generatedOutputItems = trimToLatestCompaction(extractOutputItemsFromRunItems(generatedItems, reasoningItemIdPolicy)); return dropOrphanToolCalls(generatedOutputItems); } /** * Extracts generated output without including input admitted while resuming a run. */ export function getRunOutput(generatedItems, reasoningItemIdPolicy) { return getContinuationOutputItems(generatedItems.filter((item) => item.type !== 'input_item'), reasoningItemIdPolicy); } /** * Constructs the model input array for the current turn by combining the original turn input with * any new run items (excluding tool approval placeholders). This helps ensure that repeated calls * to the Responses API only send newly generated content. * * See: https://platform.openai.com/docs/guides/conversation-state?api-mode=responses. */ export function getTurnInput(originalInput, generatedItems, reasoningItemIdPolicy) { const outputItems = getContinuationOutputItems(generatedItems, reasoningItemIdPolicy); return trimToLatestCompaction([ ...toAgentInputList(originalInput), ...outputItems, ]); } export function trimToLatestCompaction(items) { for (let index = items.length - 1; index >= 0; index -= 1) { const item = items[index]; const rawItem = item && 'rawItem' in item ? item.rawItem : item; if (rawItem?.type === 'compaction') { return items.slice(index); } } return items; } //# sourceMappingURL=items.mjs.map