@openai/agents-core
Version:
The OpenAI Agents SDK is a lightweight yet powerful framework for building multi-agent workflows.
210 lines • 9.64 kB
JavaScript
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