UNPKG

@openai/agents-core

Version:

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

553 lines 24 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.nextStepSchema = void 0; exports.resolveInterruptedTurn = resolveInterruptedTurn; exports.resolveTurnAfterModelResponse = resolveTurnAfterModelResponse; const zod_1 = require("zod"); const errors_1 = require("../errors.js"); const items_1 = require("../items.js"); const messages_1 = require("../utils/messages.js"); const tools_1 = require("../utils/tools.js"); const safeExecute_1 = require("../utils/safeExecute.js"); const context_1 = require("../tracing/context.js"); const steps_1 = require("./steps.js"); Object.defineProperty(exports, "nextStepSchema", { enumerable: true, get: function () { return steps_1.nextStepSchema; } }); const toolExecution_1 = require("./toolExecution.js"); const mcpApprovals_1 = require("./mcpApprovals.js"); const APPROVAL_ITEM_TYPES = [ 'function_call', 'computer_call', 'hosted_tool_call', 'shell_call', 'apply_patch_call', ]; function isHostedMcpApprovalItem(item) { return (item.rawItem.type === 'hosted_tool_call' && item.rawItem.providerData?.type === 'mcp_approval_request'); } function resolveApprovalState(item, state) { if (isHostedMcpApprovalItem(item)) { return 'pending'; } const rawItem = item.rawItem; const toolName = item.toolName ?? ('name' in rawItem && typeof rawItem.name === 'string' ? rawItem.name : undefined); const callId = 'callId' in rawItem && typeof rawItem.callId === 'string' ? rawItem.callId : 'id' in rawItem && typeof rawItem.id === 'string' ? rawItem.id : undefined; if (!toolName || !callId) { return 'pending'; } const approval = state._context.isToolApproved({ toolName, callId }); if (approval === true) { return 'approved'; } if (approval === false) { return 'rejected'; } return 'pending'; } function isApprovalItemLike(value) { if (!value || typeof value !== 'object') { return false; } if (!('rawItem' in value)) { return false; } const rawItem = value.rawItem; if (!rawItem || typeof rawItem !== 'object') { return false; } const itemType = rawItem.type; return APPROVAL_ITEM_TYPES.includes(itemType); } function getApprovalIdentity(approval) { const rawItem = approval.rawItem; if (!rawItem) { return undefined; } if (rawItem.type === 'function_call' && rawItem.callId) { return `function_call:${rawItem.callId}`; } if ('callId' in rawItem && rawItem.callId) { return `${rawItem.type}:${rawItem.callId}`; } const id = 'id' in rawItem ? rawItem.id : undefined; if (id) { return `${rawItem.type}:${id}`; } const providerData = typeof rawItem.providerData === 'object' && rawItem.providerData ? rawItem.providerData : undefined; if (providerData?.id) { return `${rawItem.type}:provider:${providerData.id}`; } const agentName = 'agent' in approval && approval.agent ? approval.agent.name : ''; try { return `${agentName}:${rawItem.type}:${JSON.stringify(rawItem)}`; } catch { return `${agentName}:${rawItem.type}`; } } function buildAppendContext(existingItems) { const seenItems = new Set(existingItems); const seenApprovalIdentities = new Set(); for (const item of existingItems) { if (item instanceof items_1.RunToolApprovalItem) { const identity = getApprovalIdentity(item); if (identity) { seenApprovalIdentities.add(identity); } } } return { seenItems, seenApprovalIdentities }; } function appendRunItemIfNew(item, target, context) { if (context.seenItems.has(item)) { return; } if (item instanceof items_1.RunToolApprovalItem) { const identity = getApprovalIdentity(item); if (identity) { if (context.seenApprovalIdentities.has(identity)) { return; } context.seenApprovalIdentities.add(identity); } } context.seenItems.add(item); target.push(item); } function buildApprovedCallIdSet(items, type) { const callIds = new Set(); for (const item of items) { if (!(item instanceof items_1.RunToolApprovalItem)) { continue; } const rawItem = item.rawItem; if (!rawItem || rawItem.type !== type) { continue; } if ('callId' in rawItem && rawItem.callId) { callIds.add(rawItem.callId); } else if ('id' in rawItem && rawItem.id) { callIds.add(rawItem.id); } } return callIds; } function collectCompletedCallIds(items, type) { const completed = new Set(); for (const item of items) { const rawItem = item.rawItem; if (!rawItem || typeof rawItem !== 'object') { continue; } if (rawItem.type !== type) { continue; } const callId = rawItem.callId; if (typeof callId === 'string') { completed.add(callId); } } return completed; } function filterActionsByApproval(preStepItems, actions, type) { const allowedCallIds = buildApprovedCallIdSet(preStepItems, type); if (allowedCallIds.size === 0) { return []; } return actions.filter((action) => typeof action.toolCall.callId === 'string' && allowedCallIds.has(action.toolCall.callId)); } function filterPendingActions(actions, options) { return actions.filter((action) => { const callId = action.toolCall.callId; const hasCallId = typeof callId === 'string'; if (options.allowedCallIds && options.allowedCallIds.size > 0) { if (!hasCallId || !options.allowedCallIds.has(callId)) { return false; } } if (hasCallId && options.completedCallIds.has(callId)) { return false; } return true; }); } function truncateForDeveloper(message, maxLength = 160) { const trimmed = message.trim(); if (!trimmed) { return 'Schema validation failed.'; } if (trimmed.length <= maxLength) { return trimmed; } return `${trimmed.slice(0, maxLength - 3)}...`; } function formatFinalOutputTypeError(error) { // Surface structured output validation hints without echoing potentially large or sensitive payloads. try { if (error instanceof zod_1.z.ZodError) { const issue = error.issues[0]; if (issue) { const issuePathParts = Array.isArray(issue.path) ? issue.path : []; const issuePath = issuePathParts.length > 0 ? issuePathParts.map((part) => String(part)).join('.') : '(root)'; const message = truncateForDeveloper(issue.message ?? ''); return `Invalid output type: final assistant output failed schema validation at "${issuePath}" (${message}).`; } return 'Invalid output type: final assistant output failed schema validation.'; } if (error instanceof Error && error.message) { return `Invalid output type: ${truncateForDeveloper(error.message)}`; } } catch { // Swallow formatting errors so we can return a generic message below. } return 'Invalid output type: final assistant output did not match the expected schema.'; } /** * @internal * Continues a turn that was previously interrupted waiting for tool approval. Executes the now * approved tools and returns the resulting step transition. */ async function resolveInterruptedTurn(agent, originalInput, originalPreStepItems, newResponse, processedResponse, runner, state, toolErrorFormatter) { // call_ids for function tools const functionCallIds = originalPreStepItems .filter((item) => item instanceof items_1.RunToolApprovalItem && 'callId' in item.rawItem && item.rawItem.type === 'function_call') .map((item) => item.rawItem.callId); const completedFunctionCallIds = collectCompletedCallIds(originalPreStepItems, 'function_call_result'); const completedComputerCallIds = collectCompletedCallIds(originalPreStepItems, 'computer_call_result'); const completedShellCallIds = collectCompletedCallIds(originalPreStepItems, 'shell_call_output'); const completedApplyPatchCallIds = collectCompletedCallIds(originalPreStepItems, 'apply_patch_call_output'); // We already persisted the turn once when the approval interrupt was raised, so the // counter reflects the approval items as "flushed". When we resume the same turn we need // to rewind it so the eventual tool output for this call is still written to the session. const pendingApprovalItems = state .getInterruptions() .filter(isApprovalItemLike); const pendingApprovalIdentities = new Set(); for (const approval of pendingApprovalItems) { if (!(approval instanceof items_1.RunToolApprovalItem)) { continue; } if (isHostedMcpApprovalItem(approval)) { continue; } const rawItem = approval.rawItem; if (rawItem.type === 'function_call' && rawItem.callId && completedFunctionCallIds.has(rawItem.callId)) { continue; } if (rawItem.type === 'computer_call' && rawItem.callId && completedComputerCallIds.has(rawItem.callId)) { continue; } if (rawItem.type === 'shell_call' && rawItem.callId && completedShellCallIds.has(rawItem.callId)) { continue; } if (rawItem.type === 'apply_patch_call' && rawItem.callId && completedApplyPatchCallIds.has(rawItem.callId)) { continue; } const identity = getApprovalIdentity(approval); if (identity) { if (resolveApprovalState(approval, state) === 'pending') { pendingApprovalIdentities.add(identity); } } } // Run function tools that require approval or are resuming a nested agent tool run. const functionToolRuns = processedResponse.functions.filter((run) => { const callId = run.toolCall.callId; if (!callId) { return false; } const isApprovedCall = functionCallIds.includes(callId); const isPendingNested = state.hasPendingAgentToolRun(run.tool.name, callId); if (!isApprovedCall && !isPendingNested) { return false; } return !completedFunctionCallIds.has(callId); }); const shellRuns = filterPendingActions(filterActionsByApproval(originalPreStepItems, processedResponse.shellActions, 'shell_call'), { completedCallIds: completedShellCallIds, }); const pendingComputerActions = filterPendingActions(filterActionsByApproval(originalPreStepItems, processedResponse.computerActions, 'computer_call'), { completedCallIds: completedComputerCallIds, }); const applyPatchRuns = filterPendingActions(filterActionsByApproval(originalPreStepItems, processedResponse.applyPatchActions, 'apply_patch_call'), { completedCallIds: completedApplyPatchCallIds, }); const functionResults = await (0, toolExecution_1.executeFunctionToolCalls)(agent, functionToolRuns, runner, state, toolErrorFormatter); // Computer actions may require approval; only pending approved actions are executed on resume. const computerResults = pendingComputerActions.length > 0 ? await (0, toolExecution_1.executeComputerActions)(agent, pendingComputerActions, runner, state._context, undefined, toolErrorFormatter) : []; const shellResults = shellRuns.length > 0 ? await (0, toolExecution_1.executeShellActions)(agent, shellRuns, runner, state._context, undefined, toolErrorFormatter) : []; const applyPatchResults = applyPatchRuns.length > 0 ? await (0, toolExecution_1.executeApplyPatchOperations)(agent, applyPatchRuns, runner, state._context, undefined, toolErrorFormatter) : []; const newItems = []; const appendContext = buildAppendContext(originalPreStepItems); const appendIfNew = (item) => appendRunItemIfNew(item, newItems, appendContext); for (const result of functionResults) { if (result.type === 'function_output' && Array.isArray(result.interruptions) && result.interruptions.length > 0) { continue; } appendIfNew(result.runItem); } for (const result of computerResults) { appendIfNew(result); } for (const result of shellResults) { appendIfNew(result); } for (const result of applyPatchResults) { appendIfNew(result); } const additionalInterruptions = (0, toolExecution_1.collectInterruptions)([], [...computerResults, ...shellResults, ...applyPatchResults]); const hostedMcpApprovals = await (0, mcpApprovals_1.handleHostedMcpApprovals)({ requests: processedResponse.mcpApprovalRequests, agent, state, functionResults, appendIfNew, resolveApproval: (rawItem) => { const providerData = rawItem.providerData; const approvalRequestId = rawItem.id ?? providerData?.id; if (!approvalRequestId) { return undefined; } return state._context.isToolApproved({ toolName: rawItem.name, callId: approvalRequestId, }); }, }); // Server-managed conversations rely on preStepItems to re-surface pending approvals. // Keep unresolved hosted MCP approvals in place so HITL flows still have something to approve next turn. // Drop resolved approval placeholders so they are not replayed on the next turn, but keep // pending approvals in place to signal the outstanding work to the UI and session store. const preStepItems = originalPreStepItems.filter((item) => { if (!(item instanceof items_1.RunToolApprovalItem)) { return true; } if (isHostedMcpApprovalItem(item)) { if (hostedMcpApprovals.pendingApprovals.has(item)) { return true; } const approvalRequestId = item.rawItem.id ?? item.rawItem.providerData?.id; if (approvalRequestId) { return hostedMcpApprovals.pendingApprovalIds.has(approvalRequestId); } return false; } // Preserve all other approval items so resumptions can continue to reference the // original approval requests (e.g., function/shell/apply_patch) ONLY while they are still pending. const identity = getApprovalIdentity(item); if (!identity) { return true; } return pendingApprovalIdentities.has(identity); }); const keptApprovalItems = new Set(); for (const item of preStepItems) { if (item instanceof items_1.RunToolApprovalItem) { keptApprovalItems.add(item); } } let removedApprovalCount = 0; for (const item of originalPreStepItems) { if (item instanceof items_1.RunToolApprovalItem && !keptApprovalItems.has(item)) { removedApprovalCount++; } } if (removedApprovalCount > 0) { // Persisting the approval request already advanced the counter once, so undo the increment // to make sure we write the final tool output back to the session when the turn resumes. state.rewindTurnPersistence(removedApprovalCount); } const completedStep = await maybeCompleteTurnFromToolResults({ agent, runner, state, functionResults, originalInput, newResponse, preStepItems, newItems, additionalInterruptions, }); if (completedStep) { return completedStep; } // we only ran new tools and side effects. We need to run the rest of the agent return new steps_1.SingleStepResult(originalInput, newResponse, preStepItems, newItems, { type: 'next_step_run_again' }); } /** * @internal * Executes every follow-up action the model requested (function tools, computer actions, MCP flows), * appends their outputs to the run history, and determines the next step for the agent loop. */ async function resolveTurnAfterModelResponse(agent, originalInput, originalPreStepItems, newResponse, processedResponse, runner, state, toolErrorFormatter) { // Reuse the same array reference so we can compare object identity when deciding whether to // append new items, ensuring we never double-stream existing RunItems. const preStepItems = originalPreStepItems; const newItems = []; const appendContext = buildAppendContext(originalPreStepItems); const appendIfNew = (item) => appendRunItemIfNew(item, newItems, appendContext); for (const item of processedResponse.newItems) { appendIfNew(item); } // Run function tools and computer actions in parallel; neither depends on the other's side effects. const [functionResults, computerResults, shellResults, applyPatchResults] = await Promise.all([ (0, toolExecution_1.executeFunctionToolCalls)(agent, processedResponse.functions, runner, state, toolErrorFormatter), (0, toolExecution_1.executeComputerActions)(agent, processedResponse.computerActions, runner, state._context, undefined, toolErrorFormatter), (0, toolExecution_1.executeShellActions)(agent, processedResponse.shellActions, runner, state._context, undefined, toolErrorFormatter), (0, toolExecution_1.executeApplyPatchOperations)(agent, processedResponse.applyPatchActions, runner, state._context, undefined, toolErrorFormatter), ]); for (const result of functionResults) { if (result.type === 'function_output' && Array.isArray(result.interruptions) && result.interruptions.length > 0) { continue; } appendIfNew(result.runItem); } for (const item of computerResults) { appendIfNew(item); } for (const item of shellResults) { appendIfNew(item); } for (const item of applyPatchResults) { appendIfNew(item); } const additionalInterruptions = (0, toolExecution_1.collectInterruptions)([], [...computerResults, ...shellResults, ...applyPatchResults]); if (processedResponse.mcpApprovalRequests.length > 0) { await (0, mcpApprovals_1.handleHostedMcpApprovals)({ requests: processedResponse.mcpApprovalRequests, agent, state, functionResults, appendIfNew, resolveApproval: (rawItem) => { const providerData = rawItem.providerData; const approvalRequestId = rawItem.id ?? providerData?.id; if (!approvalRequestId) { return undefined; } return state._context.isToolApproved({ toolName: rawItem.name, callId: approvalRequestId, }); }, }); } // process handoffs if (processedResponse.handoffs.length > 0) { return await (0, toolExecution_1.executeHandoffCalls)(agent, originalInput, preStepItems, newItems, newResponse, processedResponse.handoffs, runner, state._context); } const completedStep = await maybeCompleteTurnFromToolResults({ agent, runner, state, functionResults, originalInput, newResponse, preStepItems, newItems, additionalInterruptions, }); if (completedStep) { return completedStep; } // If the model issued any tool calls or handoffs in this turn, // we must NOT treat any assistant message in the same turn as the final output. // We should run the loop again so the model can see the tool results and respond. if (processedResponse.hasToolsOrApprovalsToRun()) { return new steps_1.SingleStepResult(originalInput, newResponse, preStepItems, newItems, { type: 'next_step_run_again' }); } // No tool calls/actions in this turn; safe to consider a plain assistant message as final. const messageItems = newItems.filter((item) => item instanceof items_1.RunMessageOutputItem); // we will use the last content output as the final output const potentialFinalOutput = messageItems.length > 0 ? (0, messages_1.getLastTextFromOutputMessage)(messageItems[messageItems.length - 1].rawItem) : undefined; // if there is no output we just run again if (typeof potentialFinalOutput === 'undefined') { return new steps_1.SingleStepResult(originalInput, newResponse, preStepItems, newItems, { type: 'next_step_run_again' }); } // Keep looping if any tool output placeholders still require an approval follow-up. const hasPendingToolsOrApprovals = functionResults.some((result) => result.runItem instanceof items_1.RunToolApprovalItem) || additionalInterruptions.length > 0; if (!hasPendingToolsOrApprovals) { if (agent.outputType === 'text') { return new steps_1.SingleStepResult(originalInput, newResponse, preStepItems, newItems, { type: 'next_step_final_output', output: potentialFinalOutput, }); } if (agent.outputType !== 'text' && potentialFinalOutput) { // Structured output schema => always leads to a final output if we have text. const { parser } = (0, tools_1.getSchemaAndParserFromInputType)(agent.outputType, 'final_output'); const [error] = await (0, safeExecute_1.safeExecute)(() => parser(potentialFinalOutput)); if (error) { const outputErrorMessage = formatFinalOutputTypeError(error); (0, context_1.addErrorToCurrentSpan)({ message: outputErrorMessage, data: { error: String(error), }, }); throw new errors_1.ModelBehaviorError(outputErrorMessage); } return new steps_1.SingleStepResult(originalInput, newResponse, preStepItems, newItems, { type: 'next_step_final_output', output: potentialFinalOutput }); } } return new steps_1.SingleStepResult(originalInput, newResponse, preStepItems, newItems, { type: 'next_step_run_again' }); } // Consolidates the logic that determines whether tool results yielded a final answer, // triggered an interruption, or require the agent loop to continue running. async function maybeCompleteTurnFromToolResults({ agent, runner: _runner, state, functionResults, originalInput, newResponse, preStepItems, newItems, additionalInterruptions = [], }) { const toolOutcome = await (0, toolExecution_1.checkForFinalOutputFromTools)(agent, functionResults, state, additionalInterruptions); if (toolOutcome.isFinalOutput) { // Intentional: explicit toolUseBehavior finalization (for example stop_on_first_tool) takes precedence even when other provider-managed work is still pending. return new steps_1.SingleStepResult(originalInput, newResponse, preStepItems, newItems, { type: 'next_step_final_output', output: toolOutcome.finalOutput, }); } if (toolOutcome.isInterrupted) { return new steps_1.SingleStepResult(originalInput, newResponse, preStepItems, newItems, { type: 'next_step_interruption', data: { interruptions: toolOutcome.interruptions, }, }); } return null; } //# sourceMappingURL=turnResolution.js.map