@openai/agents-core
Version:
The OpenAI Agents SDK is a lightweight yet powerful framework for building multi-agent workflows.
764 lines • 31.1 kB
JavaScript
import { ModelBehaviorError, ToolCallError, UserError } from "./errors.js";
import { getTransferMessage } from "./handoff.js";
import { RunHandoffCallItem, RunHandoffOutputItem, RunMessageOutputItem, RunReasoningItem, RunToolApprovalItem, RunToolCallItem, RunToolCallOutputItem, } from "./items.js";
import logger from "./logger.js";
import { getLastTextFromOutputMessage } from "./utils/messages.js";
import { withFunctionSpan, withHandoffSpan } from "./tracing/createSpans.js";
import { getSchemaAndParserFromInputType } from "./utils/tools.js";
import { safeExecute } from "./utils/safeExecute.js";
import { addErrorToCurrentSpan } from "./tracing/context.js";
import { RunItemStreamEvent } from "./events.js";
import { z } from '@openai/zod/v3';
import { toSmartString } from "./utils/smartString.js";
import { isZodObject } from "./utils/index.js";
/**
* @internal
*/
export function processModelResponse(modelResponse, agent, tools, handoffs) {
const items = [];
const runHandoffs = [];
const runFunctions = [];
const runComputerActions = [];
const runMCPApprovalRequests = [];
const toolsUsed = [];
const handoffMap = new Map(handoffs.map((h) => [h.toolName, h]));
const functionMap = new Map(tools.filter((t) => t.type === 'function').map((t) => [t.name, t]));
const computerTool = tools.find((t) => t.type === 'computer');
const mcpToolMap = new Map(tools
.filter((t) => t.type === 'hosted_tool' && t.providerData?.type === 'mcp')
.map((t) => t)
.map((t) => [t.providerData.server_label, t]));
for (const output of modelResponse.output) {
if (output.type === 'message') {
if (output.role === 'assistant') {
items.push(new RunMessageOutputItem(output, agent));
}
}
else if (output.type === 'hosted_tool_call') {
items.push(new RunToolCallItem(output, agent));
const toolName = output.name;
toolsUsed.push(toolName);
if (output.providerData?.type === 'mcp_approval_request' ||
output.name === 'mcp_approval_request') {
// Hosted remote MCP server's approval process
const providerData = output.providerData;
const mcpServerLabel = providerData.server_label;
const mcpServerTool = mcpToolMap.get(mcpServerLabel);
if (typeof mcpServerTool === 'undefined') {
const message = `MCP server (${mcpServerLabel}) not found in Agent (${agent.name})`;
addErrorToCurrentSpan({
message,
data: { mcp_server_label: mcpServerLabel },
});
throw new ModelBehaviorError(message);
}
// Do this approval later:
// We support both onApproval callback (like the Python SDK does) and HITL patterns.
const approvalItem = new RunToolApprovalItem({
type: 'hosted_tool_call',
// We must use this name to align with the name sent from the servers
name: providerData.name,
id: providerData.id,
status: 'in_progress',
providerData,
}, agent);
runMCPApprovalRequests.push({
requestItem: approvalItem,
mcpTool: mcpServerTool,
});
if (!mcpServerTool.providerData.on_approval) {
// When onApproval function exists, it confirms the approval right after this.
// Thus, this approval item must be appended only for the next turn interruption patterns.
items.push(approvalItem);
}
}
}
else if (output.type === 'reasoning') {
items.push(new RunReasoningItem(output, agent));
}
else if (output.type === 'computer_call') {
items.push(new RunToolCallItem(output, agent));
toolsUsed.push('computer_use');
if (!computerTool) {
addErrorToCurrentSpan({
message: 'Model produced computer action without a computer tool.',
data: {
agent_name: agent.name,
},
});
throw new ModelBehaviorError('Model produced computer action without a computer tool.');
}
runComputerActions.push({
toolCall: output,
computer: computerTool,
});
}
if (output.type !== 'function_call') {
continue;
}
toolsUsed.push(output.name);
const handoff = handoffMap.get(output.name);
if (handoff) {
items.push(new RunHandoffCallItem(output, agent));
runHandoffs.push({
toolCall: output,
handoff: handoff,
});
}
else {
const functionTool = functionMap.get(output.name);
if (!functionTool) {
addErrorToCurrentSpan({
message: `Tool ${output.name} not found in agent ${agent.name}.`,
data: {
tool_name: output.name,
agent_name: agent.name,
},
});
throw new ModelBehaviorError(`Tool ${output.name} not found in agent ${agent.name}.`);
}
items.push(new RunToolCallItem(output, agent));
runFunctions.push({
toolCall: output,
tool: functionTool,
});
}
}
return {
newItems: items,
handoffs: runHandoffs,
functions: runFunctions,
computerActions: runComputerActions,
mcpApprovalRequests: runMCPApprovalRequests,
toolsUsed: toolsUsed,
hasToolsOrApprovalsToRun() {
return (runHandoffs.length > 0 ||
runFunctions.length > 0 ||
runMCPApprovalRequests.length > 0 ||
runComputerActions.length > 0);
},
};
}
export const nextStepSchema = z.discriminatedUnion('type', [
z.object({
type: z.literal('next_step_handoff'),
newAgent: z.any(),
}),
z.object({
type: z.literal('next_step_final_output'),
output: z.string(),
}),
z.object({
type: z.literal('next_step_run_again'),
}),
z.object({
type: z.literal('next_step_interruption'),
data: z.record(z.string(), z.any()),
}),
]);
class SingleStepResult {
originalInput;
modelResponse;
preStepItems;
newStepItems;
nextStep;
constructor(
/**
* The input items i.e. the items before run() was called. May be muted by handoff input filters
*/
originalInput,
/**
* The model response for the current step
*/
modelResponse,
/**
* The items before the current step was executed
*/
preStepItems,
/**
* The items after the current step was executed
*/
newStepItems,
/**
* The next step to execute
*/
nextStep) {
this.originalInput = originalInput;
this.modelResponse = modelResponse;
this.preStepItems = preStepItems;
this.newStepItems = newStepItems;
this.nextStep = nextStep;
}
/**
* The items generated during the agent run (i.e. everything generated after originalInput)
*/
get generatedItems() {
return this.preStepItems.concat(this.newStepItems);
}
}
/**
* @internal
*/
export function maybeResetToolChoice(agent, toolUseTracker, modelSettings) {
if (agent.resetToolChoice && toolUseTracker.hasUsedTools(agent)) {
return { ...modelSettings, toolChoice: undefined };
}
return modelSettings;
}
/**
* @internal
*/
export async function executeInterruptedToolsAndSideEffects(agent, originalInput, originalPreStepItems, newResponse, processedResponse, runner, state) {
// call_ids for function tools
const functionCallIds = originalPreStepItems
.filter((item) => item instanceof RunToolApprovalItem &&
'callId' in item.rawItem &&
item.rawItem.type === 'function_call')
.map((item) => item.rawItem.callId);
// Run function tools that require approval after they get their approval results
const functionToolRuns = processedResponse.functions.filter((run) => {
return functionCallIds.includes(run.toolCall.callId);
});
const functionResults = await executeFunctionToolCalls(agent, functionToolRuns, runner, state);
// Create the initial set of the output items
const newItems = functionResults.map((r) => r.runItem);
// Run MCP tools that require approval after they get their approval results
const mcpApprovalRuns = processedResponse.mcpApprovalRequests.filter((run) => {
return (run.requestItem.type === 'tool_approval_item' &&
run.requestItem.rawItem.type === 'hosted_tool_call' &&
run.requestItem.rawItem.providerData?.type === 'mcp_approval_request');
});
for (const run of mcpApprovalRuns) {
// the approval_request_id "mcpr_123..."
const approvalRequestId = run.requestItem.rawItem.id;
const approved = state._context.isToolApproved({
// Since this item name must be the same with the one sent from Responses API server
toolName: run.requestItem.rawItem.name,
callId: approvalRequestId,
});
if (typeof approved !== 'undefined') {
const providerData = {
approve: approved,
approval_request_id: approvalRequestId,
reason: undefined,
};
// Tell Responses API server the approval result in the next turn
newItems.push(new RunToolCallItem({
type: 'hosted_tool_call',
name: 'mcp_approval_response',
providerData,
}, agent));
}
}
const checkToolOutput = await checkForFinalOutputFromTools(agent, functionResults, state);
// Exclude the tool approval items, which should not be sent to Responses API,
// from the SingleStepResult's preStepItems
const preStepItems = originalPreStepItems.filter((item) => {
return !(item instanceof RunToolApprovalItem);
});
if (checkToolOutput.isFinalOutput) {
runner.emit('agent_end', state._context, agent, checkToolOutput.finalOutput);
agent.emit('agent_end', state._context, checkToolOutput.finalOutput);
return new SingleStepResult(originalInput, newResponse, preStepItems, newItems, {
type: 'next_step_final_output',
output: checkToolOutput.finalOutput,
});
}
else if (checkToolOutput.isInterrupted) {
return new SingleStepResult(originalInput, newResponse, preStepItems, newItems, {
type: 'next_step_interruption',
data: {
interruptions: checkToolOutput.interruptions,
},
});
}
// we only ran new tools and side effects. We need to run the rest of the agent
return new SingleStepResult(originalInput, newResponse, preStepItems, newItems, { type: 'next_step_run_again' });
}
/**
* @internal
*/
export async function executeToolsAndSideEffects(agent, originalInput, originalPreStepItems, newResponse, processedResponse, runner, state) {
const preStepItems = originalPreStepItems;
let newItems = processedResponse.newItems;
const [functionResults, computerResults] = await Promise.all([
executeFunctionToolCalls(agent, processedResponse.functions, runner, state),
executeComputerActions(agent, processedResponse.computerActions, runner, state._context),
]);
newItems = newItems.concat(functionResults.map((r) => r.runItem));
newItems = newItems.concat(computerResults);
// run hosted MCP approval requests
if (processedResponse.mcpApprovalRequests.length > 0) {
for (const approvalRequest of processedResponse.mcpApprovalRequests) {
const toolData = approvalRequest.mcpTool
.providerData;
const requestData = approvalRequest.requestItem.rawItem
.providerData;
if (toolData.on_approval) {
// synchronously handle the approval process here
const approvalResult = await toolData.on_approval(state._context, approvalRequest.requestItem);
const approvalResponseData = {
approve: approvalResult.approve,
approval_request_id: requestData.id,
reason: approvalResult.reason,
};
newItems.push(new RunToolCallItem({
type: 'hosted_tool_call',
name: 'mcp_approval_response',
providerData: approvalResponseData,
}, agent));
}
else {
// receive a user's approval on the next turn
newItems.push(approvalRequest.requestItem);
const approvalItem = {
type: 'hosted_mcp_tool_approval',
tool: approvalRequest.mcpTool,
runItem: new RunToolApprovalItem({
type: 'hosted_tool_call',
name: requestData.name,
id: requestData.id,
arguments: requestData.arguments,
status: 'in_progress',
providerData: requestData,
}, agent),
};
functionResults.push(approvalItem);
// newItems.push(approvalItem.runItem);
}
}
}
// process handoffs
if (processedResponse.handoffs.length > 0) {
return await executeHandoffCalls(agent, originalInput, preStepItems, newItems, newResponse, processedResponse.handoffs, runner, state._context);
}
const checkToolOutput = await checkForFinalOutputFromTools(agent, functionResults, state);
if (checkToolOutput.isFinalOutput) {
runner.emit('agent_end', state._context, agent, checkToolOutput.finalOutput);
agent.emit('agent_end', state._context, checkToolOutput.finalOutput);
return new SingleStepResult(originalInput, newResponse, preStepItems, newItems, {
type: 'next_step_final_output',
output: checkToolOutput.finalOutput,
});
}
else if (checkToolOutput.isInterrupted) {
return new SingleStepResult(originalInput, newResponse, preStepItems, newItems, {
type: 'next_step_interruption',
data: {
interruptions: checkToolOutput.interruptions,
},
});
}
// check if the agent produced any messages
const messageItems = newItems.filter((item) => item instanceof RunMessageOutputItem);
// we will use the last content output as the final output
const potentialFinalOutput = messageItems.length > 0
? getLastTextFromOutputMessage(messageItems[messageItems.length - 1].rawItem)
: undefined;
// if there is no output we just run again
if (!potentialFinalOutput) {
return new SingleStepResult(originalInput, newResponse, preStepItems, newItems, { type: 'next_step_run_again' });
}
if (agent.outputType === 'text' &&
!processedResponse.hasToolsOrApprovalsToRun()) {
return new SingleStepResult(originalInput, newResponse, preStepItems, newItems, {
type: 'next_step_final_output',
output: potentialFinalOutput,
});
}
else if (agent.outputType !== 'text' && potentialFinalOutput) {
// Structured output schema => always leads to a final output if we have text
const { parser } = getSchemaAndParserFromInputType(agent.outputType, 'final_output');
const [error] = await safeExecute(() => parser(potentialFinalOutput));
if (error) {
addErrorToCurrentSpan({
message: 'Invalid output type',
data: {
error: String(error),
},
});
throw new ModelBehaviorError('Invalid output type');
}
return new SingleStepResult(originalInput, newResponse, preStepItems, newItems, { type: 'next_step_final_output', output: potentialFinalOutput });
}
return new SingleStepResult(originalInput, newResponse, preStepItems, newItems, { type: 'next_step_run_again' });
}
/**
* @internal
*/
export function getToolCallOutputItem(toolCall, output) {
return {
type: 'function_call_result',
name: toolCall.name,
callId: toolCall.callId,
status: 'completed',
output: {
type: 'text',
text: toSmartString(output),
},
};
}
/**
* @internal
*/
export async function executeFunctionToolCalls(agent, toolRuns, runner, state) {
async function runSingleTool(toolRun) {
let parsedArgs = toolRun.toolCall.arguments;
if (toolRun.tool.parameters) {
if (isZodObject(toolRun.tool.parameters)) {
parsedArgs = toolRun.tool.parameters.parse(parsedArgs);
}
else {
parsedArgs = JSON.parse(parsedArgs);
}
}
const needsApproval = await toolRun.tool.needsApproval(state._context, parsedArgs, toolRun.toolCall.callId);
if (needsApproval) {
const approval = state._context.isToolApproved({
toolName: toolRun.tool.name,
callId: toolRun.toolCall.callId,
});
if (approval === false) {
// rejected
return withFunctionSpan(async (span) => {
const response = 'Tool execution was not approved.';
span.setError({
message: response,
data: {
tool_name: toolRun.tool.name,
error: `Tool execution for ${toolRun.toolCall.callId} was manually rejected by user.`,
},
});
span.spanData.output = response;
return {
type: 'function_output',
tool: toolRun.tool,
output: response,
runItem: new RunToolCallOutputItem(getToolCallOutputItem(toolRun.toolCall, response), agent, response),
};
}, {
data: {
name: toolRun.tool.name,
},
});
}
if (approval !== true) {
// this approval process needs to be done in the next turn
return {
type: 'function_approval',
tool: toolRun.tool,
runItem: new RunToolApprovalItem(toolRun.toolCall, agent),
};
}
}
return withFunctionSpan(async (span) => {
if (runner.config.traceIncludeSensitiveData) {
span.spanData.input = toolRun.toolCall.arguments;
}
try {
runner.emit('agent_tool_start', state._context, agent, toolRun.tool, {
toolCall: toolRun.toolCall,
});
agent.emit('agent_tool_start', state._context, toolRun.tool, {
toolCall: toolRun.toolCall,
});
const result = await toolRun.tool.invoke(state._context, toolRun.toolCall.arguments);
// Use string data for tracing and event emitter
const stringResult = toSmartString(result);
runner.emit('agent_tool_end', state._context, agent, toolRun.tool, stringResult, { toolCall: toolRun.toolCall });
agent.emit('agent_tool_end', state._context, toolRun.tool, stringResult, { toolCall: toolRun.toolCall });
if (runner.config.traceIncludeSensitiveData) {
span.spanData.output = stringResult;
}
return {
type: 'function_output',
tool: toolRun.tool,
output: result,
runItem: new RunToolCallOutputItem(getToolCallOutputItem(toolRun.toolCall, result), agent, result),
};
}
catch (error) {
span.setError({
message: 'Error running tool',
data: {
tool_name: toolRun.tool.name,
error: String(error),
},
});
throw error;
}
}, {
data: {
name: toolRun.tool.name,
},
});
}
try {
const results = await Promise.all(toolRuns.map(runSingleTool));
return results;
}
catch (e) {
throw new ToolCallError(`Failed to run function tools: ${e}`, e, state);
}
}
/**
* @internal
*/
// Internal helper: dispatch a computer action and return a screenshot (sync/async)
async function _runComputerActionAndScreenshot(computer, toolCall) {
const action = toolCall.action;
let screenshot;
// Dispatch based on action type string (assume action.type exists)
switch (action.type) {
case 'click':
await computer.click(action.x, action.y, action.button);
break;
case 'double_click':
await computer.doubleClick(action.x, action.y);
break;
case 'drag':
await computer.drag(action.path.map((p) => [p.x, p.y]));
break;
case 'keypress':
await computer.keypress(action.keys);
break;
case 'move':
await computer.move(action.x, action.y);
break;
case 'screenshot':
screenshot = await computer.screenshot();
break;
case 'scroll':
await computer.scroll(action.x, action.y, action.scroll_x, action.scroll_y);
break;
case 'type':
await computer.type(action.text);
break;
case 'wait':
await computer.wait();
break;
default:
action; // ensures that we handle every action we know of
// Unknown action, just take screenshot
break;
}
if (typeof screenshot !== 'undefined') {
return screenshot;
}
// Always return screenshot as base64 string
if (typeof computer.screenshot === 'function') {
screenshot = await computer.screenshot();
if (typeof screenshot !== 'undefined') {
return screenshot;
}
}
throw new Error('Computer does not implement screenshot()');
}
/**
* @internal
*/
export async function executeComputerActions(agent, actions, runner, runContext, customLogger = undefined) {
const _logger = customLogger ?? logger;
const results = [];
for (const action of actions) {
const computer = action.computer.computer;
const toolCall = action.toolCall;
// Hooks: on_tool_start (global + agent)
runner.emit('agent_tool_start', runContext, agent, action.computer, {
toolCall,
});
if (typeof agent.emit === 'function') {
agent.emit('agent_tool_start', runContext, action.computer, { toolCall });
}
// Run the action and get screenshot
let output;
try {
output = await _runComputerActionAndScreenshot(computer, toolCall);
}
catch (err) {
_logger.error('Failed to execute computer action:', err);
output = '';
}
// Hooks: on_tool_end (global + agent)
runner.emit('agent_tool_end', runContext, agent, action.computer, output, {
toolCall,
});
if (typeof agent.emit === 'function') {
agent.emit('agent_tool_end', runContext, action.computer, output, {
toolCall,
});
}
// Always return a screenshot as a base64 data URL
const imageUrl = output ? `data:image/png;base64,${output}` : '';
const rawItem = {
type: 'computer_call_result',
callId: toolCall.callId,
output: { type: 'computer_screenshot', data: imageUrl },
};
results.push(new RunToolCallOutputItem(rawItem, agent, imageUrl));
}
return results;
}
/**
* @internal
*/
export async function executeHandoffCalls(agent, originalInput, preStepItems, newStepItems, newResponse, runHandoffs, runner, runContext) {
newStepItems = [...newStepItems];
if (runHandoffs.length === 0) {
logger.warn('Incorrectly called executeHandoffCalls with no handoffs. This should not happen. Moving on.');
return new SingleStepResult(originalInput, newResponse, preStepItems, newStepItems, { type: 'next_step_run_again' });
}
if (runHandoffs.length > 1) {
// multiple handoffs. Ignoring all but the first one by adding reject responses for those
const outputMessage = 'Multiple handoffs detected, ignoring this one.';
for (let i = 1; i < runHandoffs.length; i++) {
newStepItems.push(new RunToolCallOutputItem(getToolCallOutputItem(runHandoffs[i].toolCall, outputMessage), agent, outputMessage));
}
}
const actualHandoff = runHandoffs[0];
return withHandoffSpan(async (handoffSpan) => {
const handoff = actualHandoff.handoff;
const newAgent = await handoff.onInvokeHandoff(runContext, actualHandoff.toolCall.arguments);
handoffSpan.spanData.to_agent = newAgent.name;
if (runHandoffs.length > 1) {
const requestedAgents = runHandoffs.map((h) => h.handoff.agentName);
handoffSpan.setError({
message: 'Multiple handoffs requested',
data: {
requested_agents: requestedAgents,
},
});
}
newStepItems.push(new RunHandoffOutputItem(getToolCallOutputItem(actualHandoff.toolCall, getTransferMessage(newAgent)), agent, newAgent));
runner.emit('agent_handoff', runContext, agent, newAgent);
agent.emit('agent_handoff', runContext, newAgent);
const inputFilter = handoff.inputFilter ?? runner.config.handoffInputFilter;
if (inputFilter) {
logger.debug('Filtering inputs for handoff');
if (typeof inputFilter !== 'function') {
handoffSpan.setError({
message: 'Invalid input filter',
data: {
details: 'not callable',
},
});
}
const handoffInputData = {
inputHistory: Array.isArray(originalInput)
? [...originalInput]
: originalInput,
preHandoffItems: [...preStepItems],
newItems: [...newStepItems],
};
const filtered = inputFilter(handoffInputData);
originalInput = filtered.inputHistory;
preStepItems = filtered.preHandoffItems;
newStepItems = filtered.newItems;
}
return new SingleStepResult(originalInput, newResponse, preStepItems, newStepItems, { type: 'next_step_handoff', newAgent });
}, {
data: {
from_agent: agent.name,
},
});
}
const NOT_FINAL_OUTPUT = {
isFinalOutput: false,
isInterrupted: undefined,
};
/**
* @internal
*/
export async function checkForFinalOutputFromTools(agent, toolResults, state) {
if (toolResults.length === 0) {
return NOT_FINAL_OUTPUT;
}
const interruptions = toolResults
.filter((r) => r.runItem instanceof RunToolApprovalItem)
.map((r) => r.runItem);
if (interruptions.length > 0) {
return {
isFinalOutput: false,
isInterrupted: true,
interruptions,
};
}
if (agent.toolUseBehavior === 'run_llm_again') {
return NOT_FINAL_OUTPUT;
}
const firstToolResult = toolResults[0];
if (agent.toolUseBehavior === 'stop_on_first_tool') {
if (firstToolResult?.type === 'function_output') {
const stringOutput = toSmartString(firstToolResult.output);
return {
isFinalOutput: true,
isInterrupted: undefined,
finalOutput: stringOutput,
};
}
return NOT_FINAL_OUTPUT;
}
const toolUseBehavior = agent.toolUseBehavior;
if (typeof toolUseBehavior === 'object') {
const stoppingTool = toolResults.find((r) => toolUseBehavior.stopAtToolNames.includes(r.tool.name));
if (stoppingTool?.type === 'function_output') {
const stringOutput = toSmartString(stoppingTool.output);
return {
isFinalOutput: true,
isInterrupted: undefined,
finalOutput: stringOutput,
};
}
return NOT_FINAL_OUTPUT;
}
if (typeof toolUseBehavior === 'function') {
return toolUseBehavior(state._context, toolResults);
}
throw new UserError(`Invalid toolUseBehavior: ${toolUseBehavior}`, state);
}
export function addStepToRunResult(result, step) {
for (const item of step.newStepItems) {
let itemName;
if (item instanceof RunMessageOutputItem) {
itemName = 'message_output_created';
}
else if (item instanceof RunHandoffCallItem) {
itemName = 'handoff_requested';
}
else if (item instanceof RunHandoffOutputItem) {
itemName = 'handoff_occurred';
}
else if (item instanceof RunToolCallItem) {
itemName = 'tool_called';
}
else if (item instanceof RunToolCallOutputItem) {
itemName = 'tool_output';
}
else if (item instanceof RunReasoningItem) {
itemName = 'reasoning_item_created';
}
else if (item instanceof RunToolApprovalItem) {
itemName = 'tool_approval_requested';
}
else {
logger.warn('Unknown item type: ', item);
continue;
}
result._addItem(new RunItemStreamEvent(itemName, item));
}
}
export class AgentToolUseTracker {
#agentToTools = new Map();
addToolUse(agent, toolNames) {
this.#agentToTools.set(agent, toolNames);
}
hasUsedTools(agent) {
return this.#agentToTools.has(agent);
}
toJSON() {
return Object.fromEntries(Array.from(this.#agentToTools.entries()).map(([agent, toolNames]) => {
return [agent.name, toolNames];
}));
}
}
//# sourceMappingURL=runImplementation.js.map