UNPKG

@openai/agents-core

Version:

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

583 lines • 28.9 kB
import { AgentHooks } from "./lifecycle.mjs"; import { getAllMcpTools, } from "./mcp.mjs"; import { getDefaultModelSettings, isGpt5OrNewerReasoningModel, isGpt5OrNewerDefault, } from "./defaultModel.mjs"; import { RunContext } from "./runContext.mjs"; import { tool, } from "./tool.mjs"; import { getHandoff } from "./handoff.mjs"; import { Runner } from "./run.mjs"; import { RunState } from "./runState.mjs"; import { getPublicAgent } from "./agentToolConfiguration.mjs"; import { selectModel } from "./runner/modelSettings.mjs"; import { toFunctionToolName } from "./utils/tools.mjs"; import { getOutputText } from "./utils/messages.mjs"; import { isZodObject } from "./utils/typeGuards.mjs"; import { combineAbortSignals } from "./utils/abortSignals.mjs"; import { ModelBehaviorError, UserError } from "./errors.mjs"; import logger from "./logger.mjs"; import { AgentAsToolInputSchema, buildStructuredInputSchemaInfo, resolveAgentToolInput, } from "./agentToolInput.mjs"; import { getToolCallParentSpanFromDetails, getAgentToolParentRunConfigFromDetails, getInheritedAgentToolRunConfig, mergeAgentToolRunConfig, } from "./agentToolRunConfig.mjs"; import { assertStandardSchemaObjectRoot, hasStandardSchemaMarker, isStandardSchemaWithJSON, standardSchemaToJsonSchema, validateStandardSchema, unsupportedStandardSchemaError, } from "./utils/standardSchema.mjs"; import { prepareOpenAIStrictToolSchema } from "./utils/strictToolSchema.mjs"; import { saveAgentToolRunResult } from "./agentToolRunResults.mjs"; import { registerAgentToolSourceAgent } from "./agentToolSourceRegistry.mjs"; import { getToolUsageRecorder, setRunnerParentUsageRecorder, } from "./runner/usageTracking.mjs"; import { setRunnerInvocationSpanParent } from "./runner/invocationContext.mjs"; import { createAgentToolStreamBuffer, drainAgentToolStream, setAgentToolStreamBuffer, } from "./runner/agentToolStream.mjs"; import { hasDefinitelyDifferentOutputTypes } from "./agentOutputTypeWarning.mjs"; function getInitialModelSettingsForAgentModel(model) { if (model === undefined || model === Agent.DEFAULT_MODEL_PLACEHOLDER) { return getDefaultModelSettings(); } if (typeof model === 'string' && model !== Agent.DEFAULT_MODEL_PLACEHOLDER) { return getDefaultModelSettings(model); } return {}; } /** * The class representing an AI agent configured with instructions, tools, guardrails, handoffs and more. * * We strongly recommend passing `instructions`, which is the "system prompt" for the agent. In * addition, you can pass `handoffDescription`, which is a human-readable description of the * agent, used when the agent is used inside tools/handoffs. * * Agents are generic on the context type. The context is a (mutable) object you create. It is * passed to tool functions, handoffs, guardrails, etc. */ export class Agent extends AgentHooks { /** * Create an Agent with handoffs and automatically infer the union type for TOutput from the handoff agents' output types. */ static create(config) { return new Agent({ ...config, handoffs: config.handoffs, outputType: config.outputType, handoffOutputTypeWarningEnabled: false, }); } static DEFAULT_MODEL_PLACEHOLDER = ''; name; instructions; prompt; handoffDescription; handoffs; model; modelSettings; tools; mcpServers; mcpConfig; inputGuardrails; outputGuardrails; outputType = 'text'; toolUseBehavior; resetToolChoice; _toolsExplicitlyConfigured; _modelSettingsExplicitlyConfigured; constructor(config) { super(); if (typeof config.name !== 'string' || config.name.trim() === '') { throw new UserError('Agent must have a name.'); } this.name = config.name; this.instructions = config.instructions ?? Agent.DEFAULT_MODEL_PLACEHOLDER; this.prompt = config.prompt; this.handoffDescription = config.handoffDescription ?? ''; this.handoffs = config.handoffs ?? []; this.model = config.model ?? ''; this._modelSettingsExplicitlyConfigured = config.modelSettings !== undefined; this.modelSettings = config.modelSettings ?? getInitialModelSettingsForAgentModel(config.model); this.tools = config.tools ?? []; this._toolsExplicitlyConfigured = config.tools !== undefined; this.mcpServers = config.mcpServers ?? []; this.mcpConfig = config.mcpConfig ?? {}; this.inputGuardrails = config.inputGuardrails ?? []; this.outputGuardrails = config.outputGuardrails ?? []; if (config.outputType) { if (!isZodObject(config.outputType) && hasStandardSchemaMarker(config.outputType) && !isStandardSchemaWithJSON(config.outputType)) { throw unsupportedStandardSchemaError(); } if (!isZodObject(config.outputType) && isStandardSchemaWithJSON(config.outputType)) { const inputSchema = standardSchemaToJsonSchema(config.outputType, 'input'); assertStandardSchemaObjectRoot(inputSchema, 'Agent output'); prepareOpenAIStrictToolSchema(inputSchema); } this.outputType = config.outputType; } this.toolUseBehavior = config.toolUseBehavior ?? 'run_llm_again'; this.resetToolChoice = config.resetToolChoice ?? true; if ( // The user sets a non-default model. config.model !== undefined && config.model !== Agent.DEFAULT_MODEL_PLACEHOLDER && // The default model uses GPT-5-and-newer settings. isGpt5OrNewerDefault() && // The specified model is outside that settings family. (typeof config.model !== 'string' || !isGpt5OrNewerReasoningModel(config.model)) && // The model settings are not customized for the specified model. config.modelSettings === undefined) { // Avoid applying reasoning-family defaults to an unrelated model. this.modelSettings = {}; } // --- Runtime warning for handoff output type compatibility --- if (config.handoffOutputTypeWarningEnabled === undefined || config.handoffOutputTypeWarningEnabled) { if (this.handoffs && this.outputType) { const outputTypes = [this.outputType]; for (const h of this.handoffs) { if ('outputType' in h && h.outputType) { outputTypes.push(h.outputType); } else if ('agent' in h && h.agent.outputType) { outputTypes.push(h.agent.outputType); } } if (logger.dontLogModelData) { if (hasDefinitelyDifferentOutputTypes(outputTypes)) { logger.warn('[Agent] Warning: Handoff agents have different output types. Output type details are redacted. You can make it type-safe by using Agent.create({ ... }) method instead.'); } } else { const serializedOutputTypes = outputTypes.map((type) => JSON.stringify(type)); if (new Set(serializedOutputTypes).size > 1) { logger.warn(`[Agent] Warning: Handoff agents have different output types: ${serializedOutputTypes.join(', ')}. You can make it type-safe by using Agent.create({ ... }) method instead.`); } } } } } /** @internal */ hasExplicitModelSettings() { return this._modelSettingsExplicitlyConfigured; } /** * Output schema name. */ get outputSchemaName() { if (this.outputType === 'text') { return 'text'; } else if (isZodObject(this.outputType)) { return 'ZodOutput'; } else if (isStandardSchemaWithJSON(this.outputType)) { return 'StandardSchemaOutput'; } else if (typeof this.outputType === 'object') { return this.outputType.name; } throw new Error(`Unknown output type: ${this.outputType}`); } /** * Makes a copy of the agent, with the given arguments changed. For example, you could do: * * ``` * const newAgent = agent.clone({ instructions: 'New instructions' }) * ``` * * This method never copies a list property such as `tools`, `handoffs`, `mcpServers`, * `inputGuardrails`, or `outputGuardrails`. It merges `config` over this agent's current * property values and constructs a new agent from the result, so each list is whatever that * merged configuration holds. Two consequences follow: * * - A property `config` leaves out arrives as the original agent's own array, which both agents * then share, entries included. * - A property `config` includes is taken exactly as given, so it shares an array or an entry * with the original agent only where you reused one. Including the property with the value * `undefined` still counts as including it, and the new agent starts that list empty instead * of inheriting it. * * Because two agents can hold one array, mutating it through either one also changes the other. * After `const clonedAgent = agent.clone({ name: 'Clone' })`, calling * `clonedAgent.tools.push(extraTool)` adds that tool to `agent` as well. To give the clone a * list that no other agent holds, pass a new array: * * ``` * const newAgent = agent.clone({ tools: [...agent.tools, extraTool] }) * ``` * * The entries copied into that new array, such as tool and handoff objects, remain the same * objects the original agent holds. * * @param config - A partial configuration to change. * @returns A new agent with the given changes. */ clone(config) { const modelSettings = 'modelSettings' in config ? config.modelSettings : this.hasExplicitModelSettings() ? this.modelSettings : undefined; const cloned = new Agent({ ...this, ...config, modelSettings, }); if (!('modelSettings' in config) && !this.hasExplicitModelSettings() && Object.prototype.hasOwnProperty.call(this.modelSettings, 'store')) { // Preserve storage policy without making generated model defaults explicit. cloned.modelSettings.store = this.modelSettings.store; } return cloned; } asTool(options) { const { toolName, toolDescription, customOutputExtractor, needsApproval, parameters, inputBuilder, includeInputSchema, runConfig, runOptions, resumeState, isEnabled, onStream, onStreamMaxPendingEvents = 1024, } = options; if (onStreamMaxPendingEvents !== null && (!Number.isSafeInteger(onStreamMaxPendingEvents) || onStreamMaxPendingEvents <= 0)) { throw new UserError('onStreamMaxPendingEvents must be a positive safe integer or null.'); } // Event handlers are scoped to this agent tool instance and are not shared; we only support registration (no removal) to keep the API surface small. const eventHandlers = new Map(); const emitEvent = async (event) => { // We intentionally keep only add semantics (no off) to reduce surface area; handlers are scoped to this agent tool instance. const specific = eventHandlers.get(event.event.type); const wildcard = eventHandlers.get('*'); const candidates = [ ...(onStream ? [onStream] : []), ...(specific ? Array.from(specific) : []), ...(wildcard ? Array.from(wildcard) : []), ]; // Run all handlers in parallel so a slow onStream callback does not block on(...) handlers (and vice versa). await Promise.allSettled(candidates.map((handler) => Promise.resolve().then(() => handler(event)))); }; const resolvedToolName = toolName ?? toFunctionToolName(this.name); const toolParameters = (parameters ?? AgentAsToolInputSchema); const hasCustomParameters = typeof parameters !== 'undefined'; const includeSchema = includeInputSchema === true && hasCustomParameters; const shouldCaptureToolInput = hasCustomParameters || includeSchema || typeof inputBuilder === 'function'; const schemaInfo = shouldCaptureToolInput ? buildStructuredInputSchemaInfo(toolParameters, resolvedToolName, includeSchema) : undefined; const baseTool = tool({ name: resolvedToolName, description: toolDescription ?? '', parameters: toolParameters, strict: true, needsApproval: needsApproval, isEnabled, execute: async (params, context, details) => { const typedParams = params; const runContextBase = runOptions?.context instanceof RunContext ? runOptions.context : typeof runOptions?.context !== 'undefined' ? new RunContext(runOptions.context) : context instanceof RunContext ? context : typeof context !== 'undefined' ? new RunContext(context) : new RunContext(); const agentToolInvocation = { toolName: details?.toolCall?.name ?? baseTool.name, toolCallId: details?.toolCall?.callId, toolArguments: details?.toolCall?.arguments, }; const shouldClearToolInput = !shouldCaptureToolInput && typeof runContextBase.toolInput !== 'undefined'; const runContext = shouldCaptureToolInput && typeof runContextBase._forkWithToolInput === 'function' ? runContextBase._forkWithToolInput(typedParams) : shouldClearToolInput && typeof runContextBase._forkWithoutToolInput === 'function' ? runContextBase._forkWithoutToolInput() : runContextBase; const resolvedInput = await resolveAgentToolInput({ params: typedParams, schemaInfo, inputBuilder, }); if (typeof resolvedInput !== 'string' && !Array.isArray(resolvedInput)) { throw new ModelBehaviorError('Agent tool called with invalid input'); } const inheritedRunConfig = getInheritedAgentToolRunConfig(getAgentToolParentRunConfigFromDetails(details), runConfig, selectModel(this.model, runConfig?.model)); const nestedRunConfig = mergeAgentToolRunConfig(inheritedRunConfig, runConfig); const runner = new Runner(nestedRunConfig); setRunnerParentUsageRecorder(runner, getToolUsageRecorder(details)); setRunnerInvocationSpanParent(runner, getToolCallParentSpanFromDetails(details)); const resumeContextStrategy = resumeState?.contextStrategy ?? 'merge'; const resumeContext = resumeContextStrategy === 'preferSerialized' ? undefined : runContext; let runInput = resolvedInput; if (details?.resumeState) { if (resumeContextStrategy === 'preferSerialized' || !resumeContext) { runInput = await RunState.fromString(this, details.resumeState); } else { if (resumeContextStrategy === 'merge' && context && resumeContext !== context) { resumeContext._mergeApprovalState(context); } runInput = await RunState.fromStringWithContext(this, details.resumeState, resumeContext, { contextStrategy: resumeContextStrategy === 'replace' ? 'replace' : 'merge', }); } } // Only flip to streaming mode when a handler is provided to avoid extra overhead for callers that do not need events. // Flip to streaming if either a legacy onStream callback or event handlers are registered; otherwise stay on the non-stream path to avoid extra overhead. const shouldStream = typeof onStream === 'function' || eventHandlers.size > 0; const configuredSignal = runOptions?.signal; const toolCallSignal = details?.signal; const streamController = shouldStream ? new AbortController() : undefined; if (streamController && onStreamMaxPendingEvents !== null) { setAgentToolStreamBuffer(runner, createAgentToolStreamBuffer(onStreamMaxPendingEvents, streamController)); } const { signal: combinedSignal, cleanup: cleanupSignalListeners } = streamController || (configuredSignal && toolCallSignal) ? combineAbortSignals(configuredSignal, toolCallSignal, streamController?.signal) : { signal: toolCallSignal ?? configuredSignal, cleanup: () => { }, }; const runOptionsWithContext = { ...(runOptions ?? {}), context: runContext, ...(combinedSignal ? { signal: combinedSignal } : {}), }; try { const result = shouldStream ? await runner.run(this, runInput, { ...runOptionsWithContext, stream: true, }) : await runner.run(this, runInput, { ...runOptionsWithContext, }); const streamPayload = { agent: this, toolCall: details?.toolCall, }; if (shouldStream) { // Cast through unknown: the async iterator shape matches and we want to drain the stream for side effects while keeping the public API stable. const streamResult = result; // Drain the stream to deliver every event to registered handlers; ensure completion awaited so the nested run finishes before returning. await drainAgentToolStream(streamResult, (event) => emitEvent({ event, ...streamPayload }), combinedSignal, streamController); if (streamResult.cancelled) { const currentStep = streamResult.state._currentStep; const hasCommittedOutcome = (currentStep?.type === 'next_step_final_output' && streamResult.state._currentTurnInProgress === false) || (streamResult.interruptions?.length ?? 0) > 0; if (!hasCommittedOutcome) { combinedSignal?.throwIfAborted(); throw new Error('Nested agent run was cancelled.'); } } } const completedResult = result; if (completedResult.state instanceof RunState) { completedResult.state._agentToolInvocation = agentToolInvocation; } const completedResultWithAgentToolInvocation = completedResult; const usesStopAtToolNames = typeof this.toolUseBehavior === 'object' && this.toolUseBehavior !== null && 'stopAtToolNames' in this.toolUseBehavior; if (typeof customOutputExtractor !== 'function' && usesStopAtToolNames) { logger.debug(`You're passing the agent (name: ${this.name}) with toolUseBehavior.stopAtToolNames configured as a tool to a different agent; this may not work as you expect. You may want to have a wrapper function tool to consistently return the final output.`); } let outputText; if (typeof customOutputExtractor === 'function') { outputText = await customOutputExtractor(completedResultWithAgentToolInvocation); } else if ((completedResult.interruptions?.length ?? 0) > 0) { outputText = ''; } else { const outputGuardrailResult = completedResult.outputGuardrailResults?.at(-1); // Reuse the checked value; reading finalOutput can rerun schema transforms. const finalOutput = outputGuardrailResult ? outputGuardrailResult.agentOutput : completedResult.finalOutput; const finalOutputType = outputGuardrailResult ? outputGuardrailResult.agent.outputType : this.outputType; const finalOutputText = typeof finalOutput !== 'undefined' ? finalOutputType === 'text' ? String(finalOutput) : JSON.stringify(finalOutput) : undefined; const rawResponses = completedResult.rawResponses; const rawOutputText = rawResponses && rawResponses.length > 0 ? getOutputText(rawResponses[rawResponses.length - 1]) : undefined; const normalizedRawOutputText = typeof rawOutputText === 'string' && rawOutputText.trim() === '' ? undefined : rawOutputText; const prefersFinalOutput = completedResult.state?._finalOutputSource === 'error_handler' || outputGuardrailResult !== undefined; outputText = prefersFinalOutput ? (finalOutputText ?? normalizedRawOutputText ?? '') : (normalizedRawOutputText ?? finalOutputText ?? ''); } if (details?.toolCall) { saveAgentToolRunResult(details.toolCall, completedResultWithAgentToolInvocation); } return outputText; } finally { cleanupSignalListeners(); } }, }); const agentTool = { ...baseTool, on: (name, handler) => { const set = eventHandlers.get(name) ?? new Set(); set.add(handler); eventHandlers.set(name, set); return agentTool; }, }; registerAgentToolSourceAgent(agentTool, this); return agentTool; } /** * Returns the system prompt for the agent. * * If the agent has a function as its instructions, this function will be called with the * runContext and the agent instance. */ async getSystemPrompt(runContext) { if (typeof this.instructions === 'function') { return await this.instructions(runContext, this); } return this.instructions; } /** * Returns the prompt template for the agent, if defined. * * If the agent has a function as its prompt, this function will be called with the * runContext and the agent instance. */ async getPrompt(runContext) { if (typeof this.prompt === 'function') { return await this.prompt(runContext, this); } return this.prompt; } /** * Fetches the available tools from the MCP servers. * @returns the MCP powered tools */ async getMcpTools(runContext, tracingParent) { if (this.mcpServers.length > 0) { const includeServerInToolNames = this.mcpConfig.includeServerInToolNames === true; return getAllMcpTools({ mcpServers: this.mcpServers, runContext, agent: getPublicAgent(this), convertSchemasToStrict: this.mcpConfig.convertSchemasToStrict === true, errorFunction: this.mcpConfig.errorFunction, includeServerInToolNames, reservedToolNames: includeServerInToolNames ? await this.getMcpToolReservedNames(runContext) : undefined, tracingParent, }); } return []; } async getMcpToolReservedNames(runContext) { const reservedToolNames = new Set(this.tools .filter((tool) => tool.type === 'function') .map((tool) => tool.name)); const enabledHandoffs = await this.getEnabledHandoffs(runContext); for (const handoff of enabledHandoffs) { reservedToolNames.add(handoff.toolName); } return reservedToolNames; } /** * All agent tools, including the MCP and function tools. * * @returns all configured tools */ async getAllTools(runContext, tracingParent) { const mcpTools = await this.getMcpTools(runContext, tracingParent); const enabledTools = []; for (const candidate of [...this.tools]) { if (candidate.type === 'function') { const maybeIsEnabled = candidate.isEnabled; const enabled = typeof maybeIsEnabled === 'function' ? await maybeIsEnabled(runContext, getPublicAgent(this)) : typeof maybeIsEnabled === 'boolean' ? maybeIsEnabled : true; if (!enabled) { continue; } } enabledTools.push(candidate); } return [...mcpTools, ...enabledTools]; } hasExplicitToolConfig() { return this._toolsExplicitlyConfigured; } /** * Returns the handoffs that should be exposed to the model for the current run. * * Handoffs that provide an `isEnabled` function returning `false` are omitted. */ async getEnabledHandoffs(runContext) { const handoffs = this.handoffs?.map((h) => getHandoff(h)) ?? []; const enabled = []; for (const handoff of handoffs) { if (await handoff.isEnabled({ runContext, agent: this })) { enabled.push(handoff); } } return enabled; } /** * Processes the final output of the agent. * * @param output - The output of the agent. * @returns The parsed out. */ processFinalOutput(output) { if (this.outputType === 'text') { return output; } if (typeof this.outputType === 'object' || typeof this.outputType === 'function') { const parsed = JSON.parse(output); if (isZodObject(this.outputType)) { return this.outputType.parse(parsed); } if (isStandardSchemaWithJSON(this.outputType)) { const inputSchema = standardSchemaToJsonSchema(this.outputType, 'input'); assertStandardSchemaObjectRoot(inputSchema, 'Agent output'); const normalized = prepareOpenAIStrictToolSchema(inputSchema).normalizeInput(parsed); return validateStandardSchema(this.outputType, normalized); } if (hasStandardSchemaMarker(this.outputType)) { throw unsupportedStandardSchemaError(); } return parsed; } throw new Error(`Unknown output type: ${this.outputType}`); } /** * Returns a JSON representation of the agent, which is serializable. * * @returns A JSON object containing the agent's name. */ toJSON() { return { name: this.name, }; } } //# sourceMappingURL=agent.mjs.map