UNPKG

@openai/agents-core

Version:

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

210 lines • 9.64 kB
import { MaxTurnsExceededError } from "../errors.mjs"; import { RunHandoffCallItem, RunHandoffOutputItem, RunInputItem, } from "../items.mjs"; import logger from "../logger.mjs"; import { buildInputGuardrailDefinitions, runInputGuardrails, splitInputGuardrails, } from "./guardrails.mjs"; import { prepareModelInputItems } from "./items.mjs"; import { ensureAgentSpan } from "./tracing.mjs"; import { getToolCallOutputItem } from "./toolExecution.mjs"; const preparedInputIdentityByRunState = new WeakMap(); function getPreparedInputIdentityCache(state) { const cached = preparedInputIdentityByRunState.get(state); if (cached) { return cached; } const created = { pendingInputItems: new WeakMap() }; preparedInputIdentityByRunState.set(state, created); return created; } function getPreparedOriginalInput(state, input) { if (typeof input !== 'string') { return input; } const cache = getPreparedInputIdentityCache(state); if (cache.normalizedStringInput?.source === input) { return cache.normalizedStringInput.items; } const normalized = prepareModelInputItems(input, []); cache.normalizedStringInput = { source: input, items: normalized }; return normalized; } function getPreparedPendingInputItem(state, item, occurrenceIndex) { if (!item || typeof item !== 'object') { return new RunInputItem(structuredClone(item), state._currentAgent); } const cache = getPreparedInputIdentityCache(state); const cached = cache.pendingInputItems.get(item)?.[occurrenceIndex]; if (cached) { cached.agent = state._currentAgent; return cached; } const prepared = new RunInputItem(structuredClone(item), state._currentAgent); const preparedOccurrences = cache.pendingInputItems.get(item) ?? []; preparedOccurrences[occurrenceIndex] = prepared; cache.pendingInputItems.set(item, preparedOccurrences); if (prepared.rawItem && typeof prepared.rawItem === 'object') { cache.pendingInputItems.set(prepared.rawItem, [prepared]); } return prepared; } export async function prepareTurn(options) { const { state, input, generatedItems, isResumedState, preserveTurnPersistenceOnResume, continuingInterruptedTurn, serverConversationTracker, inputGuardrailDefs, guardrailHandlers, emitAgentStart, onAgentSpanReady, agentSpanParent, } = options; const { isResumingFromInterruption } = beginTurn(state, { isResumedState, preserveTurnPersistenceOnResume, continuingInterruptedTurn, }); if (state._maxTurns !== null && state._currentTurn > state._maxTurns) { state._currentAgentSpan?.setError({ message: 'Max turns exceeded', data: { max_turns: state._maxTurns }, }); throw new MaxTurnsExceededError(`Max turns (${state._maxTurns}) exceeded`, state); } logger.debug(`Running agent ${state._currentAgent.name} (turn ${state._currentTurn})`); state.setCurrentAgentSpan(ensureAgentSpan({ agent: state._currentAgent, handoffs: [], tools: [], currentSpan: state._currentAgentSpan, parent: agentSpanParent, })); onAgentSpanReady?.(state._currentTurn, state._currentAgent.name); const pendingInputSourceItems = [...state._pendingInput]; await runInputGuardrailsForTurn(state, inputGuardrailDefs, isResumingFromInterruption, guardrailHandlers); const pendingInputOccurrences = new WeakMap(); const pendingInputItems = pendingInputSourceItems.map((item) => { const occurrenceIndex = item && typeof item === 'object' ? (pendingInputOccurrences.get(item) ?? 0) : 0; if (item && typeof item === 'object') { pendingInputOccurrences.set(item, occurrenceIndex + 1); } return getPreparedPendingInputItem(state, item, occurrenceIndex); }); const generatedItemsForTurn = generatedItems.concat(pendingInputItems); const preparedOriginalInput = getPreparedOriginalInput(state, input); const turnInput = serverConversationTracker ? serverConversationTracker.prepareInput(preparedOriginalInput, generatedItemsForTurn, getManagedConversationSupplementalItems(state), pendingInputItems) : prepareModelInputItems(preparedOriginalInput, generatedItemsForTurn, state._reasoningItemIdPolicy); if (state._noActiveAgentRun) { state._currentAgent.emit('agent_start', state._context, state._currentAgent, turnInput); emitAgentStart?.(state._context, state._currentAgent, turnInput); } return { turnInput, pendingInputItems, pendingInputSourceItems, }; } const IGNORED_HANDOFF_OUTPUT_MESSAGE = 'Multiple handoffs detected, ignoring this one.'; const managedConversationSupplementalItemsCache = new WeakMap(); export function getManagedConversationSupplementalItems(state) { const processedResponse = state._lastProcessedResponse; if (!processedResponse?.handoffs.length) { return []; } const { handoffs } = processedResponse; const selected = processedResponse.newItems.find((item) => item instanceof RunHandoffCallItem); if (selected && handoffs.length === 1) { return []; } const completions = state._completedToolInvocationEvidence.get(selected?.agent ?? state._currentAgent); const primaryId = handoffs[0].toolCall.callId; const recoveredOutput = (callId) => { const pair = completions?.get(callId)?.items; if (pair?.[0] instanceof RunHandoffCallItem && pair[1].type === 'tool_call_output_item' && pair[1].rawItem.type === 'function_call_result' && state._generatedItems.includes(pair[1])) { return pair[1].rawItem; } }; const primaryRecovery = recoveredOutput(primaryId); // A filter may have removed the successful transfer from local history. if (!primaryRecovery && (handoffs.length === 1 || !state._generatedItems.some((item) => item instanceof RunHandoffOutputItem && item.rawItem.callId === primaryId))) { return []; } const cached = managedConversationSupplementalItemsCache.get(processedResponse); if (cached) return cached; // The selected result travels with normal history. An exact replay has no new call // item, so send a fresh copy of its previously committed result for this response. const acknowledged = new Set(); if (selected || !primaryRecovery) acknowledged.add(primaryId); const items = []; for (const { toolCall } of handoffs) { if (acknowledged.has(toolCall.callId)) continue; acknowledged.add(toolCall.callId); const previous = recoveredOutput(toolCall.callId); items.push(previous ? { ...previous } : getToolCallOutputItem(toolCall, IGNORED_HANDOFF_OUTPUT_MESSAGE)); } managedConversationSupplementalItemsCache.set(processedResponse, items); return items; } async function runInputGuardrailsForTurn(state, runnerGuardrails, isResumingFromInterruption, handlers = {}) { const guardrailInput = state._pendingInput.length > 0 ? state.pendingInput : undefined; if (guardrailInput === undefined && (state._currentTurn !== 1 || isResumingFromInterruption)) { return; } const guardrailDefs = buildInputGuardrailDefinitions(state, runnerGuardrails); const guardrails = splitInputGuardrails(guardrailDefs); if (guardrailInput === undefined) { state._initialInputGuardrailsCompleted = false; } const completeInitialInputGuardrails = () => { if (guardrailInput === undefined) { state._initialInputGuardrailsCompleted = true; } }; if (guardrails.blocking.length > 0) { await runInputGuardrails(state, guardrails.blocking, { input: guardrailInput, }); } if (guardrails.parallel.length > 0) { const parallelGuardrailPromise = runInputGuardrails(state, guardrails.parallel, { input: guardrailInput, onErrorObserved: handlers.onParallelError, }) .then((results) => { completeInitialInputGuardrails(); return results; }) .catch(() => []); handlers.onParallelPromise?.(parallelGuardrailPromise); } else { completeInitialInputGuardrails(); } } function beginTurn(state, options) { const isResumingFromInterruption = options.isResumedState && options.continuingInterruptedTurn; const resumingTurnInProgress = options.isResumedState && state._currentTurnInProgress === true; // Resolving approvals does not consume a turn, but the next model call does. // Only resume an already-counted turn when its model work is still in progress. if (!resumingTurnInProgress) { state._currentTurn++; // Resumed approval work may have persisted items after the run-entry snapshot. // Keep those offsets even when the checkpoint initially had none. if (!options.isResumedState || (!options.preserveTurnPersistenceOnResume && state._currentTurnPersistedItemCount === 0)) { state.resetTurnPersistence(); } else if (state._currentTurnPersistedItemCount > state._generatedItems.length) { // Reset if a stale count would skip items in subsequent turns. state.resetTurnPersistence(); } } state._currentTurnInProgress = true; return { isResumingFromInterruption }; } //# sourceMappingURL=turnPreparation.mjs.map