@openai/agents-core
Version:
The OpenAI Agents SDK is a lightweight yet powerful framework for building multi-agent workflows.
259 lines • 13.4 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.isAcceptedResponseCheckpoint = isAcceptedResponseCheckpoint;
exports.assertAcceptedResponseContinuationAuthority = assertAcceptedResponseContinuationAuthority;
exports.markAcceptedResponseProcessingStarted = markAcceptedResponseProcessingStarted;
exports.markAcceptedResponseFinalizationStarted = markAcceptedResponseFinalizationStarted;
exports.resumeAcceptedModelResponse = resumeAcceptedModelResponse;
exports.applyTurnResult = applyTurnResult;
exports.resumeInterruptedTurn = resumeInterruptedTurn;
exports.handleInterruptedOutcome = handleInterruptedOutcome;
const errors_1 = require("../errors.js");
const context_1 = require("../tracing/context.js");
const toolIdentity_1 = require("../toolIdentity.js");
const conversation_1 = require("./conversation.js");
const turnResolution_1 = require("./turnResolution.js");
function isAcceptedResponseCheckpoint(state) {
const isCheckpoint = state._currentStep?.type === 'next_step_interruption' &&
state._currentStep.data?.responseAccepted === true;
if (isCheckpoint) {
assertAcceptedResponseServerOwnership(state);
}
return isCheckpoint;
}
function assertAcceptedResponseServerOwnership(state) {
if ((0, conversation_1.getServerConversationOwner)(state._conversationId, state._previousResponseId)) {
return;
}
throw new errors_1.UserError('Accepted model response state requires exactly one server-managed conversation owner', state);
}
function assertAcceptedResponseContinuationAuthority(state, requestedConversationId, requestedPreviousResponseId) {
const currentStep = state._currentStep;
const responseAccepted = (currentStep?.type === 'next_step_interruption' &&
currentStep.data?.responseAccepted === true) ||
(currentStep?.type === 'next_step_final_output' &&
currentStep.responseAccepted === true);
if (!responseAccepted) {
return;
}
assertAcceptedResponseServerOwnership(state);
if ((requestedConversationId !== undefined &&
requestedConversationId !== state._conversationId) ||
(requestedPreviousResponseId !== undefined &&
requestedPreviousResponseId !== state._previousResponseId)) {
throw new errors_1.UserError('Accepted model response state cannot change its server-managed conversation owner', state);
}
}
function markAcceptedResponseProcessingStarted(state) {
const currentStep = state._currentStep;
if (currentStep?.type === 'next_step_interruption' &&
currentStep.data?.responseAccepted === true) {
assertAcceptedResponseServerOwnership(state);
currentStep.data.localProcessingStarted = true;
}
}
function markAcceptedResponseFinalizationStarted(state) {
const currentStep = state._currentStep;
if (currentStep?.type !== 'next_step_final_output' ||
currentStep.responseAccepted !== true) {
return;
}
assertAcceptedResponseServerOwnership(state);
if (currentStep.localFinalizationStarted === true) {
throw new errors_1.UserError('An accepted model response has unfinished local finalization and cannot be resumed safely; start a new run instead', state);
}
currentStep.localFinalizationStarted = true;
}
function hasUncommittedLocalEffects(processedResponse, suppressedToolCalls) {
return [
...processedResponse.functions.map((run) => run.toolCall),
...processedResponse.computerActions.map((run) => run.toolCall),
...processedResponse.shellActions.map((run) => run.toolCall),
...processedResponse.applyPatchActions.map((run) => run.toolCall),
...processedResponse.handoffs.map((run) => run.toolCall),
].some((toolCall) => !suppressedToolCalls.has(toolCall));
}
/**
* Continues local processing for a response that was already accepted by the model provider.
*/
async function resumeAcceptedModelResponse(options) {
const { state } = options;
if (!state._lastTurnResponse || !state._lastProcessedResponse) {
throw new errors_1.UserError('An accepted model response could not be processed; start a new run instead of retrying it', state);
}
const suppressedToolCalls = (0, turnResolution_1.preflightToolInvocations)(state._currentAgent, state, state._lastProcessedResponse);
if (state._currentStep?.type === 'next_step_interruption' &&
state._currentStep.data?.localProcessingStarted === true &&
hasUncommittedLocalEffects(state._lastProcessedResponse, suppressedToolCalls)) {
throw new errors_1.UserError('An accepted model response has unfinished local tool work and cannot be resumed safely; start a new run instead', state);
}
markAcceptedResponseProcessingStarted(state);
const turnResult = await (0, turnResolution_1.resolveTurnAfterModelResponse)(state._currentAgent, state._originalInput, state._generatedItems, state._lastTurnResponse, state._lastProcessedResponse, options.runner, state, options.toolErrorFormatter, options.agentToolParentRunConfig, undefined, options.signal, suppressedToolCalls, undefined, options.validateHandoffAgent);
applyTurnResult({
state,
turnResult,
agent: state._currentAgent,
toolsUsed: state._lastProcessedResponse.toolsUsed,
resetTurnPersistence: false,
onStepItems: options.onStepItems,
});
return turnResult;
}
function applyTurnResult(options) {
const { state, turnResult, agent, toolsUsed, resetTurnPersistence, onStepItems, } = options;
const responseAccepted = isAcceptedResponseCheckpoint(state);
onStepItems?.(turnResult);
state._commitToolInvocations(turnResult.toolInvocationCommitItems);
state._toolUseTracker.addToolUse(agent, toolsUsed);
state._originalInput = turnResult.originalInput;
state._replaceGeneratedItems(turnResult.generatedItems);
if (resetTurnPersistence &&
turnResult.nextStep.type === 'next_step_run_again') {
state.resetTurnPersistence();
}
state._currentStep =
responseAccepted && turnResult.nextStep.type === 'next_step_final_output'
? { ...turnResult.nextStep, responseAccepted: true }
: turnResult.nextStep;
state._finalOutputSource =
turnResult.nextStep.type === 'next_step_final_output'
? (turnResult.finalOutputSource ?? 'turn_resolution')
: undefined;
}
async function resumeInterruptedTurn(options) {
const { state, runner, toolErrorFormatter, agentToolParentRunConfig, signal, onStepItems, validateHandoffAgent, beforeResumedSideEffects, onHandoff, } = options;
// Validate recipient continuity before resumed-session callbacks can run.
(0, turnResolution_1.preflightToolInvocations)(state._currentAgent, state, state._lastProcessedResponse);
const approvedToolWillResume = state.getInterruptions().some((item) => {
const rawItem = item.rawItem;
if (rawItem.type === 'hosted_tool_call') {
return false;
}
const toolName = rawItem.type === 'function_call'
? (item.functionToolStateKey ??
(0, toolIdentity_1.getFunctionToolStateKeyForCall)(rawItem, item.name))
: item.name;
const callId = 'callId' in rawItem && typeof rawItem.callId === 'string'
? rawItem.callId
: 'id' in rawItem && typeof rawItem.id === 'string'
? rawItem.id
: undefined;
return (toolName !== undefined &&
callId !== undefined &&
state._context._resolveToolInvocationApproval(item.agent, toolName, rawItem) === true);
});
const pendingHandoffs = state._lastProcessedResponse?.handoffs ?? [];
const preparation = approvedToolWillResume || pendingHandoffs.length > 0
? await beforeResumedSideEffects?.()
: undefined;
let unfilteredHandoffInput;
const turnResult = await (0, turnResolution_1.resolveInterruptedTurn)(state._currentAgent, state._originalInput, state._generatedItems, state._lastTurnResponse, state._lastProcessedResponse, runner, state, toolErrorFormatter, agentToolParentRunConfig, signal, validateHandoffAgent, preparation
? (input) => {
unfilteredHandoffInput = state._captureHandoffInput(input.inputHistory, [...input.preHandoffItems, ...input.newItems]);
}
: undefined);
const resumedSideEffects = turnResult.nextStep.type === 'next_step_handoff' ||
// A handled invocation can have external effects even when every sibling approval was rejected.
turnResult.newStepItems.some((item) => {
if (item.type !== 'tool_call_output_item' ||
item.executionStatus !== 'executed') {
return false;
}
const { rawItem } = item;
return (rawItem.type === 'function_call_result' &&
pendingHandoffs.some(({ toolCall }) => toolCall.callId === rawItem.callId));
}) ||
(approvedToolWillResume &&
turnResult.generatedItems.length > state._currentTurnPersistedItemCount);
if (preparation && unfilteredHandoffInput) {
preparation.handoffInput = state._captureHandoffInput(turnResult.originalInput, turnResult.generatedItems);
const canonicalInput = state._deserializeHandoffInput(unfilteredHandoffInput);
// Streaming observes the accepted filtered step, while persistence keeps the source pairs.
onStepItems?.(turnResult);
turnResult.originalInput = canonicalInput.originalInput;
turnResult.preStepItems = [];
turnResult.newStepItems = canonicalInput.generatedItems;
turnResult.toolInvocationCommitItems = canonicalInput.generatedItems;
}
applyTurnResult({
state,
turnResult,
agent: state._currentAgent,
toolsUsed: state._lastProcessedResponse?.toolsUsed ?? [],
resetTurnPersistence: false,
onStepItems: unfilteredHandoffInput ? undefined : onStepItems,
});
if (turnResult.nextStep.type === 'next_step_run_again') {
// Approval resolution completes the previous model turn. Persist that fact
// before session writes so a retry also counts the next model request.
state._currentTurnInProgress = false;
}
if (turnResult.nextStep.type === 'next_step_handoff') {
// The transfer and tool effects have completed. Persist their continuation before
// the caller attempts the fallible resumed Session append.
state.setCurrentAgent(turnResult.nextStep.newAgent);
state._currentStep = { type: 'next_step_run_again' };
state._noActiveAgentRun = true;
state._currentTurnInProgress = false;
if (state._currentAgentSpan) {
state._currentAgentSpan.end();
(0, context_1.resetCurrentSpan)();
state.setCurrentAgentSpan(undefined);
}
if (!(0, conversation_1.getServerConversationOwner)(state._conversationId, state._previousResponseId)) {
// Local history owns the completed source-agent results. Retaining its execution
// plan would try to rehydrate source tools against the target agent on restore.
// Server-managed continuation still needs the plan for ignored handoff outputs.
state._lastProcessedResponse = undefined;
}
onHandoff?.(state._currentAgent);
}
// Map next-step outcomes to interruption flow control for the outer run loop.
// return_interruption: still waiting on approvals. rerun_turn: prepare the next model turn.
// advance_step: proceed without rerunning the same turn.
if (turnResult.nextStep.type === 'next_step_interruption') {
return {
nextStep: turnResult.nextStep,
action: 'return_interruption',
resumedSideEffects,
};
}
if (turnResult.nextStep.type === 'next_step_run_again') {
return {
nextStep: turnResult.nextStep,
action: 'rerun_turn',
resumedSideEffects,
};
}
return {
nextStep: turnResult.nextStep,
action: 'advance_step',
resumedSideEffects,
};
}
function handleInterruptedOutcome(options) {
const { state, outcome, setContinuingInterruptedTurn } = options;
switch (outcome.action) {
case 'return_interruption':
state._currentStep = outcome.nextStep;
return { shouldReturn: true, shouldContinue: false };
case 'rerun_turn':
// Preserve interruption context for preparation of the next model turn.
setContinuingInterruptedTurn(true);
state._currentStep = undefined;
return { shouldReturn: false, shouldContinue: true };
case 'advance_step':
setContinuingInterruptedTurn(false);
if (outcome.nextStep.type === 'next_step_handoff') {
// The transfer, lifecycle notification and durable continuation are already published.
return { shouldReturn: false, shouldContinue: true };
}
state._currentStep = outcome.nextStep;
return { shouldReturn: false, shouldContinue: false };
default: {
const _exhaustive = outcome.action;
throw new Error(`Unhandled interruption outcome: ${_exhaustive}`);
}
}
}
//# sourceMappingURL=runLoop.js.map