UNPKG

@openai/agents-core

Version:

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

910 lines 64.5 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Runner = void 0; exports.run = run; exports.getTurnInput = getTurnInput; exports.selectModel = selectModel; exports.getTracing = getTracing; const agent_1 = require("./agent.js"); const guardrail_1 = require("./guardrail.js"); const providers_1 = require("./providers.js"); const runContext_1 = require("./runContext.js"); const result_1 = require("./result.js"); const lifecycle_1 = require("./lifecycle.js"); const logger_1 = __importDefault(require("./logger.js")); const serialize_1 = require("./utils/serialize.js"); const errors_1 = require("./errors.js"); const runImplementation_1 = require("./runImplementation.js"); const context_1 = require("./tracing/context.js"); const tracing_1 = require("./tracing/index.js"); const usage_1 = require("./usage.js"); const events_1 = require("./events.js"); const runState_1 = require("./runState.js"); const protocol_1 = require("./types/protocol.js"); const tools_1 = require("./utils/tools.js"); const defaultModel_1 = require("./defaultModel.js"); const base64_1 = require("./utils/base64.js"); const smartString_1 = require("./utils/smartString.js"); async function run(agent, input, options) { const runner = getDefaultRunner(); if (options?.stream) { return await runner.run(agent, input, options); } else { return await runner.run(agent, input, options); } } /** * Orchestrates agent execution, including guardrails, tool calls, session persistence, and * tracing. Reuse a `Runner` instance when you want consistent configuration across multiple runs. */ class Runner extends lifecycle_1.RunHooks { config; /** * Creates a runner with optional defaults that apply to every subsequent run invocation. * * @param config - Overrides for models, guardrails, tracing, or session behavior. */ constructor(config = {}) { super(); this.config = { modelProvider: config.modelProvider ?? (0, providers_1.getDefaultModelProvider)(), model: config.model, modelSettings: config.modelSettings, handoffInputFilter: config.handoffInputFilter, inputGuardrails: config.inputGuardrails, outputGuardrails: config.outputGuardrails, tracingDisabled: config.tracingDisabled ?? false, traceIncludeSensitiveData: config.traceIncludeSensitiveData ?? true, workflowName: config.workflowName ?? 'Agent workflow', traceId: config.traceId, groupId: config.groupId, traceMetadata: config.traceMetadata, sessionInputCallback: config.sessionInputCallback, callModelInputFilter: config.callModelInputFilter, }; this.inputGuardrailDefs = (config.inputGuardrails ?? []).map(guardrail_1.defineInputGuardrail); this.outputGuardrailDefs = (config.outputGuardrails ?? []).map(guardrail_1.defineOutputGuardrail); } async run(agent, input, options = { stream: false, context: undefined, }) { const resolvedOptions = options ?? { stream: false, context: undefined }; // Per-run options take precedence over runner defaults for session memory behavior. const sessionInputCallback = resolvedOptions.sessionInputCallback ?? this.config.sessionInputCallback; // Likewise allow callers to override callModelInputFilter on individual runs. const callModelInputFilter = resolvedOptions.callModelInputFilter ?? this.config.callModelInputFilter; const hasCallModelInputFilter = Boolean(callModelInputFilter); const effectiveOptions = { ...resolvedOptions, sessionInputCallback, callModelInputFilter, }; const serverManagesConversation = Boolean(effectiveOptions.conversationId) || Boolean(effectiveOptions.previousResponseId); // When the server tracks conversation history we defer to it for previous turns so local session // persistence can focus solely on the new delta being generated in this process. const session = effectiveOptions.session; const resumingFromState = input instanceof runState_1.RunState; let sessionInputOriginalSnapshot = session && resumingFromState ? [] : undefined; let sessionInputFilteredSnapshot = undefined; // Tracks remaining persistence slots per AgentInputItem key so resumed sessions only write each original occurrence once. let sessionInputPendingWriteCounts = session && resumingFromState ? new Map() : undefined; // Keeps track of which inputs should be written back to session memory. `sourceItems` reflects // the original objects (so we can respect resume counts) while `filteredItems`, when present, // contains the filtered/redacted clones that must be persisted for history. // The helper reconciles the filtered copies produced by callModelInputFilter with their original // counterparts so resume-from-state bookkeeping stays consistent and duplicate references only // consume a single persistence slot. const recordSessionItemsForPersistence = (sourceItems, filteredItems) => { const pendingWriteCounts = sessionInputPendingWriteCounts; if (filteredItems !== undefined) { if (!pendingWriteCounts) { sessionInputFilteredSnapshot = filteredItems.map((item) => structuredClone(item)); return; } const persistableItems = []; const sourceOccurrenceCounts = new WeakMap(); // Track how many times each original object appears so duplicate references only consume one persistence slot. for (const source of sourceItems) { if (!source || typeof source !== 'object') { continue; } const nextCount = (sourceOccurrenceCounts.get(source) ?? 0) + 1; sourceOccurrenceCounts.set(source, nextCount); } // Let filtered items without a one-to-one source match claim any remaining persistence count. const consumeAnyPendingWriteSlot = () => { for (const [key, remaining] of pendingWriteCounts) { if (remaining > 0) { pendingWriteCounts.set(key, remaining - 1); return true; } } return false; }; for (let i = 0; i < filteredItems.length; i++) { const filteredItem = filteredItems[i]; if (!filteredItem) { continue; } let allocated = false; const source = sourceItems[i]; if (source && typeof source === 'object') { const pendingOccurrences = (sourceOccurrenceCounts.get(source) ?? 0) - 1; sourceOccurrenceCounts.set(source, pendingOccurrences); if (pendingOccurrences > 0) { continue; } const sourceKey = getAgentInputItemKey(source); const remaining = pendingWriteCounts.get(sourceKey) ?? 0; if (remaining > 0) { pendingWriteCounts.set(sourceKey, remaining - 1); persistableItems.push(structuredClone(filteredItem)); allocated = true; continue; } } const filteredKey = getAgentInputItemKey(filteredItem); const filteredRemaining = pendingWriteCounts.get(filteredKey) ?? 0; if (filteredRemaining > 0) { pendingWriteCounts.set(filteredKey, filteredRemaining - 1); persistableItems.push(structuredClone(filteredItem)); allocated = true; continue; } if (!source && consumeAnyPendingWriteSlot()) { persistableItems.push(structuredClone(filteredItem)); allocated = true; } if (!allocated && !source && sessionInputFilteredSnapshot === undefined) { // Preserve at least one copy so later persistence resolves even when no counters remain. persistableItems.push(structuredClone(filteredItem)); } } if (persistableItems.length > 0 || sessionInputFilteredSnapshot === undefined) { sessionInputFilteredSnapshot = persistableItems; } return; } const filtered = []; if (!pendingWriteCounts) { for (const item of sourceItems) { if (!item) { continue; } filtered.push(structuredClone(item)); } } else { for (const item of sourceItems) { if (!item) { continue; } const key = getAgentInputItemKey(item); const remaining = pendingWriteCounts.get(key) ?? 0; if (remaining <= 0) { continue; } pendingWriteCounts.set(key, remaining - 1); filtered.push(structuredClone(item)); } } if (filtered.length > 0) { sessionInputFilteredSnapshot = filtered; } else if (sessionInputFilteredSnapshot === undefined) { sessionInputFilteredSnapshot = []; } }; // Determine which items should be committed to session memory for this turn. // Filters take precedence because they reflect the exact payload delivered to the model. const resolveSessionItemsForPersistence = () => { if (sessionInputFilteredSnapshot !== undefined) { return sessionInputFilteredSnapshot; } if (hasCallModelInputFilter) { return undefined; } return sessionInputOriginalSnapshot; }; let preparedInput = input; if (!(preparedInput instanceof runState_1.RunState)) { if (session && Array.isArray(preparedInput) && !sessionInputCallback) { throw new errors_1.UserError('RunConfig.sessionInputCallback must be provided when using session history with list inputs.'); } const prepared = await (0, runImplementation_1.prepareInputItemsWithSession)(preparedInput, session, sessionInputCallback, { // When the server tracks conversation state we only send the new turn inputs; // previous messages are recovered via conversationId/previousResponseId. includeHistoryInPreparedInput: !serverManagesConversation, preserveDroppedNewItems: serverManagesConversation, }); if (serverManagesConversation && session) { // When the server manages memory we only persist the new turn inputs locally so the // conversation service stays the single source of truth for prior exchanges. const sessionItems = prepared.sessionItems; if (sessionItems && sessionItems.length > 0) { preparedInput = sessionItems; } else { preparedInput = prepared.preparedInput; } } else { preparedInput = prepared.preparedInput; } if (session) { const items = prepared.sessionItems ?? []; // Clone the items that will be persisted so later mutations (filters, hooks) cannot desync history. sessionInputOriginalSnapshot = items.map((item) => structuredClone(item)); // Reset pending counts so each prepared item reserves exactly one write slot until filters resolve matches. sessionInputPendingWriteCounts = new Map(); for (const item of items) { const key = getAgentInputItemKey(item); sessionInputPendingWriteCounts.set(key, (sessionInputPendingWriteCounts.get(key) ?? 0) + 1); } } } // Streaming runs persist the input asynchronously, so track a one-shot helper // that can be awaited from multiple branches without double-writing. let ensureStreamInputPersisted; // Sessions remain usable alongside server-managed conversations (e.g., OpenAIConversationsSession) // so callers can reuse callbacks, resume-from-state logic, and other helpers without duplicating // remote history, so persistence is gated on serverManagesConversation. if (session && !serverManagesConversation) { let persisted = false; ensureStreamInputPersisted = async () => { if (persisted) { return; } const itemsToPersist = resolveSessionItemsForPersistence(); if (!itemsToPersist || itemsToPersist.length === 0) { return; } persisted = true; await (0, runImplementation_1.saveStreamInputToSession)(session, itemsToPersist); }; } const executeRun = async () => { if (effectiveOptions.stream) { const streamResult = await this.#runIndividualStream(agent, preparedInput, effectiveOptions, ensureStreamInputPersisted, recordSessionItemsForPersistence); return streamResult; } const runResult = await this.#runIndividualNonStream(agent, preparedInput, effectiveOptions, recordSessionItemsForPersistence); // See note above: allow sessions to run for callbacks/state but skip writes when the server // is the source of truth for transcript history. if (session && !serverManagesConversation) { await (0, runImplementation_1.saveToSession)(session, resolveSessionItemsForPersistence(), runResult); } return runResult; }; if (preparedInput instanceof runState_1.RunState && preparedInput._trace) { return (0, context_1.withTrace)(preparedInput._trace, async () => { if (preparedInput._currentAgentSpan) { (0, context_1.setCurrentSpan)(preparedInput._currentAgentSpan); } return executeRun(); }); } return (0, context_1.getOrCreateTrace)(async () => executeRun(), { traceId: this.config.traceId, name: this.config.workflowName, groupId: this.config.groupId, metadata: this.config.traceMetadata, }); } // -------------------------------------------------------------- // Internals // -------------------------------------------------------------- inputGuardrailDefs; outputGuardrailDefs; /** * @internal * Resolves the effective model once so both run loops obey the same precedence rules. */ async #resolveModelForAgent(agent) { const explictlyModelSet = (agent.model !== undefined && agent.model !== agent_1.Agent.DEFAULT_MODEL_PLACEHOLDER) || (this.config.model !== undefined && this.config.model !== agent_1.Agent.DEFAULT_MODEL_PLACEHOLDER); let resolvedModel = selectModel(agent.model, this.config.model); if (typeof resolvedModel === 'string') { resolvedModel = await this.config.modelProvider.getModel(resolvedModel); } return { model: resolvedModel, explictlyModelSet }; } /** * @internal */ async #runIndividualNonStream(startingAgent, input, options, // sessionInputUpdate lets the caller adjust queued session items after filters run so we // persist exactly what we send to the model (e.g., after redactions or truncation). sessionInputUpdate) { return (0, context_1.withNewSpanContext)(async () => { // if we have a saved state we use that one, otherwise we create a new one const isResumedState = input instanceof runState_1.RunState; const state = isResumedState ? input : new runState_1.RunState(options.context instanceof runContext_1.RunContext ? options.context : new runContext_1.RunContext(options.context), input, startingAgent, options.maxTurns ?? DEFAULT_MAX_TURNS); const serverConversationTracker = options.conversationId || options.previousResponseId ? new ServerConversationTracker({ conversationId: options.conversationId, previousResponseId: options.previousResponseId, }) : undefined; if (serverConversationTracker && isResumedState) { serverConversationTracker.primeFromState({ originalInput: state._originalInput, generatedItems: state._generatedItems, modelResponses: state._modelResponses, }); } try { while (true) { // if we don't have a current step, we treat this as a new run state._currentStep = state._currentStep ?? { type: 'next_step_run_again', }; if (state._currentStep.type === 'next_step_interruption') { logger_1.default.debug('Continuing from interruption'); if (!state._lastTurnResponse || !state._lastProcessedResponse) { throw new errors_1.UserError('No model response found in previous state', state); } const turnResult = await (0, runImplementation_1.resolveInterruptedTurn)(state._currentAgent, state._originalInput, state._generatedItems, state._lastTurnResponse, state._lastProcessedResponse, this, state); state._toolUseTracker.addToolUse(state._currentAgent, state._lastProcessedResponse.toolsUsed); state._originalInput = turnResult.originalInput; state._generatedItems = turnResult.generatedItems; if (turnResult.nextStep.type === 'next_step_run_again') { state._currentTurnPersistedItemCount = 0; } state._currentStep = turnResult.nextStep; if (turnResult.nextStep.type === 'next_step_interruption') { // we are still in an interruption, so we need to avoid an infinite loop return new result_1.RunResult(state); } continue; } if (state._currentStep.type === 'next_step_run_again') { const artifacts = await prepareAgentArtifacts(state); state._currentTurn++; state._currentTurnPersistedItemCount = 0; if (state._currentTurn > state._maxTurns) { state._currentAgentSpan?.setError({ message: 'Max turns exceeded', data: { max_turns: state._maxTurns }, }); throw new errors_1.MaxTurnsExceededError(`Max turns (${state._maxTurns}) exceeded`, state); } logger_1.default.debug(`Running agent ${state._currentAgent.name} (turn ${state._currentTurn})`); if (state._currentTurn === 1) { await this.#runInputGuardrails(state); } const turnInput = serverConversationTracker ? serverConversationTracker.prepareInput(state._originalInput, state._generatedItems) : getTurnInput(state._originalInput, state._generatedItems); if (state._noActiveAgentRun) { state._currentAgent.emit('agent_start', state._context, state._currentAgent); this.emit('agent_start', state._context, state._currentAgent); } const preparedCall = await this.#prepareModelCall(state, options, artifacts, turnInput, serverConversationTracker, sessionInputUpdate); state._lastTurnResponse = await preparedCall.model.getResponse({ systemInstructions: preparedCall.modelInput.instructions, prompt: preparedCall.prompt, // Explicit agent/run config models should take precedence over prompt defaults. ...(preparedCall.explictlyModelSet ? { overridePromptModel: true } : {}), input: preparedCall.modelInput.input, previousResponseId: preparedCall.previousResponseId, conversationId: preparedCall.conversationId, modelSettings: preparedCall.modelSettings, tools: preparedCall.serializedTools, outputType: (0, tools_1.convertAgentOutputTypeToSerializable)(state._currentAgent.outputType), handoffs: preparedCall.serializedHandoffs, tracing: getTracing(this.config.tracingDisabled, this.config.traceIncludeSensitiveData), signal: options.signal, }); state._modelResponses.push(state._lastTurnResponse); state._context.usage.add(state._lastTurnResponse.usage); state._noActiveAgentRun = false; // After each turn record the items echoed by the server so future requests only // include the incremental inputs that have not yet been acknowledged. serverConversationTracker?.trackServerItems(state._lastTurnResponse); const processedResponse = (0, runImplementation_1.processModelResponse)(state._lastTurnResponse, state._currentAgent, preparedCall.tools, preparedCall.handoffs); state._lastProcessedResponse = processedResponse; const turnResult = await (0, runImplementation_1.resolveTurnAfterModelResponse)(state._currentAgent, state._originalInput, state._generatedItems, state._lastTurnResponse, state._lastProcessedResponse, this, state); state._toolUseTracker.addToolUse(state._currentAgent, state._lastProcessedResponse.toolsUsed); state._originalInput = turnResult.originalInput; state._generatedItems = turnResult.generatedItems; if (turnResult.nextStep.type === 'next_step_run_again') { state._currentTurnPersistedItemCount = 0; } state._currentStep = turnResult.nextStep; } if (state._currentStep && state._currentStep.type === 'next_step_final_output') { await this.#runOutputGuardrails(state, state._currentStep.output); this.emit('agent_end', state._context, state._currentAgent, state._currentStep.output); state._currentAgent.emit('agent_end', state._context, state._currentStep.output); return new result_1.RunResult(state); } else if (state._currentStep && state._currentStep.type === 'next_step_handoff') { state._currentAgent = state._currentStep.newAgent; if (state._currentAgentSpan) { state._currentAgentSpan.end(); (0, context_1.resetCurrentSpan)(); state._currentAgentSpan = undefined; } state._noActiveAgentRun = true; // we've processed the handoff, so we need to run the loop again state._currentStep = { type: 'next_step_run_again' }; } else if (state._currentStep && state._currentStep.type === 'next_step_interruption') { // interrupted. Don't run any guardrails return new result_1.RunResult(state); } else { logger_1.default.debug('Running next loop'); } } } catch (err) { if (state._currentAgentSpan) { state._currentAgentSpan.setError({ message: 'Error in agent run', data: { error: String(err) }, }); } throw err; } finally { if (state._currentAgentSpan) { if (state._currentStep?.type !== 'next_step_interruption') { // don't end the span if the run was interrupted state._currentAgentSpan.end(); } (0, context_1.resetCurrentSpan)(); } } }); } /** * @internal */ async #runStreamLoop(result, options, isResumedState, ensureStreamInputPersisted, sessionInputUpdate) { const serverManagesConversation = Boolean(options.conversationId) || Boolean(options.previousResponseId); const serverConversationTracker = serverManagesConversation ? new ServerConversationTracker({ conversationId: options.conversationId, previousResponseId: options.previousResponseId, }) : undefined; let handedInputToModel = false; let streamInputPersisted = false; const persistStreamInputIfNeeded = async () => { if (streamInputPersisted || !ensureStreamInputPersisted) { return; } // Both success and error paths call this helper, so guard against multiple writes. await ensureStreamInputPersisted(); streamInputPersisted = true; }; if (serverConversationTracker && isResumedState) { serverConversationTracker.primeFromState({ originalInput: result.state._originalInput, generatedItems: result.state._generatedItems, modelResponses: result.state._modelResponses, }); } try { while (true) { const currentAgent = result.state._currentAgent; result.state._currentStep = result.state._currentStep ?? { type: 'next_step_run_again', }; if (result.state._currentStep.type === 'next_step_interruption') { logger_1.default.debug('Continuing from interruption'); if (!result.state._lastTurnResponse || !result.state._lastProcessedResponse) { throw new errors_1.UserError('No model response found in previous state', result.state); } const turnResult = await (0, runImplementation_1.resolveInterruptedTurn)(result.state._currentAgent, result.state._originalInput, result.state._generatedItems, result.state._lastTurnResponse, result.state._lastProcessedResponse, this, result.state); (0, runImplementation_1.addStepToRunResult)(result, turnResult); result.state._toolUseTracker.addToolUse(result.state._currentAgent, result.state._lastProcessedResponse.toolsUsed); result.state._originalInput = turnResult.originalInput; result.state._generatedItems = turnResult.generatedItems; if (turnResult.nextStep.type === 'next_step_run_again') { result.state._currentTurnPersistedItemCount = 0; } result.state._currentStep = turnResult.nextStep; if (turnResult.nextStep.type === 'next_step_interruption') { // we are still in an interruption, so we need to avoid an infinite loop return; } continue; } if (result.state._currentStep.type === 'next_step_run_again') { const artifacts = await prepareAgentArtifacts(result.state); result.state._currentTurn++; result.state._currentTurnPersistedItemCount = 0; if (result.state._currentTurn > result.state._maxTurns) { result.state._currentAgentSpan?.setError({ message: 'Max turns exceeded', data: { max_turns: result.state._maxTurns }, }); throw new errors_1.MaxTurnsExceededError(`Max turns (${result.state._maxTurns}) exceeded`, result.state); } logger_1.default.debug(`Running agent ${currentAgent.name} (turn ${result.state._currentTurn})`); if (result.state._currentTurn === 1) { await this.#runInputGuardrails(result.state); } const turnInput = serverConversationTracker ? serverConversationTracker.prepareInput(result.input, result.newItems) : getTurnInput(result.input, result.newItems); if (result.state._noActiveAgentRun) { currentAgent.emit('agent_start', result.state._context, currentAgent); this.emit('agent_start', result.state._context, currentAgent); } let finalResponse = undefined; const preparedCall = await this.#prepareModelCall(result.state, options, artifacts, turnInput, serverConversationTracker, sessionInputUpdate); handedInputToModel = true; await persistStreamInputIfNeeded(); for await (const event of preparedCall.model.getStreamedResponse({ systemInstructions: preparedCall.modelInput.instructions, prompt: preparedCall.prompt, // Streaming requests should also honor explicitly chosen models. ...(preparedCall.explictlyModelSet ? { overridePromptModel: true } : {}), input: preparedCall.modelInput.input, previousResponseId: preparedCall.previousResponseId, conversationId: preparedCall.conversationId, modelSettings: preparedCall.modelSettings, tools: preparedCall.serializedTools, handoffs: preparedCall.serializedHandoffs, outputType: (0, tools_1.convertAgentOutputTypeToSerializable)(currentAgent.outputType), tracing: getTracing(this.config.tracingDisabled, this.config.traceIncludeSensitiveData), signal: options.signal, })) { if (event.type === 'response_done') { const parsed = protocol_1.StreamEventResponseCompleted.parse(event); finalResponse = { usage: new usage_1.Usage(parsed.response.usage), output: parsed.response.output, responseId: parsed.response.id, }; } if (result.cancelled) { // When the user's code exits a loop to consume the stream, we need to break // this loop to prevent internal false errors and unnecessary processing return; } result._addItem(new events_1.RunRawModelStreamEvent(event)); } result.state._noActiveAgentRun = false; if (!finalResponse) { throw new errors_1.ModelBehaviorError('Model did not produce a final response!', result.state); } result.state._lastTurnResponse = finalResponse; // Keep the tracker in sync with the streamed response so reconnections remain accurate. serverConversationTracker?.trackServerItems(finalResponse); result.state._modelResponses.push(result.state._lastTurnResponse); const processedResponse = (0, runImplementation_1.processModelResponse)(result.state._lastTurnResponse, currentAgent, preparedCall.tools, preparedCall.handoffs); result.state._lastProcessedResponse = processedResponse; // Record the items emitted directly from the model response so we do not // stream them again after tools and other side effects finish. const preToolItems = new Set(processedResponse.newItems); if (preToolItems.size > 0) { (0, runImplementation_1.streamStepItemsToRunResult)(result, processedResponse.newItems); } const turnResult = await (0, runImplementation_1.resolveTurnAfterModelResponse)(currentAgent, result.state._originalInput, result.state._generatedItems, result.state._lastTurnResponse, result.state._lastProcessedResponse, this, result.state); (0, runImplementation_1.addStepToRunResult)(result, turnResult, { skipItems: preToolItems, }); result.state._toolUseTracker.addToolUse(currentAgent, processedResponse.toolsUsed); result.state._originalInput = turnResult.originalInput; result.state._generatedItems = turnResult.generatedItems; if (turnResult.nextStep.type === 'next_step_run_again') { result.state._currentTurnPersistedItemCount = 0; } result.state._currentStep = turnResult.nextStep; } if (result.state._currentStep.type === 'next_step_final_output') { await this.#runOutputGuardrails(result.state, result.state._currentStep.output); await persistStreamInputIfNeeded(); // Guardrails must succeed before persisting session memory to avoid storing blocked outputs. if (!serverManagesConversation) { await (0, runImplementation_1.saveStreamResultToSession)(options.session, result); } this.emit('agent_end', result.state._context, currentAgent, result.state._currentStep.output); currentAgent.emit('agent_end', result.state._context, result.state._currentStep.output); return; } else if (result.state._currentStep.type === 'next_step_interruption') { // we are done for now. Don't run any output guardrails await persistStreamInputIfNeeded(); if (!serverManagesConversation) { await (0, runImplementation_1.saveStreamResultToSession)(options.session, result); } return; } else if (result.state._currentStep.type === 'next_step_handoff') { result.state._currentAgent = result.state._currentStep ?.newAgent; if (result.state._currentAgentSpan) { result.state._currentAgentSpan.end(); (0, context_1.resetCurrentSpan)(); } result.state._currentAgentSpan = undefined; result._addItem(new events_1.RunAgentUpdatedStreamEvent(result.state._currentAgent)); result.state._noActiveAgentRun = true; // we've processed the handoff, so we need to run the loop again result.state._currentStep = { type: 'next_step_run_again', }; } else { logger_1.default.debug('Running next loop'); } } } catch (error) { if (handedInputToModel && !streamInputPersisted) { await persistStreamInputIfNeeded(); } if (result.state._currentAgentSpan) { result.state._currentAgentSpan.setError({ message: 'Error in agent run', data: { error: String(error) }, }); } throw error; } finally { if (result.state._currentAgentSpan) { if (result.state._currentStep?.type !== 'next_step_interruption') { result.state._currentAgentSpan.end(); } (0, context_1.resetCurrentSpan)(); } } } /** * @internal */ async #runIndividualStream(agent, input, options, ensureStreamInputPersisted, sessionInputUpdate) { options = options ?? {}; return (0, context_1.withNewSpanContext)(async () => { // Initialize or reuse existing state const isResumedState = input instanceof runState_1.RunState; const state = isResumedState ? input : new runState_1.RunState(options.context instanceof runContext_1.RunContext ? options.context : new runContext_1.RunContext(options.context), input, agent, options.maxTurns ?? DEFAULT_MAX_TURNS); // Initialize the streamed result with existing state const result = new result_1.StreamedRunResult({ signal: options.signal, state, }); // Setup defaults result.maxTurns = options.maxTurns ?? state._maxTurns; // Continue the stream loop without blocking const streamLoopPromise = this.#runStreamLoop(result, options, isResumedState, ensureStreamInputPersisted, sessionInputUpdate).then(() => { result._done(); }, (err) => { result._raiseError(err); }); // Attach the stream loop promise so trace end waits for the loop to complete result._setStreamLoopPromise(streamLoopPromise); return result; }); } async #runInputGuardrails(state) { const guardrails = this.inputGuardrailDefs.concat(state._currentAgent.inputGuardrails.map(guardrail_1.defineInputGuardrail)); if (guardrails.length > 0) { const guardrailArgs = { agent: state._currentAgent, input: state._originalInput, context: state._context, }; try { const results = await Promise.all(guardrails.map(async (guardrail) => { return (0, tracing_1.withGuardrailSpan)(async (span) => { const result = await guardrail.run(guardrailArgs); span.spanData.triggered = result.output.tripwireTriggered; return result; }, { data: { name: guardrail.name } }, state._currentAgentSpan); })); for (const result of results) { if (result.output.tripwireTriggered) { if (state._currentAgentSpan) { state._currentAgentSpan.setError({ message: 'Guardrail tripwire triggered', data: { guardrail: result.guardrail.name }, }); } throw new errors_1.InputGuardrailTripwireTriggered(`Input guardrail triggered: ${JSON.stringify(result.output.outputInfo)}`, result, state); } } } catch (e) { if (e instanceof errors_1.InputGuardrailTripwireTriggered) { throw e; } // roll back the current turn to enable reruns state._currentTurn--; throw new errors_1.GuardrailExecutionError(`Input guardrail failed to complete: ${e}`, e, state); } } } async #runOutputGuardrails(state, output) { const guardrails = this.outputGuardrailDefs.concat(state._currentAgent.outputGuardrails.map(guardrail_1.defineOutputGuardrail)); if (guardrails.length > 0) { const agentOutput = state._currentAgent.processFinalOutput(output); const guardrailArgs = { agent: state._currentAgent, agentOutput, context: state._context, details: { modelResponse: state._lastTurnResponse }, }; try { const results = await Promise.all(guardrails.map(async (guardrail) => { return (0, tracing_1.withGuardrailSpan)(async (span) => { const result = await guardrail.run(guardrailArgs); span.spanData.triggered = result.output.tripwireTriggered; return result; }, { data: { name: guardrail.name } }, state._currentAgentSpan); })); for (const result of results) { if (result.output.tripwireTriggered) { if (state._currentAgentSpan) { state._currentAgentSpan.setError({ message: 'Guardrail tripwire triggered', data: { guardrail: result.guardrail.name }, }); } throw new errors_1.OutputGuardrailTripwireTriggered(`Output guardrail triggered: ${JSON.stringify(result.output.outputInfo)}`, result, state); } } } catch (e) { if (e instanceof errors_1.OutputGuardrailTripwireTriggered) { throw e; } throw new errors_1.GuardrailExecutionError(`Output guardrail failed to complete: ${e}`, e, state); } } } /** * @internal * Applies call-level filters and merges session updates so the model request mirrors exactly * what we persisted for history. */ async #prepareModelCall(state, options, artifacts, turnInput, serverConversationTracker, sessionInputUpdate) { const { model, explictlyModelSet } = await this.#resolveModelForAgent(state._currentAgent); let modelSettings = { ...this.config.modelSettings, ...state._currentAgent.modelSettings, }; modelSettings = adjustModelSettingsForNonGPT5RunnerModel(explictlyModelSet, state._currentAgent.modelSettings, model, modelSettings); modelSettings = (0, runImplementation_1.maybeResetToolChoice)(state._currentAgent, state._toolUseTracker, modelSettings); const systemInstructions = await state._currentAgent.getSystemPrompt(state._context); const prompt = await state._currentAgent.getPrompt(state._context); const { modelInput, sourceItems, persistedItems, filterApplied } = await applyCallModelInputFilter(state._currentAgent, options.callModelInputFilter, state._context, turnInput, systemInstructions); // Inform the tracker which exact original objects made it to the provider so future turns // only send the delta that has not yet been acknowledged by the server. serverConversationTracker?.markInputAsSent(sourceItems); // Provide filtered clones whenever filters run so session history mirrors the model payload. // Returning an empty array is intentional: it tells the session layer to persist "nothing" // instead of falling back to the unfiltered originals when the filter redacts everything. sessionInputUpdate?.(sourceItems, filterApplied ? persistedItems : undefined); const previousResponseId = serverConversationTracker?.previousResponseId ?? options.previousResponseId; const conversationId = serverConversationTracker?.conversationId ?? options.conversationId; return { ...artifacts, model, explictlyModelSet, modelSettings, modelInput, prompt, previousResponseId, conversationId, }; } } exports.Runner = Runner; /** * Constructs the model input array for the current turn by combining the original turn input with * any new run items (excluding tool approval placeholders). This helps ensure that repeated calls * to the Responses API only send newly generated content. * * See: https://platform.openai.com/docs/guides/conversation-state?api-mode=responses. */ function getTurnInput(originalInput, generatedItems) { const rawItems = generatedItems .filter((item) => item.type !== 'tool_approval_item') // don't include approval items to avoid double function calls .map((item) => item.rawItem); return [...toAgentInputList(originalInput), ...rawItems]; } // -------------------------------------------------------------- // Internal helpers // -------------------------------------------------------------- const DEFAULT_MAX_TURNS = 10; let _defaultRunner = undefined; function getDefaultRunner() { if (_defaultRunner) { return _defaultRunner; } _defaultRunner = new Runner(); return _defaultRunner; } /** * Resolves the effective model for the next turn by giving precedence to the agent-specific * configuration when present, otherwise falling back to the runner-level default. */ function selectModel(agentModel, runConfigModel) { // When initializing an agent without model name, the model property is set to an empty string. So, // * agentModel === Agent.DEFAULT_MODEL_PLACEHOLDER & runConfigModel exists, runConfigModel will be used // * agentModel is set, the agentModel will be used over runConfigModel if ((typeof agentModel === 'string' && agentModel !== agent_1.Agent.DEFAULT_MODEL_PLACEHOLDER) || agentModel // any truthy value ) { return agentModel; } return runConfigModel ?? agentModel ?? agent_1.Agent.DEFAULT_MODEL_PLACEHOLDER; } /** * Normalizes tracing configuration into the format expected by model providers. * Returns `false` to disable tracing, `true` to include full payload data, or * `'enabled_without_data'` to omit sensitive content while still emitting spans. */ function getTracing(tracingDisabled, traceIncludeSensitiveData) { if (tracingDisabled) { return false; } if (traceIncludeSensitiveData) { return true; } return 'enabled_without_data'; } /** * @internal */ async function applyCallModelInputFilter(agent, callModelInputFilter, context, inputItems, systemInstructions) { const cloneInputItems = (items, map) => items.map((item) => { const cloned = structuredClone(item); if (map && cloned && typeof cloned === 'object') { map.set(cloned, item); } return cloned; }); // Record the relationship between the cloned array passed to filters and the original inputs. const cloneMap = new WeakMap(); const originalPool = buildAgentInputPool(inputItems); const fallbackOriginals = []; // Track any original object inputs so filtered replacements can still mark them as delivered. for (const item of inputItems) { if (item && typeof item === 'object') { fallbackOriginals.push(item); } } const removeFromFallback = (candidate) => { if (!candidate || typeof candidate !== 'object') { return; } const index = fallbackOriginals.findIndex((original) => original === candidate); if (index !== -