UNPKG

@openai/agents-core

Version:

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

1,216 lines (1,215 loc) 80.7 kB
import { consumeAgentToolRunResult, } from "./agent.mjs"; import { ModelBehaviorError, ToolCallError, UserError } from "./errors.mjs"; import { getTransferMessage } from "./handoff.mjs"; import { RunHandoffCallItem, RunHandoffOutputItem, RunMessageOutputItem, RunReasoningItem, RunToolApprovalItem, RunToolCallItem, RunToolCallOutputItem, } from "./items.mjs"; import logger from "./logger.mjs"; import { resolveComputer, } from "./tool.mjs"; import { getLastTextFromOutputMessage } from "./utils/messages.mjs"; import { withFunctionSpan, withHandoffSpan } from "./tracing/createSpans.mjs"; import { getSchemaAndParserFromInputType } from "./utils/tools.mjs"; import { encodeUint8ArrayToBase64 } from "./utils/base64.mjs"; import { isArrayBufferView, isNodeBuffer, isSerializedBufferSnapshot, toSmartString, } from "./utils/smartString.mjs"; import { safeExecute } from "./utils/safeExecute.mjs"; import { addErrorToCurrentSpan } from "./tracing/context.mjs"; import { RunItemStreamEvent } from "./events.mjs"; import { z } from 'zod'; import { isZodObject } from "./utils/index.mjs"; import { isOpenAIResponsesCompactionAwareSession, } from "./memory/session.mjs"; import { Usage } from "./usage.mjs"; 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 itemType === 'function_call' || itemType === 'hosted_tool_call'; } 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 formatFinalOutputTypeError(error) { // Surface structured output validation hints without echoing potentially large or sensitive payloads. try { if (error instanceof 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.'; } 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)}...`; } /** * @internal * Walks a raw model response and classifies each item so the runner can schedule follow-up work. * Returns both the serializable RunItems (for history/streaming) and the actionable tool metadata. */ export function processModelResponse(modelResponse, agent, tools, handoffs) { const items = []; const runHandoffs = []; const runFunctions = []; const runComputerActions = []; const runShellActions = []; const runApplyPatchActions = []; const runMCPApprovalRequests = []; const toolsUsed = []; const handoffMap = new Map(handoffs.map((h) => [h.toolName, h])); // Resolve tools upfront so we can look up the concrete handler in O(1) while iterating outputs. const functionMap = new Map(tools.filter((t) => t.type === 'function').map((t) => [t.name, t])); const computerTool = tools.find((t) => t.type === 'computer'); const shellTool = tools.find((t) => t.type === 'shell'); const applyPatchTool = tools.find((t) => t.type === 'apply_patch'); 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, }); } else if (output.type === 'shell_call') { items.push(new RunToolCallItem(output, agent)); toolsUsed.push('shell'); if (!shellTool) { addErrorToCurrentSpan({ message: 'Model produced shell action without a shell tool.', data: { agent_name: agent.name, }, }); throw new ModelBehaviorError('Model produced shell action without a shell tool.'); } runShellActions.push({ toolCall: output, shell: shellTool, }); } else if (output.type === 'apply_patch_call') { items.push(new RunToolCallItem(output, agent)); toolsUsed.push('apply_patch'); if (!applyPatchTool) { addErrorToCurrentSpan({ message: 'Model produced apply_patch action without an apply_patch tool.', data: { agent_name: agent.name, }, }); throw new ModelBehaviorError('Model produced apply_patch action without an apply_patch tool.'); } runApplyPatchActions.push({ toolCall: output, applyPatch: applyPatchTool, }); } 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, shellActions: runShellActions, applyPatchActions: runApplyPatchActions, mcpApprovalRequests: runMCPApprovalRequests, toolsUsed: toolsUsed, hasToolsOrApprovalsToRun() { return (runHandoffs.length > 0 || runFunctions.length > 0 || runMCPApprovalRequests.length > 0 || runComputerActions.length > 0 || runShellActions.length > 0 || runApplyPatchActions.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()), }), ]); /** * Internal convenience wrapper that groups the outcome of a single agent turn. It lets the caller * update the RunState in one shot and decide which step to execute next. */ class SingleStepResult { originalInput; modelResponse; preStepItems; newStepItems; nextStep; constructor( /** * The input items (i.e., the items before run() was called). May be mutated 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 * Resets the tool choice when the agent is configured to prefer a fresh tool selection after * any tool usage. This prevents the provider from reusing stale tool hints across turns. */ export function maybeResetToolChoice(agent, toolUseTracker, modelSettings) { if (agent.resetToolChoice && toolUseTracker.hasUsedTools(agent)) { return { ...modelSettings, toolChoice: undefined }; } return modelSettings; } /** * @internal * Continues a turn that was previously interrupted waiting for tool approval. Executes the now * approved tools and returns the resulting step transition. */ export async function resolveInterruptedTurn(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); // 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); if (pendingApprovalItems.length > 0) { const pendingApprovalIdentities = new Set(); for (const approval of pendingApprovalItems) { const identity = getApprovalIdentity(approval); if (identity) { pendingApprovalIdentities.add(identity); } } if (pendingApprovalIdentities.size > 0) { let rewindCount = 0; for (let index = originalPreStepItems.length - 1; index >= 0; index--) { const item = originalPreStepItems[index]; if (!(item instanceof RunToolApprovalItem)) { continue; } const identity = getApprovalIdentity(item); if (!identity) { continue; } if (!pendingApprovalIdentities.has(identity)) { continue; } rewindCount++; pendingApprovalIdentities.delete(identity); if (pendingApprovalIdentities.size === 0) { break; } } // 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. if (rewindCount > 0) { state._currentTurnPersistedItemCount = Math.max(0, state._currentTurnPersistedItemCount - rewindCount); } } } // 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); // There is no built-in HITL approval surface for computer tools today, so every pending action // is executed immediately when the turn resumes. const computerResults = processedResponse.computerActions.length > 0 ? await executeComputerActions(agent, processedResponse.computerActions, runner, state._context) : []; // When resuming we receive the original RunItem references; suppress duplicates so history and streaming do not double-emit the same items. const originalPreStepItemSet = new Set(originalPreStepItems); const newItems = []; const newItemsSet = new Set(); const appendIfNew = (item) => { if (originalPreStepItemSet.has(item) || newItemsSet.has(item)) { return; } newItems.push(item); newItemsSet.add(item); }; for (const result of functionResults) { appendIfNew(result.runItem); } for (const result of computerResults) { appendIfNew(result); } // 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'); }); // Hosted MCP approvals may still be waiting on a human decision when the turn resumes. const pendingHostedMCPApprovals = new Set(); const pendingHostedMCPApprovalIds = new Set(); // Keep track of approvals we still need to surface next turn so HITL flows can resume cleanly. for (const run of mcpApprovalRuns) { // the approval_request_id "mcpr_123..." const rawItem = run.requestItem.rawItem; if (rawItem.type !== 'hosted_tool_call') { continue; } const approvalRequestId = rawItem.id; const approved = state._context.isToolApproved({ // Since this item name must be the same with the one sent from Responses API server toolName: 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 const responseItem = new RunToolCallItem({ type: 'hosted_tool_call', name: 'mcp_approval_response', providerData, }, agent); appendIfNew(responseItem); } else { pendingHostedMCPApprovals.add(run.requestItem); pendingHostedMCPApprovalIds.add(approvalRequestId); functionResults.push({ type: 'hosted_mcp_tool_approval', tool: run.mcpTool, runItem: run.requestItem, }); appendIfNew(run.requestItem); } } // 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 RunToolApprovalItem)) { return true; } if (item.rawItem.type === 'hosted_tool_call' && item.rawItem.providerData?.type === 'mcp_approval_request') { if (pendingHostedMCPApprovals.has(item)) { return true; } const approvalRequestId = item.rawItem.id; if (approvalRequestId) { return pendingHostedMCPApprovalIds.has(approvalRequestId); } return false; } return false; }); const completedStep = await maybeCompleteTurnFromToolResults({ agent, runner, state, functionResults, originalInput, newResponse, preStepItems, newItems, }); if (completedStep) { return completedStep; } // 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 * 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. */ export async function resolveTurnAfterModelResponse(agent, originalInput, originalPreStepItems, newResponse, processedResponse, runner, state) { // 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 seenItems = new Set(originalPreStepItems); const newItems = []; const appendIfNew = (item) => { if (seenItems.has(item)) { return; } newItems.push(item); seenItems.add(item); }; 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([ executeFunctionToolCalls(agent, processedResponse.functions, runner, state), executeComputerActions(agent, processedResponse.computerActions, runner, state._context), executeShellActions(agent, processedResponse.shellActions, runner, state._context), executeApplyPatchOperations(agent, processedResponse.applyPatchActions, runner, state._context), ]); for (const result of functionResults) { appendIfNew(result.runItem); } for (const item of computerResults) { appendIfNew(item); } for (const item of shellResults) { appendIfNew(item); } for (const item of applyPatchResults) { appendIfNew(item); } // 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 completedStep = await maybeCompleteTurnFromToolResults({ agent, runner, state, functionResults, originalInput, newResponse, preStepItems, newItems, }); 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. const hadToolCallsOrActions = (processedResponse.functions?.length ?? 0) > 0 || (processedResponse.computerActions?.length ?? 0) > 0 || (processedResponse.shellActions?.length ?? 0) > 0 || (processedResponse.applyPatchActions?.length ?? 0) > 0 || (processedResponse.mcpApprovalRequests?.length ?? 0) > 0 || (processedResponse.handoffs?.length ?? 0) > 0; if (hadToolCallsOrActions) { return new 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 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 (typeof potentialFinalOutput === 'undefined') { return new 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 RunToolApprovalItem); if (!hasPendingToolsOrApprovals) { if (agent.outputType === 'text') { return new 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 } = getSchemaAndParserFromInputType(agent.outputType, 'final_output'); const [error] = await safeExecute(() => parser(potentialFinalOutput)); if (error) { const outputErrorMessage = formatFinalOutputTypeError(error); addErrorToCurrentSpan({ message: outputErrorMessage, data: { error: String(error), }, }); throw new ModelBehaviorError(outputErrorMessage); } 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' }); } // 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, state, functionResults, originalInput, newResponse, preStepItems, newItems, }) { const toolOutcome = await checkForFinalOutputFromTools(agent, functionResults, state); if (toolOutcome.isFinalOutput) { runner.emit('agent_end', state._context, agent, toolOutcome.finalOutput); agent.emit('agent_end', state._context, toolOutcome.finalOutput); return new SingleStepResult(originalInput, newResponse, preStepItems, newItems, { type: 'next_step_final_output', output: toolOutcome.finalOutput, }); } if (toolOutcome.isInterrupted) { return new SingleStepResult(originalInput, newResponse, preStepItems, newItems, { type: 'next_step_interruption', data: { interruptions: toolOutcome.interruptions, }, }); } return null; } /** * @internal * Normalizes tool outputs once so downstream code works with fully structured protocol items. * Doing this here keeps API surface stable even when providers add new shapes. */ export function getToolCallOutputItem(toolCall, output) { const maybeStructuredOutputs = normalizeStructuredToolOutputs(output); if (maybeStructuredOutputs) { const structuredItems = maybeStructuredOutputs.map(convertStructuredToolOutputToInputItem); return { type: 'function_call_result', name: toolCall.name, callId: toolCall.callId, status: 'completed', output: structuredItems, }; } return { type: 'function_call_result', name: toolCall.name, callId: toolCall.callId, status: 'completed', output: { type: 'text', text: toSmartString(output), }, }; } function normalizeFileValue(value) { const directFile = value.file; if (typeof directFile === 'string' && directFile.length > 0) { return directFile; } const normalizedObject = normalizeFileObjectCandidate(directFile); if (normalizedObject) { return normalizedObject; } const legacyValue = normalizeLegacyFileValue(value); if (legacyValue) { return legacyValue; } return null; } function normalizeFileObjectCandidate(value) { if (!isRecord(value)) { return null; } if ('data' in value && value.data !== undefined) { const dataValue = value.data; const hasStringData = typeof dataValue === 'string' && dataValue.length > 0; const hasBinaryData = dataValue instanceof Uint8Array && dataValue.length > 0; if (!hasStringData && !hasBinaryData) { return null; } if (!isNonEmptyString(value.mediaType) || !isNonEmptyString(value.filename)) { return null; } return { data: typeof dataValue === 'string' ? dataValue : new Uint8Array(dataValue), mediaType: value.mediaType, filename: value.filename, }; } if (isNonEmptyString(value.url)) { const result = { url: value.url }; if (isNonEmptyString(value.filename)) { result.filename = value.filename; } return result; } const referencedId = (isNonEmptyString(value.id) && value.id) || (isNonEmptyString(value.fileId) && value.fileId); if (referencedId) { const result = { id: referencedId }; if (isNonEmptyString(value.filename)) { result.filename = value.filename; } return result; } return null; } function normalizeLegacyFileValue(value) { const filename = typeof value.filename === 'string' && value.filename.length > 0 ? value.filename : undefined; const mediaType = typeof value.mediaType === 'string' && value.mediaType.length > 0 ? value.mediaType : undefined; if (typeof value.fileData === 'string' && value.fileData.length > 0) { if (!mediaType || !filename) { return null; } return { data: value.fileData, mediaType, filename }; } if (value.fileData instanceof Uint8Array && value.fileData.length > 0) { if (!mediaType || !filename) { return null; } return { data: new Uint8Array(value.fileData), mediaType, filename }; } if (typeof value.fileUrl === 'string' && value.fileUrl.length > 0) { const result = { url: value.fileUrl }; if (filename) { result.filename = filename; } return result; } if (typeof value.fileId === 'string' && value.fileId.length > 0) { const result = { id: value.fileId }; if (filename) { result.filename = filename; } return result; } return null; } function isRecord(value) { return typeof value === 'object' && value !== null; } function isNonEmptyString(value) { return typeof value === 'string' && value.length > 0; } function toInlineImageString(data, mediaType) { if (typeof data === 'string') { if (mediaType && !data.startsWith('data:')) { return asDataUrl(data, mediaType); } return data; } const base64 = encodeUint8ArrayToBase64(data); return asDataUrl(base64, mediaType); } function asDataUrl(base64, mediaType) { return mediaType ? `data:${mediaType};base64,${base64}` : base64; } /** * @internal * Runs every function tool call requested by the model and returns their outputs alongside * the `RunItem` instances that should be appended to history. */ 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); } } // Some tools require a human or policy check before execution; defer until approval is recorded. 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 toolOutput = await toolRun.tool.invoke(state._context, toolRun.toolCall.arguments, { toolCall: toolRun.toolCall }); // Use string data for tracing and event emitter const stringResult = toSmartString(toolOutput); 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; } const functionResult = { type: 'function_output', tool: toolRun.tool, output: toolOutput, runItem: new RunToolCallOutputItem(getToolCallOutputItem(toolRun.toolCall, toolOutput), agent, toolOutput), }; const nestedRunResult = consumeAgentToolRunResult(toolRun.toolCall); if (nestedRunResult) { functionResult.agentRunResult = nestedRunResult; const nestedInterruptions = nestedRunResult.interruptions; if (nestedInterruptions.length > 0) { functionResult.interruptions = nestedInterruptions; } } return functionResult; } catch (error) { span.setError({ message: 'Error running tool', data: { tool_name: toolRun.tool.name, error: String(error), }, }); // Emit agent_tool_end even on error to maintain consistent event lifecycle const errorResult = String(error); runner.emit('agent_tool_end', state._context, agent, toolRun.tool, errorResult, { toolCall: toolRun.toolCall, }); agent.emit('agent_tool_end', state._context, toolRun.tool, errorResult, { toolCall: toolRun.toolCall, }); 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()'); } function toErrorMessage(error) { if (error instanceof Error) { return error.message || error.toString(); } try { return JSON.stringify(error); } catch { return String(error); } } export async function executeShellActions(agent, actions, runner, runContext, customLogger = undefined) { const _logger = customLogger ?? logger; const results = []; for (const action of actions) { const shellTool = action.shell; const toolCall = action.toolCall; const approvalItem = new RunToolApprovalItem(toolCall, agent, shellTool.name); const requiresApproval = await shellTool.needsApproval(runContext, toolCall.action, toolCall.callId); if (requiresApproval) { if (shellTool.onApproval) { const decision = await shellTool.onApproval(runContext, approvalItem); if (decision.approve === true) { runContext.approveTool(approvalItem); } else if (decision.approve === false) { runContext.rejectTool(approvalItem); } } const approval = runContext.isToolApproved({ toolName: shellTool.name, callId: toolCall.callId, }); if (approval === false) { const response = 'Tool execution was not approved.'; const rejectionOutput = { stdout: '', stderr: response, outcome: { type: 'exit', exitCode: null }, }; results.push(new RunToolCallOutputItem({ type: 'shell_call_output', callId: toolCall.callId, output: [rejectionOutput], }, agent, response)); continue; } if (approval !== true) { results.push(approvalItem); continue; } } runner.emit('agent_tool_start', runContext, agent, shellTool, { toolCall, }); if (typeof agent.emit === 'function') { agent.emit('agent_tool_start', runContext, shellTool, { toolCall }); } let shellOutputs; const providerMeta = {}; let maxOutputLength; try { const shellResult = await shellTool.shell.run(toolCall.action); shellOutputs = shellResult.output ?? []; if (shellResult.providerData) { Object.assign(providerMeta, shellResult.providerData); } if (typeof shellResult.maxOutputLength === 'number') { maxOutputLength = shellResult.maxOutputLength; } } catch (err) { const errorText = toErrorMessage(err); shellOutputs = [ { stdout: '', stderr: errorText, outcome: { type: 'exit', exitCode: null }, }, ]; _logger.error('Failed to execute shell action:', err); } shellOutputs = shellOutputs ?? []; runner.emit('agent_tool_end', runContext, agent, shellTool, JSON.stringify(shellOutputs), { toolCall, }); if (typeof agent.emit === 'function') { agent.emit('agent_tool_end', runContext, shellTool, JSON.stringify(shellOutputs), { toolCall, }); } const rawItem = { type: 'shell_call_output', callId: toolCall.callId, output: shellOutputs ?? [], }; if (typeof maxOutputLength === 'number') { rawItem.maxOutputLength = maxOutputLength; } if (Object.keys(providerMeta).length > 0) { rawItem.providerData = providerMeta; } results.push(new RunToolCallOutputItem(rawItem, agent, rawItem.output)); } return results; } export async function executeApplyPatchOperations(agent, actions, runner, runContext, customLogger = undefined) { const _logger = customLogger ?? logger; const results = []; for (const action of actions) { const applyPatchTool = action.applyPatch; const toolCall = action.toolCall; const approvalItem = new RunToolApprovalItem(toolCall, agent, applyPatchTool.name); const requiresApproval = await applyPatchTool.needsApproval(runContext, toolCall.operation, toolCall.callId); if (requiresApproval) { if (applyPatchTool.onApproval) { const decision = await applyPatchTool.onApproval(runContext, approvalItem); if (decision.approve === true) { runContext.approveTool(approvalItem); } else if (decision.approve === false) { runContext.rejectTool(approvalItem); } } const approval = runContext.isToolApproved({ toolName: applyPatchTool.name, callId: toolCall.callId, }); if (approval === false) { const response = 'Tool execution was not approved.'; results.push(new RunToolCallOutputItem({ type: 'apply_patch_call_output', callId: toolCall.callId, status: 'failed', output: response, }, agent, response)); continue; } if (approval !== true) { results.push(approvalItem); continue; } } runner.emit('agent_tool_start', runContext, agent, applyPatchTool, { toolCall, }); if (typeof agent.emit === 'function') { agent.emit('agent_tool_start', runContext, applyPatchTool, { toolCall, }); } let status = 'completed'; let output = ''; try { let result; switch (toolCall.operation.type) { case 'create_file': result = await applyPatchTool.editor.createFile(toolCall.operation); break; case 'update_file': result = await applyPatchTool.editor.updateFile(toolCall.operation); break; case 'delete_file': result = await applyPatchTool.editor.deleteFile(toolCall.operation); break; default: throw new Error('Unsupported apply_patch operation'); } if (result && typeof result.status === 'string') { status = result.status; } if (result && typeof result.output === 'string') { output = result.output; } } catch (err) { status = 'failed'; output = toErrorMessage(err); _logger.error('Failed to execute apply_patch operation:', err); } runner.emit('agent_tool_end', runContext, agent, applyPatchTool, output, { toolCall, }); if (typeof agent.emit === 'function') { agent.emit('agent_tool_end', runContext, applyPatchTool, output, { toolCall, }); } const rawItem = { type: 'apply_patch_call_output', callId: toolCall.callId, status, }; if (output) { rawItem.output = output; } results.push(new RunToolCallOutputItem(rawItem, agent, output)); } return results; } /** * @internal * Executes any computer-use actions emitted by the model and returns the resulting items so the * run history reflects the computer session. */ export async function executeComputerActions(agent, actions, runner, runContext, customLogger = undefined) { const _logger = customLogger ?? logger; const results = []; for (const action of actions) { 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 { const computer = await resolveComputer({ tool: action.computer, runContext, }); output = await _runComputerActionAndScreenshot(computer, toolCall); } catch (err) { _logger.error('Failed to execute computer action:', err); output = '';